From 0a7048c3abb6eab068c0d2bc72badf5c993232a0 Mon Sep 17 00:00:00 2001 From: daledah Date: Mon, 6 Jul 2026 18:20:58 +0700 Subject: [PATCH 1/6] refactor function getGroupChatName to use data from useOnyx --- src/libs/ReportNameUtils.ts | 9 ++-- .../DynamicReportParticipantsInvitePage.tsx | 4 +- src/pages/GroupChatNameEditPage.tsx | 4 +- src/selectors/ReportMetaData.ts | 11 ++++- tests/unit/ReportNameUtilsTest.ts | 45 +++++++++++++++++++ 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index c5576ec3e8b0..738db6903a66 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -11,11 +11,11 @@ import type { ReportAction, ReportActions, ReportAttributesDerivedValue, - ReportMetadata, ReportNameValuePairs, Transaction, } from '@src/types/onyx'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; +import type {PendingChatMember} from '@src/types/onyx/ReportMetadata'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -263,17 +263,18 @@ function getGroupChatName( participants?: SelectedParticipant[], shouldApplyLimit = false, report?: OnyxEntry, - reportMetadataParam?: OnyxEntry, + pendingChatMembers?: PendingChatMember[], ): string | undefined { // If we have a report always try to get the name from the report. if (report?.reportName) { return report.reportName; } - const reportMetadata = reportMetadataParam ?? getReportMetadata(report?.reportID); + // TODO: Remove the getReportMetadata fallback once https://github.com/Expensify/App/issues/66421 is done + const resolvedPendingChatMembers = pendingChatMembers ?? getReportMetadata(report?.reportID)?.pendingChatMembers; const pendingMemberAccountIDs = new Set( - reportMetadata?.pendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).map((member) => member.accountID), + resolvedPendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).map((member) => member.accountID), ); let participantAccountIDs = participants?.map((participant) => participant.accountID) ?? diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index c5b14f2db470..1e5e0b4239ae 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -30,6 +30,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {InvitedEmailsToAccountIDs} from '@src/types/onyx'; +import {pendingChatMembersListSelector} from '@selectors/ReportMetaData'; import React, {useEffect, useState} from 'react'; import type {WithReportOrNotFoundProps} from './inbox/report/withReportOrNotFound'; @@ -45,6 +46,7 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants const {translate, formatPhoneNumber} = useLocalize(); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const [personalDetailsList] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [pendingChatMembers] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, {selector: pendingChatMembersListSelector}); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_PARTICIPANTS_INVITE.path); @@ -114,7 +116,7 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants toggleSelection(option); }; - const reportName = getGroupChatName(formatPhoneNumber, undefined, true, report); + const reportName = getGroupChatName(formatPhoneNumber, undefined, true, report, pendingChatMembers); const goBack = () => { Navigation.goBack(backPath); diff --git a/src/pages/GroupChatNameEditPage.tsx b/src/pages/GroupChatNameEditPage.tsx index 3c8c41744965..0e102138f14e 100644 --- a/src/pages/GroupChatNameEditPage.tsx +++ b/src/pages/GroupChatNameEditPage.tsx @@ -23,6 +23,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import {pendingChatMembersListSelector} from '@src/selectors/ReportMetaData'; import INPUT_IDS from '@src/types/form/NewChatNameForm'; import type {Report as ReportOnyxType} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -40,12 +41,13 @@ function GroupChatNameEditPage({report}: GroupChatNameEditPageProps) { const reportID = report?.reportID; const isUpdatingExistingReport = !!reportID; const [groupChatDraft, groupChatDraftMetadata] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT); + const [pendingChatMembers] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, {selector: pendingChatMembersListSelector}); const styles = useThemeStyles(); const {translate, formatPhoneNumber} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); - const existingReportName = report ? getGroupChatName(formatPhoneNumber, undefined, false, report) : getGroupChatName(formatPhoneNumber, groupChatDraft?.participants); + const existingReportName = report ? getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers) : getGroupChatName(formatPhoneNumber, groupChatDraft?.participants); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const currentChatName = reportID ? existingReportName : groupChatDraft?.reportName || existingReportName; diff --git a/src/selectors/ReportMetaData.ts b/src/selectors/ReportMetaData.ts index 4f71ae2358d0..245760ddc311 100644 --- a/src/selectors/ReportMetaData.ts +++ b/src/selectors/ReportMetaData.ts @@ -11,6 +11,15 @@ const isLoadingInitialReportActionsSelector = (loadingState: OnyxEntry): OnyxEntry => reportMetadata ? {pendingChatMembers: reportMetadata.pendingChatMembers} : undefined; +const pendingChatMembersListSelector = (reportMetadata: OnyxEntry) => reportMetadata?.pendingChatMembers; + const pendingNewTransactionIDsSelector = (reportMetadata: OnyxEntry) => reportMetadata?.pendingNewTransactionIDs; -export {isActionLoadingSelector, hasOnceLoadedReportActionsSelector, isLoadingInitialReportActionsSelector, pendingNewTransactionIDsSelector, pendingChatMembersSelector}; +export { + isActionLoadingSelector, + hasOnceLoadedReportActionsSelector, + isLoadingInitialReportActionsSelector, + pendingNewTransactionIDsSelector, + pendingChatMembersSelector, + pendingChatMembersListSelector, +}; diff --git a/tests/unit/ReportNameUtilsTest.ts b/tests/unit/ReportNameUtilsTest.ts index a3a9c0f22320..310d83f18bb8 100644 --- a/tests/unit/ReportNameUtilsTest.ts +++ b/tests/unit/ReportNameUtilsTest.ts @@ -1261,6 +1261,51 @@ describe('ReportNameUtils', () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); expect(getGroupChatName(formatPhoneNumber, undefined, false, report)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); }); + + it('excludes participants with pending DELETE action from pendingChatMembers', async () => { + const report: Report = { + ...createRegularChat(1, [1, 2, 3, 4]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportName: '', + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + + const pendingChatMembers = [ + {accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE as const}, + {accountID: '4', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE as const}, + ]; + expect(getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers)).toEqual('One, Three'); + }); + + it('includes all participants when pendingChatMembers has no DELETE actions', async () => { + const report: Report = { + ...createRegularChat(1, [1, 2, 3, 4]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportName: '', + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + + const pendingChatMembers = [{accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD as const}]; + expect(getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers)).toEqual('Four, One, Three, Two'); + }); + + it('uses passed pendingChatMembers instead of falling back to report metadata', async () => { + const report: Report = { + ...createRegularChat(1, [1, 2, 3, 4]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportName: '', + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`, { + pendingChatMembers: [{accountID: '1', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}], + }); + + const pendingChatMembers = [{accountID: '3', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE as const}]; + expect(getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers)).toEqual('Four, One, Two'); + }); }); }); From bc7ea82f5c748d0218dd23e64effaf1ea154b4f2 Mon Sep 17 00:00:00 2001 From: daledah Date: Tue, 7 Jul 2026 12:37:57 +0700 Subject: [PATCH 2/6] fix ts --- tests/unit/ReportNameUtilsTest.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/ReportNameUtilsTest.ts b/tests/unit/ReportNameUtilsTest.ts index 310d83f18bb8..8009794dbf73 100644 --- a/tests/unit/ReportNameUtilsTest.ts +++ b/tests/unit/ReportNameUtilsTest.ts @@ -1272,8 +1272,8 @@ describe('ReportNameUtils', () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); const pendingChatMembers = [ - {accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE as const}, - {accountID: '4', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE as const}, + {accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + {accountID: '4', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, ]; expect(getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers)).toEqual('One, Three'); }); @@ -1287,7 +1287,7 @@ describe('ReportNameUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - const pendingChatMembers = [{accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD as const}]; + const pendingChatMembers = [{accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}]; expect(getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers)).toEqual('Four, One, Three, Two'); }); @@ -1303,7 +1303,7 @@ describe('ReportNameUtils', () => { pendingChatMembers: [{accountID: '1', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}], }); - const pendingChatMembers = [{accountID: '3', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE as const}]; + const pendingChatMembers = [{accountID: '3', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}]; expect(getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers)).toEqual('Four, One, Two'); }); }); From 85556dc719a6504540c95bca681ffa04af7770cf Mon Sep 17 00:00:00 2001 From: daledah Date: Tue, 7 Jul 2026 15:58:50 +0700 Subject: [PATCH 3/6] address comment --- src/libs/ReportNameUtils.ts | 13 +++++++------ .../DynamicReportParticipantsInvitePage.tsx | 6 +++--- src/pages/GroupChatNameEditPage.tsx | 8 +++++--- src/selectors/ReportMetaData.ts | 6 ++++-- tests/unit/ReportNameUtilsTest.ts | 18 ++++++------------ 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 738db6903a66..ba0386c725d9 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -15,7 +15,6 @@ import type { Transaction, } from '@src/types/onyx'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; -import type {PendingChatMember} from '@src/types/onyx/ReportMetadata'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -263,7 +262,7 @@ function getGroupChatName( participants?: SelectedParticipant[], shouldApplyLimit = false, report?: OnyxEntry, - pendingChatMembers?: PendingChatMember[], + pendingDeleteMemberAccountIDs?: string[], ): string | undefined { // If we have a report always try to get the name from the report. if (report?.reportName) { @@ -271,11 +270,13 @@ function getGroupChatName( } // TODO: Remove the getReportMetadata fallback once https://github.com/Expensify/App/issues/66421 is done - const resolvedPendingChatMembers = pendingChatMembers ?? getReportMetadata(report?.reportID)?.pendingChatMembers; + const resolvedPendingDeleteMemberAccountIDs = + pendingDeleteMemberAccountIDs ?? + getReportMetadata(report?.reportID) + ?.pendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + .map((member) => member.accountID); - const pendingMemberAccountIDs = new Set( - resolvedPendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).map((member) => member.accountID), - ); + const pendingMemberAccountIDs = new Set(resolvedPendingDeleteMemberAccountIDs); let participantAccountIDs = participants?.map((participant) => participant.accountID) ?? Object.keys(report?.participants ?? {}) diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index 1e5e0b4239ae..4a1a907ed69f 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -30,7 +30,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {InvitedEmailsToAccountIDs} from '@src/types/onyx'; -import {pendingChatMembersListSelector} from '@selectors/ReportMetaData'; +import {pendingDeleteMemberAccountIDsSelector} from '@selectors/ReportMetaData'; import React, {useEffect, useState} from 'react'; import type {WithReportOrNotFoundProps} from './inbox/report/withReportOrNotFound'; @@ -46,7 +46,7 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants const {translate, formatPhoneNumber} = useLocalize(); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const [personalDetailsList] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [pendingChatMembers] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, {selector: pendingChatMembersListSelector}); + const [pendingDeleteMemberAccountIDs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, {selector: pendingDeleteMemberAccountIDsSelector}); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_PARTICIPANTS_INVITE.path); @@ -116,7 +116,7 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants toggleSelection(option); }; - const reportName = getGroupChatName(formatPhoneNumber, undefined, true, report, pendingChatMembers); + const reportName = getGroupChatName(formatPhoneNumber, undefined, true, report, pendingDeleteMemberAccountIDs); const goBack = () => { Navigation.goBack(backPath); diff --git a/src/pages/GroupChatNameEditPage.tsx b/src/pages/GroupChatNameEditPage.tsx index 0e102138f14e..545ed8c5da4c 100644 --- a/src/pages/GroupChatNameEditPage.tsx +++ b/src/pages/GroupChatNameEditPage.tsx @@ -23,7 +23,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import {pendingChatMembersListSelector} from '@src/selectors/ReportMetaData'; +import {pendingDeleteMemberAccountIDsSelector} from '@src/selectors/ReportMetaData'; import INPUT_IDS from '@src/types/form/NewChatNameForm'; import type {Report as ReportOnyxType} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -41,13 +41,15 @@ function GroupChatNameEditPage({report}: GroupChatNameEditPageProps) { const reportID = report?.reportID; const isUpdatingExistingReport = !!reportID; const [groupChatDraft, groupChatDraftMetadata] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT); - const [pendingChatMembers] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, {selector: pendingChatMembersListSelector}); + const [pendingDeleteMemberAccountIDs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, {selector: pendingDeleteMemberAccountIDsSelector}); const styles = useThemeStyles(); const {translate, formatPhoneNumber} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); - const existingReportName = report ? getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers) : getGroupChatName(formatPhoneNumber, groupChatDraft?.participants); + const existingReportName = report + ? getGroupChatName(formatPhoneNumber, undefined, false, report, pendingDeleteMemberAccountIDs) + : getGroupChatName(formatPhoneNumber, groupChatDraft?.participants); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const currentChatName = reportID ? existingReportName : groupChatDraft?.reportName || existingReportName; diff --git a/src/selectors/ReportMetaData.ts b/src/selectors/ReportMetaData.ts index 245760ddc311..33a30e0ae778 100644 --- a/src/selectors/ReportMetaData.ts +++ b/src/selectors/ReportMetaData.ts @@ -1,3 +1,4 @@ +import CONST from '@src/CONST'; import type {ReportLoadingState, ReportMetadata} from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -11,7 +12,8 @@ const isLoadingInitialReportActionsSelector = (loadingState: OnyxEntry): OnyxEntry => reportMetadata ? {pendingChatMembers: reportMetadata.pendingChatMembers} : undefined; -const pendingChatMembersListSelector = (reportMetadata: OnyxEntry) => reportMetadata?.pendingChatMembers; +const pendingDeleteMemberAccountIDsSelector = (reportMetadata: OnyxEntry) => + reportMetadata?.pendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).map((member) => member.accountID); const pendingNewTransactionIDsSelector = (reportMetadata: OnyxEntry) => reportMetadata?.pendingNewTransactionIDs; @@ -21,5 +23,5 @@ export { isLoadingInitialReportActionsSelector, pendingNewTransactionIDsSelector, pendingChatMembersSelector, - pendingChatMembersListSelector, + pendingDeleteMemberAccountIDsSelector, }; diff --git a/tests/unit/ReportNameUtilsTest.ts b/tests/unit/ReportNameUtilsTest.ts index 8009794dbf73..63c1d197b249 100644 --- a/tests/unit/ReportNameUtilsTest.ts +++ b/tests/unit/ReportNameUtilsTest.ts @@ -1262,7 +1262,7 @@ describe('ReportNameUtils', () => { expect(getGroupChatName(formatPhoneNumber, undefined, false, report)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); }); - it('excludes participants with pending DELETE action from pendingChatMembers', async () => { + it('excludes participants whose accountIDs are in pendingDeleteMemberAccountIDs', async () => { const report: Report = { ...createRegularChat(1, [1, 2, 3, 4]), chatType: CONST.REPORT.CHAT_TYPE.GROUP, @@ -1271,14 +1271,10 @@ describe('ReportNameUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - const pendingChatMembers = [ - {accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, - {accountID: '4', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, - ]; - expect(getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers)).toEqual('One, Three'); + expect(getGroupChatName(formatPhoneNumber, undefined, false, report, ['2', '4'])).toEqual('One, Three'); }); - it('includes all participants when pendingChatMembers has no DELETE actions', async () => { + it('includes all participants when pendingDeleteMemberAccountIDs is empty', async () => { const report: Report = { ...createRegularChat(1, [1, 2, 3, 4]), chatType: CONST.REPORT.CHAT_TYPE.GROUP, @@ -1287,11 +1283,10 @@ describe('ReportNameUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - const pendingChatMembers = [{accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}]; - expect(getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers)).toEqual('Four, One, Three, Two'); + expect(getGroupChatName(formatPhoneNumber, undefined, false, report, [])).toEqual('Four, One, Three, Two'); }); - it('uses passed pendingChatMembers instead of falling back to report metadata', async () => { + it('uses passed pendingDeleteMemberAccountIDs instead of falling back to report metadata', async () => { const report: Report = { ...createRegularChat(1, [1, 2, 3, 4]), chatType: CONST.REPORT.CHAT_TYPE.GROUP, @@ -1303,8 +1298,7 @@ describe('ReportNameUtils', () => { pendingChatMembers: [{accountID: '1', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}], }); - const pendingChatMembers = [{accountID: '3', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}]; - expect(getGroupChatName(formatPhoneNumber, undefined, false, report, pendingChatMembers)).toEqual('Four, One, Two'); + expect(getGroupChatName(formatPhoneNumber, undefined, false, report, ['3'])).toEqual('Four, One, Two'); }); }); }); From 4a11a1e51a722537a47bcba4bdc7d68335ee5e40 Mon Sep 17 00:00:00 2001 From: daledah Date: Tue, 7 Jul 2026 16:46:28 +0700 Subject: [PATCH 4/6] handle computeReportName function --- src/libs/ReportNameUtils.ts | 5 ++++- src/libs/actions/OnyxDerived/configs/reportAttributes.ts | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index ba0386c725d9..bc68747f38ad 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -190,6 +190,7 @@ type ComputeReportName = { conciergeReportID?: string; reportAttributes?: ReportAttributesDerivedValue['reports']; isTrackIntentUser: boolean | undefined; + pendingDeleteMemberAccountIDs?: string[]; }; let allPersonalDetails: OnyxEntry; @@ -955,6 +956,7 @@ function computeReportName({ conciergeReportID, reportAttributes, isTrackIntentUser, + pendingDeleteMemberAccountIDs, }: ComputeReportName): string { if (!report?.reportID) { return ''; @@ -1001,6 +1003,7 @@ function computeReportName({ conciergeReportID, reportAttributes, isTrackIntentUser, + pendingDeleteMemberAccountIDs: undefined, }); return getCreatedReportForUnapprovedTransactionsMessage(originalID, reportName, isOriginalReportDeleted(parentReportAction, originalReport), translate); } @@ -1035,7 +1038,7 @@ function computeReportName({ } if (isGroupChat(report)) { - return getGroupChatName(formatPhoneNumberPhoneUtils, undefined, true, report) ?? ''; + return getGroupChatName(formatPhoneNumberPhoneUtils, undefined, true, report, pendingDeleteMemberAccountIDs) ?? ''; } let formattedName: string | undefined; diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 3156fc2ed8df..33fa1137ec94 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -116,6 +116,7 @@ export default createOnyxDerivedValueConfig({ policyTags, conciergeReportID, introSelected, + reportMetadata, ], {currentValue, sourceValues}, ) => { @@ -365,6 +366,11 @@ export default createOnyxDerivedValueConfig({ actionTargetReportActionID = actionGreenTargetReportActionID; } + const reportReportMetadata = reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`]; + const pendingDeleteMemberAccountIDs = reportReportMetadata?.pendingChatMembers + ?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + .map((member) => member.accountID); + acc[report.reportID] = { reportName: report ? computeReportName({ @@ -382,6 +388,7 @@ export default createOnyxDerivedValueConfig({ conciergeReportID: conciergeReportID ?? undefined, reportAttributes: currentValue?.reports, isTrackIntentUser: isTrackIntentUserSelector(introSelected), + pendingDeleteMemberAccountIDs, }) : '', isEmpty: generateIsEmptyReport(report, isReportArchived), From 6ee89d75227f6fdb86be921fcab9128e6ad2efd3 Mon Sep 17 00:00:00 2001 From: daledah Date: Tue, 14 Jul 2026 10:40:34 +0700 Subject: [PATCH 5/6] address comment --- src/libs/ReportNameUtils.ts | 7 +-- src/libs/ReportUtils.ts | 26 +++++++++++ .../OnyxDerived/configs/reportAttributes.ts | 5 +-- src/selectors/ReportMetaData.ts | 6 +-- tests/unit/ReportUtilsTest.ts | 43 +++++++++++++++++++ 5 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index a76b8087f81b..b181e92dc3ff 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -140,6 +140,7 @@ import { getPolicyName, getReimbursementDeQueuedOrCanceledActionMessage, getReimbursementQueuedActionMessage, + getPendingDeleteMemberAccountIDs, getReportMetadata, getReportOrDraftReport, getTransactionReportName, @@ -271,11 +272,7 @@ function getGroupChatName( } // TODO: Remove the getReportMetadata fallback once https://github.com/Expensify/App/issues/66421 is done - const resolvedPendingDeleteMemberAccountIDs = - pendingDeleteMemberAccountIDs ?? - getReportMetadata(report?.reportID) - ?.pendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) - .map((member) => member.accountID); + const resolvedPendingDeleteMemberAccountIDs = pendingDeleteMemberAccountIDs ?? getPendingDeleteMemberAccountIDs(getReportMetadata(report?.reportID)?.pendingChatMembers); const pendingMemberAccountIDs = new Set(resolvedPendingDeleteMemberAccountIDs); let participantAccountIDs = diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5d6f210efc27..5bbabb8a88a6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6066,6 +6066,31 @@ function getPendingChatMembers(accountIDs: number[], previousPendingChatMembers: return [...previousPendingChatMembers, ...pendingChatMembers]; } +/** + * Returns account IDs whose latest pending action is DELETE. + * Uses `findLast` so that a subsequent ADD (e.g. re-invite while offline) takes precedence over an earlier DELETE. + */ +function getPendingDeleteMemberAccountIDs(pendingChatMembers: PendingChatMember[] | undefined): string[] { + if (!pendingChatMembers?.length) { + return []; + } + + const seen = new Set(); + const result: string[] = []; + for (const member of pendingChatMembers) { + seen.add(member.accountID); + } + + for (const accountID of seen) { + const latestAction = pendingChatMembers.findLast((member) => member.accountID === accountID); + if (latestAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + result.push(accountID); + } + } + + return result; +} + /** * Gets the parent navigation subtitle for the report */ @@ -13629,6 +13654,7 @@ export { getParticipantsAccountIDsForDisplay, getParticipantsList, getPendingChatMembers, + getPendingDeleteMemberAccountIDs, getPersonalDetailsForAccountID, getPolicyDescriptionText, getPolicyExpenseChat, diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index e88a619efc98..5bfda7c71683 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -15,6 +15,7 @@ import { isPolicyAdmin, isPolicyExpenseChat, isProcessingReport, + getPendingDeleteMemberAccountIDs, isValidReport, } from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; @@ -515,9 +516,7 @@ export default createOnyxDerivedValueConfig({ } const reportReportMetadata = reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`]; - const pendingDeleteMemberAccountIDs = reportReportMetadata?.pendingChatMembers - ?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) - .map((member) => member.accountID); + const pendingDeleteMemberAccountIDs = getPendingDeleteMemberAccountIDs(reportReportMetadata?.pendingChatMembers); acc[report.reportID] = { reportName: report diff --git a/src/selectors/ReportMetaData.ts b/src/selectors/ReportMetaData.ts index 33a30e0ae778..8ec0603f9b06 100644 --- a/src/selectors/ReportMetaData.ts +++ b/src/selectors/ReportMetaData.ts @@ -1,4 +1,5 @@ -import CONST from '@src/CONST'; +import {getPendingDeleteMemberAccountIDs} from '@libs/ReportUtils'; + import type {ReportLoadingState, ReportMetadata} from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -12,8 +13,7 @@ const isLoadingInitialReportActionsSelector = (loadingState: OnyxEntry): OnyxEntry => reportMetadata ? {pendingChatMembers: reportMetadata.pendingChatMembers} : undefined; -const pendingDeleteMemberAccountIDsSelector = (reportMetadata: OnyxEntry) => - reportMetadata?.pendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).map((member) => member.accountID); +const pendingDeleteMemberAccountIDsSelector = (reportMetadata: OnyxEntry) => getPendingDeleteMemberAccountIDs(reportMetadata?.pendingChatMembers); const pendingNewTransactionIDsSelector = (reportMetadata: OnyxEntry) => reportMetadata?.pendingNewTransactionIDs; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 12cee89da147..806c1cf56ea0 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -108,6 +108,7 @@ import { getParentNavigationSubtitle, getParsedComment, getParticipantsList, + getPendingDeleteMemberAccountIDs, getPolicyChangeLogCopyMessage, getPolicyExpenseChat, getPolicyIDsWithEmptyReportsForAccount, @@ -20483,3 +20484,45 @@ describe('areAllRequestsBeingSmartScanned', () => { expect(areAllRequestsBeingSmartScanned(undefined, reportPreviewAction, transactions)).toBe(false); }); }); + +describe('getPendingDeleteMemberAccountIDs', () => { + it('returns empty array for undefined input', () => { + expect(getPendingDeleteMemberAccountIDs(undefined)).toEqual([]); + }); + + it('returns empty array for empty array input', () => { + expect(getPendingDeleteMemberAccountIDs([])).toEqual([]); + }); + + it('returns account IDs with DELETE as the latest pending action', () => { + const pendingChatMembers = [ + {accountID: '1', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + {accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + ]; + + const result = getPendingDeleteMemberAccountIDs(pendingChatMembers); + expect(result).toEqual(['1']); + }); + + it('uses findLast so a subsequent ADD overrides an earlier DELETE for the same member', () => { + const pendingChatMembers = [ + {accountID: '1', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + {accountID: '1', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + ]; + + const result = getPendingDeleteMemberAccountIDs(pendingChatMembers); + expect(result).toEqual([]); + }); + + it('correctly handles mixed scenarios with multiple members', () => { + const pendingChatMembers = [ + {accountID: '1', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + {accountID: '2', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + {accountID: '1', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + {accountID: '3', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + ]; + + const result = getPendingDeleteMemberAccountIDs(pendingChatMembers); + expect(result).toEqual(['2', '3']); + }); +}); From 7a0df0eadfe0b4503588239e85cd2629617be43d Mon Sep 17 00:00:00 2001 From: daledah Date: Tue, 14 Jul 2026 11:00:21 +0700 Subject: [PATCH 6/6] fix jest test --- tests/unit/reportAttributesTest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/reportAttributesTest.ts b/tests/unit/reportAttributesTest.ts index 7c76a9ac4116..2da5812d3ba8 100644 --- a/tests/unit/reportAttributesTest.ts +++ b/tests/unit/reportAttributesTest.ts @@ -22,6 +22,7 @@ jest.mock('@libs/ReportUtils', () => ({ actionTargetReportActionID: undefined, })), generateIsEmptyReport: jest.fn(() => false), + getPendingDeleteMemberAccountIDs: jest.fn(() => []), hasVisibleReportFieldViolations: jest.fn(() => false), isArchivedReport: jest.fn(() => false), isValidReport: jest.fn(() => true),