From da3a27bc02d8649fd1cb7ba02f30c77323afdce9 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Wed, 29 Apr 2026 15:22:42 +0300 Subject: [PATCH 1/7] fix: sort attendees in one canonical order across all consumers --- .../sections/AttendeeField.tsx | 36 ++++++++++--------- .../ReportActionItem/MoneyRequestView.tsx | 36 ++++++++++--------- src/libs/TransactionUtils/index.ts | 10 ++++-- tests/unit/MergeTransactionUtilsTest.ts | 4 +-- tests/unit/TransactionUtilsTest.ts | 32 +++++++++++++++++ 5 files changed, 82 insertions(+), 36 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx b/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx index f05c57dd78f0..5c9ba1e1b630 100644 --- a/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx @@ -33,7 +33,25 @@ function AttendeeField({formattedAmountPerAttendee, isReadOnly, transactionID, a const personalDetailsList = usePersonalDetails(); const shouldDisplayAttendeesError = formError === 'violations.missingAttendees'; - const iouAttendees = getAttendees(transaction, currentUserPersonalDetails); + const rawIouAttendees = getAttendees(transaction, currentUserPersonalDetails); + // Enrich + sort once so pills and accessibility label share one canonical order. + const iouAttendees = Array.isArray(rawIouAttendees) + ? sortAlphabetically( + rawIouAttendees.map((a) => { + const pd = a?.accountID ? personalDetailsList?.[a.accountID] : undefined; + const freshAvatar = typeof pd?.avatar === 'string' ? pd.avatar : undefined; + return { + ...a, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string + displayName: pd?.displayName || a?.displayName, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string + avatarUrl: freshAvatar || a?.avatarUrl, + }; + }), + 'displayName', + localeCompare, + ) + : rawIouAttendees; return ( { - const pd = a?.accountID ? personalDetailsList?.[a.accountID] : undefined; - const freshAvatar = typeof pd?.avatar === 'string' ? pd.avatar : undefined; - return { - ...a, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - displayName: pd?.displayName || a?.displayName, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - avatarUrl: freshAvatar || a?.avatarUrl, - }; - }), - 'displayName', - localeCompare, - ).map((a) => ({ + users={iouAttendees.map((a) => ({ avatar: a?.avatarUrl, displayName: a?.displayName ?? a?.login ?? a?.email ?? '', accountID: a?.accountID, diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 28ccbf212f4c..e6ba71d04ba2 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -284,7 +284,25 @@ function MoneyRequestView({ const isTransactionScanning = isScanning(updatedTransaction ?? transaction); const hasRoute = hasRouteTransactionUtils(transactionBackup ?? transaction, isDistanceRequest); - const actualAttendees = isFromMergeTransaction && updatedTransaction ? updatedTransaction.comment?.attendees : transactionAttendees; + const rawActualAttendees = isFromMergeTransaction && updatedTransaction ? updatedTransaction.comment?.attendees : transactionAttendees; + // Enrich + sort once so pills, hover-Copy value, accessibility label, and violation check share one canonical order. + const actualAttendees = Array.isArray(rawActualAttendees) + ? sortAlphabetically( + rawActualAttendees.map((a) => { + const pd = a?.accountID ? personalDetailsList?.[a.accountID] : undefined; + const freshAvatar = typeof pd?.avatar === 'string' ? pd.avatar : undefined; + return { + ...a, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string + displayName: pd?.displayName || a?.displayName, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string + avatarUrl: freshAvatar || a?.avatarUrl, + }; + }), + 'displayName', + localeCompare, + ) + : rawActualAttendees; // Use the updated transaction amount in merge flow to have correct positive/negative sign const actualAmount = isFromMergeTransaction && updatedTransaction ? updatedTransaction.amount : transactionAmount; @@ -1178,21 +1196,7 @@ function MoneyRequestView({ titleComponent={ Array.isArray(actualAttendees) ? ( { - const pd = a?.accountID ? personalDetailsList?.[a.accountID] : undefined; - const freshAvatar = typeof pd?.avatar === 'string' ? pd.avatar : undefined; - return { - ...a, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - displayName: pd?.displayName || a?.displayName, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - avatarUrl: freshAvatar || a?.avatarUrl, - }; - }), - 'displayName', - localeCompare, - ).map((a) => ({ + users={actualAttendees.map((a) => ({ avatar: a?.avatarUrl, displayName: a?.displayName ?? a?.login ?? a?.email ?? '', accountID: a?.accountID, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index f13057593ecf..a9ef5ae064dc 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1201,11 +1201,17 @@ function getAttendees(transaction: OnyxInputOrEntry, currentUserPer return attendees; } +// Mirrors LocaleContextProvider so output here matches the user-facing pill order. +const ATTENDEES_DISPLAY_COLLATOR = new Intl.Collator(undefined, {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}); + /** - * Return the list of attendees as a string of display names/logins. + * Return the attendees list as an alphabetically sorted display string. Sorting here keeps every consumer in sync. */ function getAttendeesListDisplayString(attendees: Attendee[]): string { - return attendees.map((item) => item.displayName ?? item.login).join(', '); + return [...attendees] + .sort((a, b) => ATTENDEES_DISPLAY_COLLATOR.compare(a.displayName ?? a.login ?? '', b.displayName ?? b.login ?? '')) + .map((item) => item.displayName ?? item.login) + .join(', '); } /** diff --git a/tests/unit/MergeTransactionUtilsTest.ts b/tests/unit/MergeTransactionUtilsTest.ts index 8d4f4efd977f..081d4d096d25 100644 --- a/tests/unit/MergeTransactionUtilsTest.ts +++ b/tests/unit/MergeTransactionUtilsTest.ts @@ -893,7 +893,7 @@ describe('MergeTransactionUtils', () => { expect(result).toBe('Department, Engineering, Frontend'); }); - it('should return correct value for attendees field', () => { + it('should return attendees in alphabetical order regardless of insertion order', () => { const transaction = { ...createRandomTransaction(0), comment: { @@ -906,7 +906,7 @@ describe('MergeTransactionUtils', () => { }; const result = getDisplayValue('attendees', transaction, undefined, translateLocal); - expect(result).toBe('Test User 2, Test User 1'); + expect(result).toBe('Test User 1, Test User 2'); }); it('should return string values directly', () => { diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 79925928b333..e024fdfb84cc 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -1391,6 +1391,38 @@ describe('TransactionUtils', () => { }); }); + describe('getAttendeesListDisplayString', () => { + it('returns attendees alphabetically regardless of insertion order (deploy blocker #89130)', () => { + const attendees: Attendee[] = [ + {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, + {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, + ]; + expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('apple, banana'); + }); + + it('uses numeric-aware sort so "User 9" comes before "User 10"', () => { + const attendees: Attendee[] = [ + {email: '10@x.com', displayName: 'User 10', avatarUrl: '', login: '10@x.com'}, + {email: '9@x.com', displayName: 'User 9', avatarUrl: '', login: '9@x.com'}, + ]; + expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('User 9, User 10'); + }); + + it('returns empty string for empty array', () => { + expect(TransactionUtils.getAttendeesListDisplayString([])).toBe(''); + }); + + it('does not mutate the input array', () => { + const attendees: Attendee[] = [ + {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, + {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, + ]; + const snapshot = [...attendees]; + TransactionUtils.getAttendeesListDisplayString(attendees); + expect(attendees).toEqual(snapshot); + }); + }); + describe('isCategoryBeingAnalyzed', () => { it('should return false for undefined transaction', () => { expect(TransactionUtils.isCategoryBeingAnalyzed(undefined)).toBe(false); From b3d80d1ee9652bdb961a65124a77659fc67347ac Mon Sep 17 00:00:00 2001 From: TaduJR Date: Wed, 29 Apr 2026 15:41:16 +0300 Subject: [PATCH 2/7] refactor: extract enrichAndSortAttendees into AttendeeUtils to remove duplication --- .../sections/AttendeeField.tsx | 21 +------ .../ReportActionItem/MoneyRequestView.tsx | 23 +------- src/libs/AttendeeUtils.ts | 35 +++++++++++- tests/unit/AttendeeUtilsTest.ts | 56 ++++++++++++++++++- 4 files changed, 93 insertions(+), 42 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx b/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx index 5c9ba1e1b630..93150ddf8462 100644 --- a/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx @@ -6,8 +6,8 @@ import UserPills from '@components/UserPills'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {enrichAndSortAttendees} from '@libs/AttendeeUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {sortAlphabetically} from '@libs/OptionsListUtils'; import {getAttendees} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {IOUAction, IOUType} from '@src/CONST'; @@ -34,24 +34,7 @@ function AttendeeField({formattedAmountPerAttendee, isReadOnly, transactionID, a const shouldDisplayAttendeesError = formError === 'violations.missingAttendees'; const rawIouAttendees = getAttendees(transaction, currentUserPersonalDetails); - // Enrich + sort once so pills and accessibility label share one canonical order. - const iouAttendees = Array.isArray(rawIouAttendees) - ? sortAlphabetically( - rawIouAttendees.map((a) => { - const pd = a?.accountID ? personalDetailsList?.[a.accountID] : undefined; - const freshAvatar = typeof pd?.avatar === 'string' ? pd.avatar : undefined; - return { - ...a, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string - displayName: pd?.displayName || a?.displayName, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string - avatarUrl: freshAvatar || a?.avatarUrl, - }; - }), - 'displayName', - localeCompare, - ) - : rawIouAttendees; + const iouAttendees = enrichAndSortAttendees(rawIouAttendees, personalDetailsList, localeCompare); return ( { - const pd = a?.accountID ? personalDetailsList?.[a.accountID] : undefined; - const freshAvatar = typeof pd?.avatar === 'string' ? pd.avatar : undefined; - return { - ...a, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string - displayName: pd?.displayName || a?.displayName, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string - avatarUrl: freshAvatar || a?.avatarUrl, - }; - }), - 'displayName', - localeCompare, - ) - : rawActualAttendees; + const actualAttendees = enrichAndSortAttendees(rawActualAttendees, personalDetailsList, localeCompare); // Use the updated transaction amount in merge flow to have correct positive/negative sign const actualAmount = isFromMergeTransaction && updatedTransaction ? updatedTransaction.amount : transactionAmount; diff --git a/src/libs/AttendeeUtils.ts b/src/libs/AttendeeUtils.ts index 63b85b991076..cafc3f5f3f0c 100644 --- a/src/libs/AttendeeUtils.ts +++ b/src/libs/AttendeeUtils.ts @@ -1,8 +1,10 @@ +import type {OnyxEntry} from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; -import type {PolicyCategories, PolicyCategory} from '@src/types/onyx'; +import type {PersonalDetailsList, PolicyCategories, PolicyCategory} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails'; +import {sortAlphabetically} from './OptionsListUtils'; function getNormalizedString(value?: string): string | undefined { const normalizedValue = value?.trim(); @@ -133,4 +135,33 @@ function syncMissingAttendeesViolation( return violations; } -export {formatRequiredFieldsTitle, getIsMissingAttendeesViolation, normalizeAttendee, normalizeAttendees, syncMissingAttendeesViolation}; +/** + * Enrich each attendee with the live displayName/avatar from `personalDetails` and return them sorted alphabetically. + * Centralised so every attendee renderer (pills, copy value, accessibility label, violation check) shares one canonical order. + */ +function enrichAndSortAttendees( + attendees: Attendee[] | undefined, + personalDetailsList: OnyxEntry, + localeCompare: LocaleContextProps['localeCompare'], +): Attendee[] | undefined { + if (!Array.isArray(attendees)) { + return attendees; + } + return sortAlphabetically( + attendees.map((a) => { + const pd = a?.accountID ? personalDetailsList?.[a.accountID] : undefined; + const freshAvatar = typeof pd?.avatar === 'string' ? pd.avatar : undefined; + return { + ...a, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string + displayName: pd?.displayName || a?.displayName, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional || to fall back when personalDetails has an empty string + avatarUrl: freshAvatar || a?.avatarUrl, + }; + }), + 'displayName', + localeCompare, + ); +} + +export {enrichAndSortAttendees, formatRequiredFieldsTitle, getIsMissingAttendeesViolation, normalizeAttendee, normalizeAttendees, syncMissingAttendeesViolation}; diff --git a/tests/unit/AttendeeUtilsTest.ts b/tests/unit/AttendeeUtilsTest.ts index 1120a3252299..0e0656cb7464 100644 --- a/tests/unit/AttendeeUtilsTest.ts +++ b/tests/unit/AttendeeUtilsTest.ts @@ -1,4 +1,5 @@ -import {normalizeAttendee, normalizeAttendees} from '@libs/AttendeeUtils'; +import {enrichAndSortAttendees, normalizeAttendee, normalizeAttendees} from '@libs/AttendeeUtils'; +import type {PersonalDetailsList} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; describe('AttendeeUtils', () => { @@ -71,4 +72,57 @@ describe('AttendeeUtils', () => { ]); }); }); + + describe('enrichAndSortAttendees', () => { + const localeCompare = (a: string, b: string) => a.localeCompare(b); + + it('returns input as-is when it is not an array', () => { + expect(enrichAndSortAttendees(undefined, undefined, localeCompare)).toBeUndefined(); + }); + + it('sorts alphabetically by stored displayName when no personalDetails are available', () => { + const attendees: Attendee[] = [ + {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, + {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, + ]; + + expect(enrichAndSortAttendees(attendees, undefined, localeCompare)?.map((a) => a.displayName)).toEqual(['apple', 'banana']); + }); + + it('enriches displayName and avatar from personalDetails when accountID matches', () => { + const attendees: Attendee[] = [{accountID: 1, displayName: 'Old', avatarUrl: 'old.png'} as Attendee]; + const personalDetailsList = {1: {accountID: 1, displayName: 'New', avatar: 'new.png'}} as unknown as PersonalDetailsList; + + const result = enrichAndSortAttendees(attendees, personalDetailsList, localeCompare); + + expect(result?.[0].displayName).toBe('New'); + expect(result?.[0].avatarUrl).toBe('new.png'); + }); + + it('falls back to stored value when personalDetails has empty strings', () => { + const attendees: Attendee[] = [{accountID: 1, displayName: 'Stored', avatarUrl: 'stored.png'} as Attendee]; + const personalDetailsList = {1: {accountID: 1, displayName: '', avatar: ''}} as unknown as PersonalDetailsList; + + const result = enrichAndSortAttendees(attendees, personalDetailsList, localeCompare); + + expect(result?.[0].displayName).toBe('Stored'); + expect(result?.[0].avatarUrl).toBe('stored.png'); + }); + + it('sorts using enriched displayName so a profile rename moves the pill', () => { + const attendees: Attendee[] = [{accountID: 1, displayName: 'alice', avatarUrl: ''} as Attendee, {accountID: 2, displayName: 'bob', avatarUrl: ''} as Attendee]; + const personalDetailsList = {1: {accountID: 1, displayName: 'zoe'}} as unknown as PersonalDetailsList; + + expect(enrichAndSortAttendees(attendees, personalDetailsList, localeCompare)?.map((a) => a.displayName)).toEqual(['bob', 'zoe']); + }); + + it('does not mutate the input array', () => { + const attendees: Attendee[] = [{displayName: 'banana', avatarUrl: ''} as Attendee, {displayName: 'apple', avatarUrl: ''} as Attendee]; + const snapshot = [...attendees]; + + enrichAndSortAttendees(attendees, undefined, localeCompare); + + expect(attendees).toEqual(snapshot); + }); + }); }); From 9c0c801dcd1e1eb46c63aa89bf5db92442c14bc6 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Wed, 29 Apr 2026 16:41:53 +0300 Subject: [PATCH 3/7] fix: sort attendees with the app's preferred locale --- src/libs/AttendeeUtils.ts | 13 +++++++---- src/libs/TransactionUtils/index.ts | 20 +++++++++++++---- tests/unit/AttendeeUtilsTest.ts | 35 +++++++++++++++++++----------- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/libs/AttendeeUtils.ts b/src/libs/AttendeeUtils.ts index cafc3f5f3f0c..f92c621d5a57 100644 --- a/src/libs/AttendeeUtils.ts +++ b/src/libs/AttendeeUtils.ts @@ -136,14 +136,19 @@ function syncMissingAttendeesViolation( } /** - * Enrich each attendee with the live displayName/avatar from `personalDetails` and return them sorted alphabetically. - * Centralised so every attendee renderer (pills, copy value, accessibility label, violation check) shares one canonical order. + * Enrich each attendee with live `personalDetails` and return them sorted alphabetically by displayName. */ +function enrichAndSortAttendees(attendees: Attendee[], personalDetailsList: OnyxEntry, localeCompare: LocaleContextProps['localeCompare']): Attendee[]; function enrichAndSortAttendees( - attendees: Attendee[] | undefined, + attendees: Attendee[] | string | undefined, + personalDetailsList: OnyxEntry, + localeCompare: LocaleContextProps['localeCompare'], +): Attendee[] | string | undefined; +function enrichAndSortAttendees( + attendees: Attendee[] | string | undefined, personalDetailsList: OnyxEntry, localeCompare: LocaleContextProps['localeCompare'], -): Attendee[] | undefined { +): Attendee[] | string | undefined { if (!Array.isArray(attendees)) { return attendees; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index a9ef5ae064dc..6baa37cacd4d 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1201,15 +1201,27 @@ function getAttendees(transaction: OnyxInputOrEntry, currentUserPer return attendees; } -// Mirrors LocaleContextProvider so output here matches the user-facing pill order. -const ATTENDEES_DISPLAY_COLLATOR = new Intl.Collator(undefined, {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}); +// Mirrors LocaleContextProvider (same options + same IntlStore source) so non-React callers sort by the user's preferred locale. +const ATTENDEES_DISPLAY_COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}; +const attendeesDisplayCollatorCache = new Map(); + +function getAttendeesDisplayCollator(): Intl.Collator { + const locale = IntlStore.getCurrentLocale() ?? ''; + let collator = attendeesDisplayCollatorCache.get(locale); + if (!collator) { + collator = new Intl.Collator(locale || undefined, ATTENDEES_DISPLAY_COLLATOR_OPTIONS); + attendeesDisplayCollatorCache.set(locale, collator); + } + return collator; +} /** - * Return the attendees list as an alphabetically sorted display string. Sorting here keeps every consumer in sync. + * Returns attendees joined as an alphabetically sorted display string. Sort is part of the contract — every consumer relies on it. */ function getAttendeesListDisplayString(attendees: Attendee[]): string { + const collator = getAttendeesDisplayCollator(); return [...attendees] - .sort((a, b) => ATTENDEES_DISPLAY_COLLATOR.compare(a.displayName ?? a.login ?? '', b.displayName ?? b.login ?? '')) + .sort((a, b) => collator.compare(a.displayName ?? a.login ?? '', b.displayName ?? b.login ?? '')) .map((item) => item.displayName ?? item.login) .join(', '); } diff --git a/tests/unit/AttendeeUtilsTest.ts b/tests/unit/AttendeeUtilsTest.ts index 0e0656cb7464..f234a39e222b 100644 --- a/tests/unit/AttendeeUtilsTest.ts +++ b/tests/unit/AttendeeUtilsTest.ts @@ -86,38 +86,47 @@ describe('AttendeeUtils', () => { {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, ]; - expect(enrichAndSortAttendees(attendees, undefined, localeCompare)?.map((a) => a.displayName)).toEqual(['apple', 'banana']); + expect(enrichAndSortAttendees(attendees, undefined, localeCompare).map((a) => a.displayName)).toEqual(['apple', 'banana']); }); it('enriches displayName and avatar from personalDetails when accountID matches', () => { - const attendees: Attendee[] = [{accountID: 1, displayName: 'Old', avatarUrl: 'old.png'} as Attendee]; - const personalDetailsList = {1: {accountID: 1, displayName: 'New', avatar: 'new.png'}} as unknown as PersonalDetailsList; + const accountID = 1; + const attendees: Attendee[] = [{accountID, displayName: 'Old', avatarUrl: 'old.png'}]; + const personalDetailsList: PersonalDetailsList = {[accountID]: {accountID, displayName: 'New', avatar: 'new.png'}}; const result = enrichAndSortAttendees(attendees, personalDetailsList, localeCompare); - expect(result?.[0].displayName).toBe('New'); - expect(result?.[0].avatarUrl).toBe('new.png'); + expect(result.at(0)?.displayName).toBe('New'); + expect(result.at(0)?.avatarUrl).toBe('new.png'); }); it('falls back to stored value when personalDetails has empty strings', () => { - const attendees: Attendee[] = [{accountID: 1, displayName: 'Stored', avatarUrl: 'stored.png'} as Attendee]; - const personalDetailsList = {1: {accountID: 1, displayName: '', avatar: ''}} as unknown as PersonalDetailsList; + const accountID = 1; + const attendees: Attendee[] = [{accountID, displayName: 'Stored', avatarUrl: 'stored.png'}]; + const personalDetailsList: PersonalDetailsList = {[accountID]: {accountID, displayName: '', avatar: ''}}; const result = enrichAndSortAttendees(attendees, personalDetailsList, localeCompare); - expect(result?.[0].displayName).toBe('Stored'); - expect(result?.[0].avatarUrl).toBe('stored.png'); + expect(result.at(0)?.displayName).toBe('Stored'); + expect(result.at(0)?.avatarUrl).toBe('stored.png'); }); it('sorts using enriched displayName so a profile rename moves the pill', () => { - const attendees: Attendee[] = [{accountID: 1, displayName: 'alice', avatarUrl: ''} as Attendee, {accountID: 2, displayName: 'bob', avatarUrl: ''} as Attendee]; - const personalDetailsList = {1: {accountID: 1, displayName: 'zoe'}} as unknown as PersonalDetailsList; + const renamedAccountID = 1; + const attendees: Attendee[] = [ + {accountID: renamedAccountID, displayName: 'alice', avatarUrl: ''}, + {accountID: 2, displayName: 'bob', avatarUrl: ''}, + ]; + const personalDetailsList: PersonalDetailsList = {[renamedAccountID]: {accountID: renamedAccountID, displayName: 'zoe'}}; - expect(enrichAndSortAttendees(attendees, personalDetailsList, localeCompare)?.map((a) => a.displayName)).toEqual(['bob', 'zoe']); + expect(enrichAndSortAttendees(attendees, personalDetailsList, localeCompare).map((a) => a.displayName)).toEqual(['bob', 'zoe']); }); it('does not mutate the input array', () => { - const attendees: Attendee[] = [{displayName: 'banana', avatarUrl: ''} as Attendee, {displayName: 'apple', avatarUrl: ''} as Attendee]; + const attendees: Attendee[] = [ + {displayName: 'banana', avatarUrl: ''}, + {displayName: 'apple', avatarUrl: ''}, + ]; const snapshot = [...attendees]; enrichAndSortAttendees(attendees, undefined, localeCompare); From 1f85c8b38dda4d0c2807f80b95dc434f6cb95a40 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Wed, 29 Apr 2026 17:02:25 +0300 Subject: [PATCH 4/7] fix: lowercase attendees before comparing so joined string matches pill order --- src/libs/TransactionUtils/index.ts | 3 ++- tests/unit/TransactionUtilsTest.ts | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 6baa37cacd4d..e3efe1a5690d 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1220,8 +1220,9 @@ function getAttendeesDisplayCollator(): Intl.Collator { */ function getAttendeesListDisplayString(attendees: Attendee[]): string { const collator = getAttendeesDisplayCollator(); + // Lowercase to match sortAlphabetically (the pill sort) so joined string and pill order never disagree on case. return [...attendees] - .sort((a, b) => collator.compare(a.displayName ?? a.login ?? '', b.displayName ?? b.login ?? '')) + .sort((a, b) => collator.compare((a.displayName ?? a.login ?? '').toLowerCase(), (b.displayName ?? b.login ?? '').toLowerCase())) .map((item) => item.displayName ?? item.login) .join(', '); } diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index e024fdfb84cc..dcb62dd8cd09 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -1408,6 +1408,14 @@ describe('TransactionUtils', () => { expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('User 9, User 10'); }); + it('compares case-insensitively so the joined string matches pill order', () => { + const attendees: Attendee[] = [ + {email: 'b@x.com', displayName: 'Bob', avatarUrl: '', login: 'b@x.com'}, + {email: 'a@x.com', displayName: 'alice', avatarUrl: '', login: 'a@x.com'}, + ]; + expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('alice, Bob'); + }); + it('returns empty string for empty array', () => { expect(TransactionUtils.getAttendeesListDisplayString([])).toBe(''); }); From a8234f840e88e21647ed22d6968dd5a20b746b1d Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 30 Apr 2026 07:46:30 +0300 Subject: [PATCH 5/7] refactor: thread localeCompare through utils --- src/libs/Localize/index.ts | 20 +- src/libs/MergeTransactionUtils.ts | 16 +- src/libs/ModifiedExpenseMessage.ts | 6 +- .../LocalNotification/BrowserNotifications.ts | 3 +- src/libs/OptionsListUtils/index.ts | 3 +- src/libs/ReportNameUtils.ts | 3 +- src/libs/ReportUtils.ts | 3 +- src/libs/TransactionUtils/index.ts | 24 +- .../TransactionMerge/DetailsReviewPage.tsx | 3 +- .../report/ContextMenu/ContextMenuActions.tsx | 2 + .../actionContents/ModifiedExpenseContent.tsx | 3 +- .../ModifiedExpenseMessage.perf-test.ts | 12 +- tests/unit/MergeTransactionUtilsTest.ts | 24 +- tests/unit/ModifiedExpenseMessageTest.ts | 381 ++++++++++++++++-- tests/unit/TransactionUtilsTest.ts | 12 +- 15 files changed, 425 insertions(+), 90 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 7a6cd5cf8cef..806d64e9483d 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -199,5 +199,23 @@ function getDevicePreferredLocale(): Locale { return RNLocalize.findBestLanguageTag(Object.values(CONST.LOCALES))?.languageTag ?? CONST.LOCALES.DEFAULT; } +const COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}; +const localeCompareCache = new Map number>(); + +/** + * Locale-aware string comparator for non-React callers (mirrors `translateLocal`). React code should use the + * `localeCompare` returned by `useLocalize` instead. + */ +function localeCompareLocal(a: string, b: string): number { + const locale = IntlStore.getCurrentLocale() ?? ''; + let compare = localeCompareCache.get(locale); + if (!compare) { + const collator = new Intl.Collator(locale || undefined, COLLATOR_OPTIONS); + compare = (left, right) => collator.compare(left, right); + localeCompareCache.set(locale, compare); + } + return compare(a, b); +} + // eslint-disable-next-line @typescript-eslint/no-deprecated -export {translate, translateLocal, formatList, formatMessageElementList, getDevicePreferredLocale}; +export {translate, translateLocal, localeCompareLocal, formatList, formatMessageElementList, getDevicePreferredLocale}; diff --git a/src/libs/MergeTransactionUtils.ts b/src/libs/MergeTransactionUtils.ts index 5b376d909029..23f2d9a79ef6 100644 --- a/src/libs/MergeTransactionUtils.ts +++ b/src/libs/MergeTransactionUtils.ts @@ -498,7 +498,14 @@ function selectTargetAndSourceTransactionsForMerge( * @param translate - The translation function * @returns The formatted display string for the field value */ -function getDisplayValue(field: MergeFieldKey, transaction: Transaction, policy: Policy | undefined, translate: LocaleContextProps['translate'], reports?: Array>): string { +function getDisplayValue( + field: MergeFieldKey, + transaction: Transaction, + policy: Policy | undefined, + translate: LocaleContextProps['translate'], + localeCompare: LocaleContextProps['localeCompare'], + reports?: Array>, +): string { const fieldValue = getMergeFieldValue(getTransactionDetails(transaction), transaction, field); if (isEmptyMergeValue(fieldValue) || fieldValue === undefined) { @@ -528,7 +535,7 @@ function getDisplayValue(field: MergeFieldKey, transaction: Transaction, policy: return transaction?.reportName ?? getReportName(getReportOrDraftReport(SafeString(fieldValue), reports)); } if (field === 'attendees') { - return Array.isArray(fieldValue) ? getAttendeesListDisplayString(fieldValue) : ''; + return Array.isArray(fieldValue) ? getAttendeesListDisplayString(fieldValue, localeCompare) : ''; } if (field === 'taxValue') { @@ -554,6 +561,7 @@ function buildMergeFieldsData( targetTransactionPolicy: Policy | undefined, sourceTransactionPolicy: Policy | undefined, translate: LocaleContextProps['translate'], + localeCompare: LocaleContextProps['localeCompare'], reports: Array> = [], ): MergeFieldData[] { if (!targetTransaction || !sourceTransaction) { @@ -568,12 +576,12 @@ function buildMergeFieldsData( const options: MergeFieldOption[] = [ { transaction: targetTransaction, - displayValue: getDisplayValue(field, targetTransaction, targetTransactionPolicy, translate, reports), + displayValue: getDisplayValue(field, targetTransaction, targetTransactionPolicy, translate, localeCompare, reports), isSelected: selectedTransactionId === targetTransaction.transactionID, }, { transaction: sourceTransaction, - displayValue: getDisplayValue(field, sourceTransaction, sourceTransactionPolicy, translate, reports), + displayValue: getDisplayValue(field, sourceTransaction, sourceTransactionPolicy, translate, localeCompare, reports), isSelected: selectedTransactionId === sourceTransaction.transactionID, }, ]; diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 3e0629c82a2b..655381d2a3f0 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,7 +1,7 @@ import isEmpty from 'lodash/isEmpty'; import type {OnyxEntry} from 'react-native-onyx'; import type {Entries, ValueOf} from 'type-fest'; -import type {LocalizedTranslate} from '@components/LocaleContextProvider'; +import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Policy, PolicyTagLists, Report, ReportAction, ReportAttributesDerivedValue} from '@src/types/onyx'; @@ -246,6 +246,7 @@ function getRulesModifiedMessage( */ function getForReportAction({ translate, + localeCompare, reportAction, policy, movedFromReport, @@ -255,6 +256,7 @@ function getForReportAction({ reportAttributes, }: { translate: LocalizedTranslate; + localeCompare: LocaleContextProps['localeCompare']; reportAction: OnyxEntry; policy: OnyxEntry; movedFromReport?: OnyxEntry; @@ -452,7 +454,7 @@ function getForReportAction({ const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'newAttendees' in reportActionOriginalMessage; if (hasModifiedAttendees) { - const [oldAttendees, attendees] = getFormattedAttendees(reportActionOriginalMessage.newAttendees, reportActionOriginalMessage.oldAttendees); + const [oldAttendees, attendees] = getFormattedAttendees(localeCompare, reportActionOriginalMessage.newAttendees, reportActionOriginalMessage.oldAttendees); buildMessageFragmentForValue(translate, oldAttendees, attendees, translate('iou.attendees'), false, setFragments, removalFragments, changeFragments); } diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index 35d6780a522b..9279e5dede61 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -4,7 +4,7 @@ import type {ImageSourcePropType} from 'react-native'; import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.png'; import * as AppUpdate from '@libs/actions/AppUpdate'; // eslint-disable-next-line @typescript-eslint/no-deprecated -- translateLocal is deprecated; BrowserNotifications is non-React code that cannot use the translate hook -import {translateLocal} from '@libs/Localize'; +import {localeCompareLocal, translateLocal} from '@libs/Localize'; import {getForReportAction} from '@libs/ModifiedExpenseMessage'; import {getTextFromHtml} from '@libs/ReportActionsUtils'; import {getReportName} from '@libs/ReportNameUtils'; @@ -153,6 +153,7 @@ export default { const bodyWithHTML = getForReportAction({ // eslint-disable-next-line @typescript-eslint/no-deprecated -- translateLocal is deprecated; BrowserNotifications is non-React code that cannot use the translate hook translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy, movedFromReport, diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 5b5bd8c77f2d..fa4a071c72dd 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -15,7 +15,7 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isReportMessageAttachment} from '@libs/isReportMessageAttachment'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; // eslint-disable-next-line @typescript-eslint/no-deprecated -import {translateLocal} from '@libs/Localize'; +import {localeCompareLocal, translateLocal} from '@libs/Localize'; import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from '@libs/LoginUtils'; import {MaxHeap} from '@libs/MaxHeap'; import {MinHeap} from '@libs/MinHeap'; @@ -754,6 +754,7 @@ function getLastMessageTextForReport({ } else if (isModifiedExpenseAction(lastReportAction)) { const properSchemaForModifiedExpenseMessageWithHTML = getForReportAction({ translate, + localeCompare: localeCompareLocal, reportAction: lastReportAction, policy, movedFromReport, diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 12e319d9a277..b45644362d48 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -25,7 +25,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {convertToDisplayString} from './CurrencyUtils'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from './LocalePhoneNumber'; // eslint-disable-next-line @typescript-eslint/no-deprecated -import {translateLocal} from './Localize'; +import {localeCompareLocal, translateLocal} from './Localize'; // eslint-disable-next-line import/no-cycle import {getForReportAction, getMovedReportID} from './ModifiedExpenseMessage'; import Parser from './Parser'; @@ -835,6 +835,7 @@ function computeChatThreadReportName( const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(parentReportAction, CONST.REPORT.MOVE_TYPE.TO)}`]; const modifiedMessageWithHTML = getForReportAction({ translate, + localeCompare: localeCompareLocal, reportAction: parentReportAction, movedFromReport, movedToReport, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f058f76d96f5..6dfeb754ed8a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -111,7 +111,7 @@ import getBase62ReportID from './getBase62ReportID'; import {isReportMessageAttachment} from './isReportMessageAttachment'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from './LocalePhoneNumber'; // eslint-disable-next-line @typescript-eslint/no-deprecated -import {translateLocal} from './Localize'; +import {localeCompareLocal, translateLocal} from './Localize'; import Log from './Log'; import {isEmailPublicDomain} from './LoginUtils'; // eslint-disable-next-line import/no-cycle @@ -5862,6 +5862,7 @@ function getReportName(reportNameInformation: GetReportNameParams): string { const modifiedMessageWithHTML = getForReportAction({ // eslint-disable-next-line @typescript-eslint/no-deprecated -- translateLocal is deprecated; getReportName is non-React code that cannot use the translate hook translate: translateLocal, + localeCompare: localeCompareLocal, reportAction: parentReportAction, policy, movedFromReport, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index e3efe1a5690d..bef1657947ea 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -5,6 +5,7 @@ import lodashSet from 'lodash/set'; import type {NullishDeep, OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {Coordinate} from '@components/MapView/MapViewTypes'; import utils from '@components/MapView/utils'; import type {UnreportedExpenseListItemType} from '@components/Search/SearchList/ListItem/types'; @@ -1201,28 +1202,13 @@ function getAttendees(transaction: OnyxInputOrEntry, currentUserPer return attendees; } -// Mirrors LocaleContextProvider (same options + same IntlStore source) so non-React callers sort by the user's preferred locale. -const ATTENDEES_DISPLAY_COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}; -const attendeesDisplayCollatorCache = new Map(); - -function getAttendeesDisplayCollator(): Intl.Collator { - const locale = IntlStore.getCurrentLocale() ?? ''; - let collator = attendeesDisplayCollatorCache.get(locale); - if (!collator) { - collator = new Intl.Collator(locale || undefined, ATTENDEES_DISPLAY_COLLATOR_OPTIONS); - attendeesDisplayCollatorCache.set(locale, collator); - } - return collator; -} - /** * Returns attendees joined as an alphabetically sorted display string. Sort is part of the contract — every consumer relies on it. */ -function getAttendeesListDisplayString(attendees: Attendee[]): string { - const collator = getAttendeesDisplayCollator(); +function getAttendeesListDisplayString(attendees: Attendee[], localeCompare: LocaleContextProps['localeCompare']): string { // Lowercase to match sortAlphabetically (the pill sort) so joined string and pill order never disagree on case. return [...attendees] - .sort((a, b) => collator.compare((a.displayName ?? a.login ?? '').toLowerCase(), (b.displayName ?? b.login ?? '').toLowerCase())) + .sort((a, b) => localeCompare((a.displayName ?? a.login ?? '').toLowerCase(), (b.displayName ?? b.login ?? '').toLowerCase())) .map((item) => item.displayName ?? item.login) .join(', '); } @@ -1230,10 +1216,10 @@ function getAttendeesListDisplayString(attendees: Attendee[]): string { /** * Return the list of attendees as a string and modified list of attendees as a string if present. */ -function getFormattedAttendees(modifiedAttendees?: Attendee[], attendees?: Attendee[]): [string, string] { +function getFormattedAttendees(localeCompare: LocaleContextProps['localeCompare'], modifiedAttendees?: Attendee[], attendees?: Attendee[]): [string, string] { const oldAttendees = modifiedAttendees ?? []; const newAttendees = attendees ?? []; - return [getAttendeesListDisplayString(oldAttendees), getAttendeesListDisplayString(newAttendees)]; + return [getAttendeesListDisplayString(oldAttendees, localeCompare), getAttendeesListDisplayString(newAttendees, localeCompare)]; } /** diff --git a/src/pages/TransactionMerge/DetailsReviewPage.tsx b/src/pages/TransactionMerge/DetailsReviewPage.tsx index a7340f0afc30..dc66ad0713be 100644 --- a/src/pages/TransactionMerge/DetailsReviewPage.tsx +++ b/src/pages/TransactionMerge/DetailsReviewPage.tsx @@ -150,7 +150,7 @@ function DetailsReviewPage({route}: DetailsReviewPageProps) { // Build merge fields array with all necessary information const mergeFields = useMemo( () => - buildMergeFieldsData(conflictFields, targetTransaction, sourceTransaction, mergeTransaction, targetTransactionPolicy, sourceTransactionPolicy, translate, [ + buildMergeFieldsData(conflictFields, targetTransaction, sourceTransaction, mergeTransaction, targetTransactionPolicy, sourceTransactionPolicy, translate, localeCompare, [ targetTransactionReport, sourceTransactionReport, ]), @@ -164,6 +164,7 @@ function DetailsReviewPage({route}: DetailsReviewPageProps) { targetTransactionPolicy, sourceTransactionPolicy, translate, + localeCompare, ], ); diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index 2d23bfa75a33..23570019df91 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -18,6 +18,7 @@ import {getEnvironmentURL} from '@libs/Environment/Environment'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; +import {localeCompareLocal} from '@libs/Localize'; import {getForReportAction} from '@libs/ModifiedExpenseMessage'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -852,6 +853,7 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (isModifiedExpenseAction(reportAction)) { const modifyExpenseMessageWithHTML = getForReportAction({ translate, + localeCompare: localeCompareLocal, reportAction, policy, movedFromReport, diff --git a/src/pages/inbox/report/actionContents/ModifiedExpenseContent.tsx b/src/pages/inbox/report/actionContents/ModifiedExpenseContent.tsx index 1501c8ab14d9..14f128b575e0 100644 --- a/src/pages/inbox/report/actionContents/ModifiedExpenseContent.tsx +++ b/src/pages/inbox/report/actionContents/ModifiedExpenseContent.tsx @@ -18,7 +18,7 @@ type ModifiedExpenseContentProps = { }; function ModifiedExpenseContent({action, report, childReport, originalReport}: ModifiedExpenseContentProps) { - const {translate} = useLocalize(); + const {translate, localeCompare} = useLocalize(); const {email: currentUserEmail} = useCurrentUserPersonalDetails(); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); @@ -32,6 +32,7 @@ function ModifiedExpenseContent({action, report, childReport, originalReport}: M const modifiedExpenseMessage = getForReportAction({ translate, + localeCompare, reportAction: action, policy, movedFromReport, diff --git a/tests/perf-test/ModifiedExpenseMessage.perf-test.ts b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts index 738adf26c919..30bf826f7304 100644 --- a/tests/perf-test/ModifiedExpenseMessage.perf-test.ts +++ b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts @@ -5,6 +5,7 @@ import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, Report} from '@src/types/onyx'; +import {localeCompareLocal} from '../../src/libs/Localize'; import {getForReportAction} from '../../src/libs/ModifiedExpenseMessage'; import createCollection from '../utils/collections/createCollection'; import createRandomPolicy from '../utils/collections/policies'; @@ -65,5 +66,14 @@ test('[ModifiedExpenseMessage] getForReportAction on 1k reports and policies', a }); await waitForBatchedUpdates(); - await measureFunction(() => getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: mockedPolicyTags, currentUserLogin: CURRENT_USER_LOGIN})); + await measureFunction(() => + getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: mockedPolicyTags, + currentUserLogin: CURRENT_USER_LOGIN, + }), + ); }); diff --git a/tests/unit/MergeTransactionUtilsTest.ts b/tests/unit/MergeTransactionUtilsTest.ts index 081d4d096d25..d581dcd49ce1 100644 --- a/tests/unit/MergeTransactionUtilsTest.ts +++ b/tests/unit/MergeTransactionUtilsTest.ts @@ -825,7 +825,7 @@ describe('MergeTransactionUtils', () => { }; // When we get display value for merchant - const result = getDisplayValue('merchant', transaction, undefined, translateLocal); + const result = getDisplayValue('merchant', transaction, undefined, translateLocal, mockLocaleCompare); // Then it should return empty string expect(result).toBe(''); @@ -840,8 +840,8 @@ describe('MergeTransactionUtils', () => { }; // When we get display values for boolean fields - const reimbursableResult = getDisplayValue('reimbursable', transaction, undefined, translateLocal); - const billableResult = getDisplayValue('billable', transaction, undefined, translateLocal); + const reimbursableResult = getDisplayValue('reimbursable', transaction, undefined, translateLocal, mockLocaleCompare); + const billableResult = getDisplayValue('billable', transaction, undefined, translateLocal, mockLocaleCompare); // Then it should return translated Yes/No values expect(reimbursableResult).toBe('common.yes'); @@ -857,7 +857,7 @@ describe('MergeTransactionUtils', () => { }; // When we get display value for amount - const result = getDisplayValue('amount', transaction, undefined, translateLocal); + const result = getDisplayValue('amount', transaction, undefined, translateLocal, mockLocaleCompare); // Then it should return formatted currency string expect(result).toBe('$10.00'); @@ -873,7 +873,7 @@ describe('MergeTransactionUtils', () => { }; // When we get display value for description - const result = getDisplayValue('description', transaction, undefined, translateLocal); + const result = getDisplayValue('description', transaction, undefined, translateLocal, mockLocaleCompare); // Then it should return cleaned text without HTML and with spaces instead of line breaks expect(result).toBe('This is a test description with line breaks and more text'); @@ -887,7 +887,7 @@ describe('MergeTransactionUtils', () => { }; // When we get display value for tag - const result = getDisplayValue('tag', transaction, undefined, translateLocal); + const result = getDisplayValue('tag', transaction, undefined, translateLocal, mockLocaleCompare); // Then it should return sanitized tag names separated by commas expect(result).toBe('Department, Engineering, Frontend'); @@ -904,7 +904,7 @@ describe('MergeTransactionUtils', () => { ], }, }; - const result = getDisplayValue('attendees', transaction, undefined, translateLocal); + const result = getDisplayValue('attendees', transaction, undefined, translateLocal, mockLocaleCompare); expect(result).toBe('Test User 1, Test User 2'); }); @@ -919,8 +919,8 @@ describe('MergeTransactionUtils', () => { }; // When we get display values for string fields - const merchantResult = getDisplayValue('merchant', transaction, undefined, translateLocal); - const categoryResult = getDisplayValue('category', transaction, undefined, translateLocal); + const merchantResult = getDisplayValue('merchant', transaction, undefined, translateLocal, mockLocaleCompare); + const categoryResult = getDisplayValue('category', transaction, undefined, translateLocal, mockLocaleCompare); // Then it should return the string values expect(merchantResult).toBe('Starbucks Coffee'); @@ -935,7 +935,7 @@ describe('MergeTransactionUtils', () => { }; // When we get display value for reportID - const result = getDisplayValue('reportID', transaction, undefined, translateLocal); + const result = getDisplayValue('reportID', transaction, undefined, translateLocal, mockLocaleCompare); // Then it should return translated "None" expect(result).toBe('common.none'); @@ -950,7 +950,7 @@ describe('MergeTransactionUtils', () => { }; // When we get display value for reportID - const result = getDisplayValue('reportID', transaction, undefined, translateLocal); + const result = getDisplayValue('reportID', transaction, undefined, translateLocal, mockLocaleCompare); // Then it should return the reportName expect(result).toBe('Test Report Name'); @@ -976,7 +976,7 @@ describe('MergeTransactionUtils', () => { }; // When we get display value for reportID - const result = getDisplayValue('reportID', transaction, undefined, translateLocal); + const result = getDisplayValue('reportID', transaction, undefined, translateLocal, mockLocaleCompare); // Then it should return the report's name from Onyx expect(result).toBe(report.reportName); diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 35e16d4c8e16..290532ea0083 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -9,7 +9,7 @@ import * as ReportNameUtils from '@libs/ReportNameUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; -import {translate} from '@src/libs/Localize'; +import {localeCompareLocal, translate} from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy} from '@src/types/onyx'; import type {OriginalMessageModifiedExpense} from '@src/types/onyx/OriginalMessage'; @@ -283,7 +283,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `changed the amount to $18.00 (previously $12.55)`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -304,7 +311,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `changed the amount to $18.00 (previously $0.00)`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -327,7 +341,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55)\nremoved the description (previously "this is for the shuttle")'; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -352,7 +373,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55)\nset the category to "Benefits"\nremoved the description (previously "this is for the shuttle")'; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -375,7 +403,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55) and the merchant to "Taco Bell" (previously "Big Belly")'; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -403,7 +438,14 @@ describe('ModifiedExpenseMessage', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55) and the merchant to "Taco Bell" (previously "Big Belly")\nset the category to "Benefits"\nremoved the description (previously "this is for the shuttle")'; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -429,7 +471,14 @@ describe('ModifiedExpenseMessage', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55), the description to "I bought it on the way" (previously "from the business trip"), and the merchant to "Taco Bell" (previously "Big Belly")'; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -448,7 +497,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `removed the merchant (previously "Big Belly")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -467,7 +523,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `set the merchant to "KFC"`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -486,7 +549,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `set the merchant to "KFC"`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -505,7 +575,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct "removed" text message', () => { const expectedResult = `removed the merchant (previously "Old Merchant")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -524,7 +601,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct "changed" text message', () => { const expectedResult = `changed the merchant to "New Merchant" (previously "Old Merchant")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -545,7 +629,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `removed the description (previously "mini shore") and the merchant (previously "Big Belly")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -568,7 +659,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `removed the description (previously "mini shore"), the merchant (previously "Big Belly"), and the category (previously "Benefits")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -587,7 +685,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `set the merchant to "Big Belly"`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -608,7 +713,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `set the description to "mini shore" and the merchant to "Big Belly"`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -631,7 +743,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `set the description to "mini shore", the merchant to "Big Belly", and the category to "Benefits"`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -650,7 +769,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the date to 2023-12-27 (previously 2023-12-26)'; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -668,7 +794,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the expense'; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -690,7 +823,14 @@ describe('ModifiedExpenseMessage', () => { it('then the message says the distance is changed and shows the new and old merchant and amount', () => { const expectedResult = `changed the distance to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $7.00 (previously $0.70)`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); }); @@ -711,7 +851,14 @@ describe('ModifiedExpenseMessage', () => { it('then the message says the rate is changed and shows the new and old merchant and amount', () => { const expectedResult = `changed the rate to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $55.80 (previously $39.45)`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); }); @@ -730,7 +877,15 @@ describe('ModifiedExpenseMessage', () => { reportName: '', }; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN, movedFromReport}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + movedFromReport, + }); expect(result).toEqual(expectedResult); }); }); @@ -749,7 +904,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message with AI attribution when setting description', () => { const expectedResult = 'set the description based on past activity to "Flight to client meeting"'; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -769,7 +931,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message with AI attribution when changing description', () => { const expectedResult = 'changed the description based on past activity to "New description" (previously "Old description")'; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -789,7 +958,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message with AI attribution', () => { const expectedResult = `changed the category based on past activity to "Travel" (previously "Food")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -809,7 +985,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message with MCC attribution for non-admin', () => { const expectedResult = `changed the category based on workspace rule to "Travel" (previously "Food")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -831,7 +1014,14 @@ describe('ModifiedExpenseMessage', () => { jest.spyOn(PolicyUtils, 'isPolicyAdmin').mockReturnValue(true); - const result = getForReportAction({translate: translateLocal, reportAction, policy: mockPolicy, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: mockPolicy, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); // Verify the policyID in the URL exactly matches the policy.id (case-preserved) expect(result).toContain(`workspaces/${mockPolicy.id}/rules`); @@ -854,7 +1044,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message with AI attribution', () => { const expectedResult = `set the category based on past activity to "Travel"`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -874,7 +1071,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message without showing previously uncategorized', () => { const expectedResult = `set the category based on past activity to "6403 Travel - Member Services"`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -893,7 +1097,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the generic changed expense message since no meaningful change occurred', () => { const expectedResult = `changed the expense`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -913,7 +1124,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message with AI attribution', () => { const expectedResult = `removed the category based on past activity (previously "Travel")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -949,7 +1167,14 @@ describe('ModifiedExpenseMessage', () => { } as OriginalMessageModifiedExpense, }; - const result = getForReportAction({translate: translateLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: policyRulesPolicy, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); const expectedResult = `marked the expense as "billable", marked the expense as "reimbursable", set the category to "Travel", and merchant to "McDonald's" via workspace rules`; @@ -975,7 +1200,14 @@ describe('ModifiedExpenseMessage', () => { } as OriginalMessageModifiedExpense, }; - const result = getForReportAction({translate: translateLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: policyRulesPolicy, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); const expectedResult = `set the tax rate to "New Tax Rate" via workspace rules`; @@ -995,7 +1227,14 @@ describe('ModifiedExpenseMessage', () => { } as OriginalMessageModifiedExpense, }; - const result = getForReportAction({translate: translateLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: policyRulesPolicy, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); const expectedResult = `set the category to "Travel" and merchant to "McDonald's" via workspace rules`; @@ -1014,7 +1253,14 @@ describe('ModifiedExpenseMessage', () => { } as OriginalMessageModifiedExpense, }; - const result = getForReportAction({translate: translateLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: policyRulesPolicy, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); const expectedResult = `marked the expense as "billable" via workspace rules`; @@ -1034,7 +1280,14 @@ describe('ModifiedExpenseMessage', () => { } as OriginalMessageModifiedExpense, }; - const result = getForReportAction({translate: translateLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: policyRulesPolicy, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); const expectedResult = `marked the expense as "reimbursable" and marked the expense as "billable" via workspace rules`; @@ -1056,7 +1309,14 @@ describe('ModifiedExpenseMessage', () => { } as OriginalMessageModifiedExpense, }; - const result = getForReportAction({translate: translateLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: policyRulesPolicy, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toContain(CONST.CONFIGURE_EXPENSE_REPORT_RULES_HELP_URL); expect(result).toContain('workspace rules'); @@ -1077,7 +1337,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message without attribution', () => { const expectedResult = `changed the category to "Travel" (previously "Food")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -1097,7 +1364,14 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message without attribution', () => { const expectedResult = `changed the category to "Travel" (previously "Food")`; - const result = getForReportAction({translate: translateLocal, reportAction, policy: undefined, policyTags: undefined, currentUserLogin: CURRENT_USER_LOGIN}); + const result = getForReportAction({ + translate: translateLocal, + localeCompare: localeCompareLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserLogin: CURRENT_USER_LOGIN, + }); expect(result).toEqual(expectedResult); }); @@ -1122,6 +1396,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1136,6 +1411,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1173,6 +1449,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1198,6 +1475,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1223,6 +1501,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1248,6 +1527,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1262,6 +1542,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1296,6 +1577,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1310,6 +1592,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1344,6 +1627,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1360,6 +1644,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1392,6 +1677,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: mockPolicy, policyTags: undefined, @@ -1423,6 +1709,7 @@ describe('ModifiedExpenseMessage', () => { const expectedResult = `changed the distance to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $7.00 (previously $0.70)`; const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1448,6 +1735,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, movedFromReport, policy: undefined, @@ -1480,6 +1768,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, movedFromReport, policy, @@ -1513,6 +1802,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, movedToReport, policy, @@ -1539,6 +1829,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, movedToReport, policy: undefined, @@ -1562,6 +1853,7 @@ describe('ModifiedExpenseMessage', () => { it('returns an empty string', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1574,6 +1866,7 @@ describe('ModifiedExpenseMessage', () => { it('returns an empty string even when a valid policy is provided', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1604,6 +1897,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1618,6 +1912,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1651,6 +1946,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1665,6 +1961,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1698,6 +1995,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1712,6 +2010,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1742,6 +2041,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1756,6 +2056,7 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, + localeCompare: localeCompareLocal, reportAction, policy: { id: '123', diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index dcb62dd8cd09..52f1fae2ac57 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -1392,12 +1392,14 @@ describe('TransactionUtils', () => { }); describe('getAttendeesListDisplayString', () => { + const localeCompare = (a: string, b: string) => a.localeCompare(b, undefined, {numeric: true, sensitivity: 'variant', caseFirst: 'upper'}); + it('returns attendees alphabetically regardless of insertion order (deploy blocker #89130)', () => { const attendees: Attendee[] = [ {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, ]; - expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('apple, banana'); + expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('apple, banana'); }); it('uses numeric-aware sort so "User 9" comes before "User 10"', () => { @@ -1405,7 +1407,7 @@ describe('TransactionUtils', () => { {email: '10@x.com', displayName: 'User 10', avatarUrl: '', login: '10@x.com'}, {email: '9@x.com', displayName: 'User 9', avatarUrl: '', login: '9@x.com'}, ]; - expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('User 9, User 10'); + expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('User 9, User 10'); }); it('compares case-insensitively so the joined string matches pill order', () => { @@ -1413,11 +1415,11 @@ describe('TransactionUtils', () => { {email: 'b@x.com', displayName: 'Bob', avatarUrl: '', login: 'b@x.com'}, {email: 'a@x.com', displayName: 'alice', avatarUrl: '', login: 'a@x.com'}, ]; - expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('alice, Bob'); + expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('alice, Bob'); }); it('returns empty string for empty array', () => { - expect(TransactionUtils.getAttendeesListDisplayString([])).toBe(''); + expect(TransactionUtils.getAttendeesListDisplayString([], localeCompare)).toBe(''); }); it('does not mutate the input array', () => { @@ -1426,7 +1428,7 @@ describe('TransactionUtils', () => { {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, ]; const snapshot = [...attendees]; - TransactionUtils.getAttendeesListDisplayString(attendees); + TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare); expect(attendees).toEqual(snapshot); }); }); From e4dc39d6dd6d7060b2f4d81bcb0b7caa535ab06c Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 30 Apr 2026 08:03:41 +0300 Subject: [PATCH 6/7] refactor: shorten localeCompareLocal JSDoc to a single line --- src/libs/Localize/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 806d64e9483d..b4403a47d8b7 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -202,10 +202,7 @@ function getDevicePreferredLocale(): Locale { const COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}; const localeCompareCache = new Map number>(); -/** - * Locale-aware string comparator for non-React callers (mirrors `translateLocal`). React code should use the - * `localeCompare` returned by `useLocalize` instead. - */ +/** Mirrors `translateLocal` for non-React callers. React code should use `useLocalize().localeCompare`. */ function localeCompareLocal(a: string, b: string): number { const locale = IntlStore.getCurrentLocale() ?? ''; let compare = localeCompareCache.get(locale); From 1082c1d8d87866bb7afe0e4fe35cb31371cb2315 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Sat, 2 May 2026 22:01:08 +0300 Subject: [PATCH 7/7] fix: strip @expensify.sms from joined attendee string --- .../sections/AttendeeField.tsx | 4 +- .../ReportActionItem/MoneyRequestView.tsx | 4 +- src/libs/Localize/index.ts | 17 +---- src/libs/ModifiedExpenseMessage.ts | 6 +- .../LocalNotification/BrowserNotifications.ts | 3 +- src/libs/OptionsListUtils/index.ts | 3 +- src/libs/ReportNameUtils.ts | 3 +- src/libs/ReportUtils.ts | 3 +- src/libs/TransactionUtils/index.ts | 20 +++--- .../report/ContextMenu/ContextMenuActions.tsx | 2 - .../actionContents/ModifiedExpenseContent.tsx | 3 +- .../ModifiedExpenseMessage.perf-test.ts | 2 - tests/unit/ModifiedExpenseMessageTest.ts | 68 +------------------ tests/unit/TransactionUtilsTest.ts | 16 +++++ 14 files changed, 42 insertions(+), 112 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx b/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx index 93150ddf8462..56c8fe82f667 100644 --- a/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx @@ -8,7 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {enrichAndSortAttendees} from '@libs/AttendeeUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getAttendees} from '@libs/TransactionUtils'; +import {getAttendees, getAttendeesListDisplayString} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {IOUAction, IOUType} from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -40,7 +40,7 @@ function AttendeeField({formattedAmountPerAttendee, isReadOnly, transactionID, a a?.displayName ?? a?.login).join(', ')}`} + accessibilityLabel={`${translate('iou.attendees')}, ${Array.isArray(iouAttendees) ? getAttendeesListDisplayString(iouAttendees) : ''}`} description={`${translate('iou.attendees')} ${ iouAttendees?.length && iouAttendees.length > 1 && formattedAmountPerAttendee ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : '' }`} diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index f485a212f10d..2038610f5195 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -82,6 +82,7 @@ import { } from '@libs/ReportUtils'; import {hasEnabledTags, shouldShowDependentTagList} from '@libs/TagsOptionsListUtils'; import { + getAttendeesListDisplayString, getBillable, getCurrency, getDescription, @@ -803,7 +804,8 @@ function MoneyRequestView({ const previousTagLength = getLengthOfTag(previousTag ?? ''); const currentTagLength = getLengthOfTag(currentTransactionTag ?? ''); - const getAttendeesTitle = Array.isArray(actualAttendees) ? actualAttendees.map((item) => item?.displayName ?? item?.login).join(', ') : ''; + // actualAttendees is already sorted by enrichAndSortAttendees above; pass without localeCompare to preserve that order while stripping the SMS domain. + const getAttendeesTitle = Array.isArray(actualAttendees) ? getAttendeesListDisplayString(actualAttendees) : ''; const attendeesCopyValue = !canEdit ? getAttendeesTitle : undefined; const tagList = policyTagLists.map(({name, orderWeight, tags}, index) => { diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index be72212f58a6..21f2ec5ac1a3 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -199,20 +199,5 @@ function getDevicePreferredLocale(): Locale { return RNLocalize.findBestLanguageTag(Object.values(CONST.LOCALES))?.languageTag ?? CONST.LOCALES.DEFAULT; } -const COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}; -const localeCompareCache = new Map number>(); - -/** Mirrors `translateLocal` for non-React callers. React code should use `useLocalize().localeCompare`. */ -function localeCompareLocal(a: string, b: string): number { - const locale = IntlStore.getCurrentLocale() ?? ''; - let compare = localeCompareCache.get(locale); - if (!compare) { - const collator = new Intl.Collator(locale || undefined, COLLATOR_OPTIONS); - compare = (left, right) => collator.compare(left, right); - localeCompareCache.set(locale, compare); - } - return compare(a, b); -} - // eslint-disable-next-line @typescript-eslint/no-deprecated -export {translate, translateLocal, localeCompareLocal, formatList, formatMessageElementList, getDevicePreferredLocale}; +export {translate, translateLocal, formatList, formatMessageElementList, getDevicePreferredLocale}; diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 655381d2a3f0..3e0629c82a2b 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,7 +1,7 @@ import isEmpty from 'lodash/isEmpty'; import type {OnyxEntry} from 'react-native-onyx'; import type {Entries, ValueOf} from 'type-fest'; -import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; +import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Policy, PolicyTagLists, Report, ReportAction, ReportAttributesDerivedValue} from '@src/types/onyx'; @@ -246,7 +246,6 @@ function getRulesModifiedMessage( */ function getForReportAction({ translate, - localeCompare, reportAction, policy, movedFromReport, @@ -256,7 +255,6 @@ function getForReportAction({ reportAttributes, }: { translate: LocalizedTranslate; - localeCompare: LocaleContextProps['localeCompare']; reportAction: OnyxEntry; policy: OnyxEntry; movedFromReport?: OnyxEntry; @@ -454,7 +452,7 @@ function getForReportAction({ const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'newAttendees' in reportActionOriginalMessage; if (hasModifiedAttendees) { - const [oldAttendees, attendees] = getFormattedAttendees(localeCompare, reportActionOriginalMessage.newAttendees, reportActionOriginalMessage.oldAttendees); + const [oldAttendees, attendees] = getFormattedAttendees(reportActionOriginalMessage.newAttendees, reportActionOriginalMessage.oldAttendees); buildMessageFragmentForValue(translate, oldAttendees, attendees, translate('iou.attendees'), false, setFragments, removalFragments, changeFragments); } diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index e2373648c81f..2327a7596405 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -3,7 +3,7 @@ import {Str} from 'expensify-common'; import type {ImageSourcePropType} from 'react-native'; import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.png'; import * as AppUpdate from '@libs/actions/AppUpdate'; -import {localeCompareLocal, translateLocal} from '@libs/Localize'; +import {translateLocal} from '@libs/Localize'; import {getForReportAction} from '@libs/ModifiedExpenseMessage'; import {getTextFromHtml} from '@libs/ReportActionsUtils'; import {getReportName} from '@libs/ReportNameUtils'; @@ -152,7 +152,6 @@ export default { const bodyWithHTML = getForReportAction({ // eslint-disable-next-line @typescript-eslint/no-deprecated -- translateLocal is deprecated; BrowserNotifications is non-React code that cannot use the translate hook translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy, movedFromReport, diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 037bb7a9f688..f94b4806521a 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -14,7 +14,7 @@ import filterArrayByMatch from '@libs/filterArrayByMatch'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isReportMessageAttachment} from '@libs/isReportMessageAttachment'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; -import {localeCompareLocal, translateLocal} from '@libs/Localize'; +import {translateLocal} from '@libs/Localize'; import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from '@libs/LoginUtils'; import {MaxHeap} from '@libs/MaxHeap'; import {MinHeap} from '@libs/MinHeap'; @@ -750,7 +750,6 @@ function getLastMessageTextForReport({ } else if (isModifiedExpenseAction(lastReportAction)) { const properSchemaForModifiedExpenseMessageWithHTML = getForReportAction({ translate, - localeCompare: localeCompareLocal, reportAction: lastReportAction, policy, movedFromReport, diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 86e0b7560938..8047be7231ab 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -24,7 +24,7 @@ import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {convertToDisplayString} from './CurrencyUtils'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from './LocalePhoneNumber'; -import {localeCompareLocal, translateLocal} from './Localize'; +import {translateLocal} from './Localize'; // eslint-disable-next-line import/no-cycle import {getForReportAction, getMovedReportID} from './ModifiedExpenseMessage'; import Parser from './Parser'; @@ -844,7 +844,6 @@ function computeChatThreadReportName( const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(parentReportAction, CONST.REPORT.MOVE_TYPE.TO)}`]; const modifiedMessageWithHTML = getForReportAction({ translate, - localeCompare: localeCompareLocal, reportAction: parentReportAction, movedFromReport, movedToReport, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 908340fc8261..8186b7e12a22 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -118,7 +118,7 @@ import type {FormulaContext} from './Formula'; import getBase62ReportID from './getBase62ReportID'; import {isReportMessageAttachment} from './isReportMessageAttachment'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from './LocalePhoneNumber'; -import {localeCompareLocal, translateLocal} from './Localize'; +import {translateLocal} from './Localize'; import Log from './Log'; import {isEmailPublicDomain} from './LoginUtils'; // eslint-disable-next-line import/no-cycle @@ -5878,7 +5878,6 @@ function getReportName(reportNameInformation: GetReportNameParams): string { const modifiedMessageWithHTML = getForReportAction({ // eslint-disable-next-line @typescript-eslint/no-deprecated -- translateLocal is deprecated; getReportName is non-React code that cannot use the translate hook translate: translateLocal, - localeCompare: localeCompareLocal, reportAction: parentReportAction, policy, movedFromReport, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 73ddbe06a41f..730cfd74e2a2 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1,4 +1,5 @@ import {format, isValid, parse} from 'date-fns'; +import {Str} from 'expensify-common'; import {deepEqual} from 'fast-equals'; import lodashDeepClone from 'lodash/cloneDeep'; import lodashSet from 'lodash/set'; @@ -1205,20 +1206,23 @@ function getAttendees(transaction: OnyxInputOrEntry, currentUserPer } /** - * Returns attendees joined as an alphabetically sorted display string. Sort is part of the contract — every consumer relies on it. + * Returns attendees joined as a display string. Pass `localeCompare` to sort alphabetically (matches the pill sort); + * omit it to keep insertion order — used by non-React callers that don't want to thread the comparator. + * Strips the SMS domain so phone-login attendees render the same as in the rendered pills. */ -function getAttendeesListDisplayString(attendees: Attendee[], localeCompare: LocaleContextProps['localeCompare']): string { - // Lowercase to match sortAlphabetically (the pill sort) so joined string and pill order never disagree on case. - return [...attendees] - .sort((a, b) => localeCompare((a.displayName ?? a.login ?? '').toLowerCase(), (b.displayName ?? b.login ?? '').toLowerCase())) - .map((item) => item.displayName ?? item.login) - .join(', '); +function getAttendeesListDisplayString(attendees: Attendee[], localeCompare?: LocaleContextProps['localeCompare']): string { + const getName = (a: Attendee) => Str.removeSMSDomain(a.displayName ?? a.login ?? ''); + const ordered = localeCompare + ? // Lowercase to match sortAlphabetically (the pill sort) so joined string and pill order never disagree on case. + [...attendees].sort((a, b) => localeCompare(getName(a).toLowerCase(), getName(b).toLowerCase())) + : attendees; + return ordered.map(getName).join(', '); } /** * Return the list of attendees as a string and modified list of attendees as a string if present. */ -function getFormattedAttendees(localeCompare: LocaleContextProps['localeCompare'], modifiedAttendees?: Attendee[], attendees?: Attendee[]): [string, string] { +function getFormattedAttendees(modifiedAttendees?: Attendee[], attendees?: Attendee[], localeCompare?: LocaleContextProps['localeCompare']): [string, string] { const oldAttendees = modifiedAttendees ?? []; const newAttendees = attendees ?? []; return [getAttendeesListDisplayString(oldAttendees, localeCompare), getAttendeesListDisplayString(newAttendees, localeCompare)]; diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index 976a58eccb61..5beec1a30aaf 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -18,7 +18,6 @@ import {getEnvironmentURL} from '@libs/Environment/Environment'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; -import {localeCompareLocal} from '@libs/Localize'; import {getForReportAction} from '@libs/ModifiedExpenseMessage'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -857,7 +856,6 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (isModifiedExpenseAction(reportAction)) { const modifyExpenseMessageWithHTML = getForReportAction({ translate, - localeCompare: localeCompareLocal, reportAction, policy, movedFromReport, diff --git a/src/pages/inbox/report/actionContents/ModifiedExpenseContent.tsx b/src/pages/inbox/report/actionContents/ModifiedExpenseContent.tsx index 14f128b575e0..1501c8ab14d9 100644 --- a/src/pages/inbox/report/actionContents/ModifiedExpenseContent.tsx +++ b/src/pages/inbox/report/actionContents/ModifiedExpenseContent.tsx @@ -18,7 +18,7 @@ type ModifiedExpenseContentProps = { }; function ModifiedExpenseContent({action, report, childReport, originalReport}: ModifiedExpenseContentProps) { - const {translate, localeCompare} = useLocalize(); + const {translate} = useLocalize(); const {email: currentUserEmail} = useCurrentUserPersonalDetails(); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); @@ -32,7 +32,6 @@ function ModifiedExpenseContent({action, report, childReport, originalReport}: M const modifiedExpenseMessage = getForReportAction({ translate, - localeCompare, reportAction: action, policy, movedFromReport, diff --git a/tests/perf-test/ModifiedExpenseMessage.perf-test.ts b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts index 30bf826f7304..7728fe91187c 100644 --- a/tests/perf-test/ModifiedExpenseMessage.perf-test.ts +++ b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts @@ -5,7 +5,6 @@ import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, Report} from '@src/types/onyx'; -import {localeCompareLocal} from '../../src/libs/Localize'; import {getForReportAction} from '../../src/libs/ModifiedExpenseMessage'; import createCollection from '../utils/collections/createCollection'; import createRandomPolicy from '../utils/collections/policies'; @@ -69,7 +68,6 @@ test('[ModifiedExpenseMessage] getForReportAction on 1k reports and policies', a await measureFunction(() => getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: mockedPolicyTags, diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 003188b662e0..43e62efb0954 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -7,7 +7,7 @@ import * as ReportNameUtils from '@libs/ReportNameUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; -import {localeCompareLocal, translate} from '@src/libs/Localize'; +import {translate} from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy} from '@src/types/onyx'; import type {OriginalMessageModifiedExpense} from '@src/types/onyx/OriginalMessage'; @@ -283,7 +283,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -311,7 +310,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -341,7 +339,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -373,7 +370,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -403,7 +399,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -438,7 +433,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -471,7 +465,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -497,7 +490,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -523,7 +515,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -549,7 +540,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -575,7 +565,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -601,7 +590,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -629,7 +617,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -659,7 +646,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -685,7 +671,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -713,7 +698,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -743,7 +727,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -769,7 +752,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -794,7 +776,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -823,7 +804,6 @@ describe('ModifiedExpenseMessage', () => { const expectedResult = `changed the distance to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $7.00 (previously $0.70)`; const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -851,7 +831,6 @@ describe('ModifiedExpenseMessage', () => { const expectedResult = `changed the rate to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $55.80 (previously $39.45)`; const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -877,7 +856,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -904,7 +882,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -931,7 +908,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -958,7 +934,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -985,7 +960,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1014,7 +988,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: mockPolicy, policyTags: undefined, @@ -1044,7 +1017,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1071,7 +1043,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1097,7 +1068,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1124,7 +1094,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1167,7 +1136,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, @@ -1200,7 +1168,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, @@ -1227,7 +1194,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, @@ -1253,7 +1219,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, @@ -1280,7 +1245,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, @@ -1309,7 +1273,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: policyRulesPolicy, policyTags: undefined, @@ -1337,7 +1300,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1364,7 +1326,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1394,7 +1355,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1409,7 +1369,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1447,7 +1406,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1473,7 +1431,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1499,7 +1456,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1525,7 +1481,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1540,7 +1495,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1575,7 +1529,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1590,7 +1543,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1625,7 +1577,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1642,7 +1593,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1675,7 +1625,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: mockPolicy, policyTags: undefined, @@ -1707,7 +1656,6 @@ describe('ModifiedExpenseMessage', () => { const expectedResult = `changed the distance to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $7.00 (previously $0.70)`; const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1733,7 +1681,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, movedFromReport, policy: undefined, @@ -1766,7 +1713,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, movedFromReport, policy, @@ -1800,7 +1746,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, movedToReport, policy, @@ -1827,7 +1772,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, movedToReport, policy: undefined, @@ -1851,7 +1795,6 @@ describe('ModifiedExpenseMessage', () => { it('returns an empty string', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1864,7 +1807,6 @@ describe('ModifiedExpenseMessage', () => { it('returns an empty string even when a valid policy is provided', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1895,7 +1837,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1910,7 +1851,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1944,7 +1884,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -1959,7 +1898,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -1993,7 +1931,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -2008,7 +1945,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: { id: '123', @@ -2039,7 +1975,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: undefined, policyTags: undefined, @@ -2054,7 +1989,6 @@ describe('ModifiedExpenseMessage', () => { const result = getForReportAction({ translate: translateLocal, - localeCompare: localeCompareLocal, reportAction, policy: { id: '123', diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 8f3c5bff2787..2681919605f2 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -1400,6 +1400,14 @@ describe('TransactionUtils', () => { describe('getAttendeesListDisplayString', () => { const localeCompare = (a: string, b: string) => a.localeCompare(b, undefined, {numeric: true, sensitivity: 'variant', caseFirst: 'upper'}); + it('preserves insertion order when no localeCompare is provided', () => { + const attendees: Attendee[] = [ + {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, + {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, + ]; + expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('banana, apple'); + }); + it('returns attendees alphabetically regardless of insertion order (deploy blocker #89130)', () => { const attendees: Attendee[] = [ {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, @@ -1424,6 +1432,14 @@ describe('TransactionUtils', () => { expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('alice, Bob'); }); + it('strips the @expensify.sms domain so phone-login attendees render the same as in pills', () => { + const attendees: Attendee[] = [ + {displayName: '+15551234567@expensify.sms', avatarUrl: '', login: '+15551234567@expensify.sms'}, + {displayName: 'Alice', avatarUrl: '', login: 'alice@x.com'}, + ]; + expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('+15551234567, Alice'); + }); + it('returns empty string for empty array', () => { expect(TransactionUtils.getAttendeesListDisplayString([], localeCompare)).toBe(''); });