diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 4eef9e93f188..02279e23a907 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -126,16 +126,16 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp newReportOption: OptionsListUtils.SearchOption; }> = []; - Object.keys(personalDetails).forEach((accoutID) => { - const prevPersonalDetail = prevPersonalDetails?.[accoutID]; - const personalDetail = personalDetails?.[accoutID]; + Object.keys(personalDetails).forEach((accountID) => { + const prevPersonalDetail = prevPersonalDetails?.[accountID]; + const personalDetail = personalDetails?.[accountID]; if (isEqualPersonalDetail(prevPersonalDetail, personalDetail)) { return; } Object.values(reports ?? {}) - .filter((report) => Boolean(report?.participantAccountIDs?.includes(Number(accoutID))) || (ReportUtils.isSelfDM(report) && report?.ownerAccountID === Number(accoutID))) + .filter((report) => Boolean(Object.keys(report?.participants ?? {}).includes(accountID)) || (ReportUtils.isSelfDM(report) && report?.ownerAccountID === Number(accountID))) .forEach((report) => { if (!report) { return; diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index d00120a594d8..029bd57cd876 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -145,10 +145,10 @@ function OptionRow({ const hoveredStyle = hoverStyle ? flattenHoverStyle : styles.sidebarLinkHover; const hoveredBackgroundColor = hoveredStyle?.backgroundColor ? (hoveredStyle.backgroundColor as string) : backgroundColor; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; - const isMultipleParticipant = (option.participantsList?.length ?? 0) > 1; + const shouldUseShortFormInTooltip = (option.participantsList?.length ?? 0) > 1; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((option.participantsList ?? (option.accountID ? [option] : [])).slice(0, 10), isMultipleParticipant); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((option.participantsList ?? (option.accountID ? [option] : [])).slice(0, 10), shouldUseShortFormInTooltip); let subscriptColor = theme.appBG; if (optionIsFocused) { subscriptColor = focusedBackgroundColor; diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 83aefcd8aba9..cfbce4358eb5 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -1,7 +1,7 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -34,12 +34,16 @@ type ReportWelcomeTextProps = ReportWelcomeTextOnyxProps & { function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [session] = useOnyx(ONYXKEYS.SESSION); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isChatRoom = ReportUtils.isChatRoom(report); const isSelfDM = ReportUtils.isSelfDM(report); const isInvoiceRoom = ReportUtils.isInvoiceRoom(report); + const isOneOnOneChat = ReportUtils.isOneOnOneChat(report); const isDefault = !(isChatRoom || isPolicyExpenseChat || isSelfDM || isInvoiceRoom); - const participantAccountIDs = report?.participantAccountIDs ?? []; + const participantAccountIDs = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== session?.accountID || !isOneOnOneChat); const isMultipleParticipant = participantAccountIDs.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index c1503595fa24..46ffa7d999ee 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -38,7 +38,7 @@ const ReportIDsContext = createContext({ const chatReportSelector = (report: OnyxEntry): ChatReportSelector => (report && { reportID: report.reportID, - participantAccountIDs: report.participantAccountIDs, + participants: report.participants, isPinned: report.isPinned, isHidden: report.isHidden, notificationPreference: report.notificationPreference, diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index eba705c23722..7f66ff428b54 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -709,11 +709,20 @@ function createOption( result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); - result.tooltipText = ReportUtils.getReportParticipantsTitle(report.visibleChatMemberAccountIDs ?? []); result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; result.policyID = report.policyID; result.isSelfDM = ReportUtils.isSelfDM(report); + // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants + const isOneOnOneChat = ReportUtils.isOneOnOneChat(report); + const visibleParticipantAccountIDs = Object.entries(report.participants ?? {}) + .filter(([, participant]) => participant && !participant.hidden) + .map(([accountID]) => Number(accountID)) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChat); + + result.tooltipText = ReportUtils.getReportParticipantsTitle(visibleParticipantAccountIDs); + result.isOneOnOneChat = isOneOnOneChat; + hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; subtitle = ReportUtils.getChatRoomSubtitle(report); @@ -776,8 +785,15 @@ function createOption( function getReportOption(participant: Participant): ReportUtils.OptionData { const report = ReportUtils.getReport(participant.reportID); + // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants + const isOneOnOneChat = ReportUtils.isOneOnOneChat(report); + const visibleParticipantAccountIDs = Object.entries(report?.participants ?? {}) + .filter(([, reportParticipant]) => reportParticipant && !reportParticipant.hidden) + .map(([accountID]) => Number(accountID)) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChat); + const option = createOption( - report?.visibleChatMemberAccountIDs ?? [], + visibleParticipantAccountIDs, allPersonalDetails ?? {}, !isEmptyObject(report) ? report : null, {}, @@ -806,8 +822,12 @@ function getReportOption(participant: Participant): ReportUtils.OptionData { function getPolicyExpenseReportOption(participant: Participant | ReportUtils.OptionData): ReportUtils.OptionData { const expenseReport = ReportUtils.isPolicyExpenseChat(participant) ? ReportUtils.getReport(participant.reportID) : null; + const visibleParticipantAccountIDs = Object.entries(expenseReport?.participants ?? {}) + .filter(([, reportParticipant]) => reportParticipant && !reportParticipant.hidden) + .map(([accountID]) => Number(accountID)); + const option = createOption( - expenseReport?.visibleChatMemberAccountIDs ?? [], + visibleParticipantAccountIDs, allPersonalDetails ?? {}, !isEmptyObject(expenseReport) ? expenseReport : null, {}, @@ -1467,19 +1487,11 @@ function createOptionList(personalDetails: OnyxEntry, repor return; } - const isSelfDM = ReportUtils.isSelfDM(report); - let accountIDs = []; - - if (isSelfDM) { - // For selfDM we need to add the currentUser as participants. - accountIDs = [currentUserAccountID ?? 0]; - } else { - accountIDs = Object.keys(report.participants ?? {}).map(Number); - if (ReportUtils.isOneOnOneChat(report)) { - // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants - accountIDs = accountIDs.filter((accountID) => accountID !== currentUserAccountID); - } - } + // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants + const isOneOnOneChat = ReportUtils.isOneOnOneChat(report); + const accountIDs = Object.keys(report.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChat); if (!accountIDs || accountIDs.length === 0) { return; @@ -1511,8 +1523,11 @@ function createOptionList(personalDetails: OnyxEntry, repor } function createOptionFromReport(report: Report, personalDetails: OnyxEntry) { - const isSelfDM = ReportUtils.isSelfDM(report); - const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.participantAccountIDs ?? []; + // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants + const isOneOnOneChat = ReportUtils.isOneOnOneChat(report); + const accountIDs = Object.keys(report.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChat); return { item: report, @@ -1768,18 +1783,12 @@ function getOptions( const isPolicyExpenseChat = option.isPolicyExpenseChat; const isMoneyRequestReport = option.isMoneyRequestReport; const isSelfDM = option.isSelfDM; - let accountIDs = []; - - if (isSelfDM) { - // For selfDM we need to add the currentUser as participants. - accountIDs = [currentUserAccountID ?? 0]; - } else { - accountIDs = Object.keys(report.participants ?? {}).map(Number); - if (ReportUtils.isOneOnOneChat(report)) { - // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants - accountIDs = accountIDs.filter((accountID) => accountID !== currentUserAccountID); - } - } + const isOneOnOneChat = option.isOneOnOneChat; + + // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants + const accountIDs = Object.keys(report.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChat); if (isPolicyExpenseChat && report.isOwnPolicyExpenseChat && !includeOwnedWorkspaceChats) { return; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fbc378b4d3dd..d3195193c742 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -282,8 +282,6 @@ type OptimisticChatReport = Pick< | 'parentReportActionID' | 'parentReportID' | 'participants' - | 'participantAccountIDs' - | 'visibleChatMemberAccountIDs' | 'policyID' | 'reportID' | 'reportName' @@ -345,8 +343,7 @@ type OptimisticTaskReport = Pick< | 'reportName' | 'description' | 'ownerAccountID' - | 'participantAccountIDs' - | 'visibleChatMemberAccountIDs' + | 'participants' | 'managerID' | 'type' | 'parentReportID' @@ -385,8 +382,7 @@ type OptimisticIOUReport = Pick< | 'managerID' | 'policyID' | 'ownerAccountID' - | 'participantAccountIDs' - | 'visibleChatMemberAccountIDs' + | 'participants' | 'reportID' | 'stateNum' | 'statusNum' @@ -446,6 +442,7 @@ type OptionData = { isDisabled?: boolean | null; name?: string | null; isSelfDM?: boolean; + isOneOnOneChat?: boolean; reportID?: string; enabled?: boolean; code?: string; @@ -1051,7 +1048,10 @@ function isSystemChat(report: OnyxEntry): boolean { * Only returns true if this is our main 1:1 DM report with Concierge */ function isConciergeChatReport(report: OnyxEntry): boolean { - return report?.participantAccountIDs?.length === 1 && Number(report.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); + const participantAccountIDs = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID); + return participantAccountIDs.length === 1 && participantAccountIDs[0] === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } function findSelfDMReportID(): string | undefined { @@ -1107,7 +1107,7 @@ function isProcessingReport(report: OnyxEntry | EmptyObject): boolean { * and personal detail of participant is optimistic data */ function shouldDisableDetailPage(report: OnyxEntry): boolean { - const participantAccountIDs = report?.participantAccountIDs ?? []; + const participantAccountIDs = Object.keys(report?.participants ?? {}).map(Number); if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report) || isTaskReport(report)) { return false; @@ -1122,8 +1122,10 @@ function shouldDisableDetailPage(report: OnyxEntry): boolean { * Returns true if this report has only one participant and it's an Expensify account. */ function isExpensifyOnlyParticipantInReport(report: OnyxEntry): boolean { - const reportParticipants = report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserAccountID) ?? []; - return reportParticipants.length === 1 && reportParticipants.some((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); + const otherParticipants = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID); + return otherParticipants.length === 1 && otherParticipants.some((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); } /** @@ -1132,8 +1134,10 @@ function isExpensifyOnlyParticipantInReport(report: OnyxEntry): boolean * */ function canCreateTaskInReport(report: OnyxEntry): boolean { - const otherReportParticipants = report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserAccountID) ?? []; - const areExpensifyAccountsOnlyOtherParticipants = otherReportParticipants?.length >= 1 && otherReportParticipants?.every((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); + const otherParticipants = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID); + const areExpensifyAccountsOnlyOtherParticipants = otherParticipants.length >= 1 && otherParticipants.every((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); if (areExpensifyAccountsOnlyOtherParticipants && isDM(report)) { return false; } @@ -1187,7 +1191,7 @@ function findLastAccessedReport( // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. // Domain rooms are now the only type of default room that are on the defaultRooms beta. sortedReports = sortedReports.filter( - (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(report?.participantAccountIDs ?? []), + (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number)), ); } @@ -1301,13 +1305,6 @@ function isPolicyAdmin(policyID: string, policies: OnyxCollection): bool return policyRole === CONST.POLICY.ROLE.ADMIN; } -/** - * Returns true if report has a single participant. - */ -function hasSingleParticipant(report: OnyxEntry): boolean { - return report?.participantAccountIDs?.length === 1; -} - /** * Checks whether all the transactions linked to the IOU report are of the Distance Request type with pending routes */ @@ -1420,7 +1417,9 @@ function isOneTransactionThread(reportID: string, parentReportID: string): boole * */ function isOneOnOneChat(report: OnyxEntry): boolean { - const participantAccountIDs = report?.participantAccountIDs ?? []; + const participantAccountIDs = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID); return ( !isChatRoom(report) && !isExpenseRequest(report) && @@ -1585,7 +1584,8 @@ function getRoomWelcomeMessage(report: OnyxEntry, isUserPolicyAdmin: boo * Returns true if Concierge is one of the chat participants (1:1 as well as group chats) */ function chatIncludesConcierge(report: Partial>): boolean { - return Boolean(report?.participantAccountIDs?.length && report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CONCIERGE)); + const participantAccountIDs = Object.keys(report?.participants ?? {}).map(Number); + return participantAccountIDs.includes(CONST.ACCOUNT_ID.CONCIERGE); } /** @@ -1601,29 +1601,37 @@ function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAcc // get parent report and use its participants array. if (isThread(report) && !(isTaskReport(report) || isMoneyRequestReport(report))) { const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; - if (hasSingleParticipant(parentReport)) { + if (isOneOnOneChat(parentReport)) { finalReport = parentReport; } } - let finalParticipantAccountIDs: number[] | undefined = []; + let finalParticipantAccountIDs: number[] = []; if (isMoneyRequestReport(report)) { - // For money requests i.e the IOU (1:1 person) and Expense (1:* person) reports, use the full `initialParticipantAccountIDs` array - // and add the `ownerAccountId`. Money request reports don't add `ownerAccountId` in `participantAccountIDs` array - const defaultParticipantAccountIDs = finalReport?.participantAccountIDs ?? []; + // For money requests i.e the IOU (1:1 person) and Expense (1:* person) reports, use the full `participants` + // and add the `ownerAccountId`. Money request reports don't add `ownerAccountId` in `participants` array + const defaultParticipantAccountIDs = Object.keys(finalReport?.participants ?? {}).map(Number); const setOfParticipantAccountIDs = new Set(report?.ownerAccountID ? [...defaultParticipantAccountIDs, report.ownerAccountID] : defaultParticipantAccountIDs); finalParticipantAccountIDs = [...setOfParticipantAccountIDs]; } else if (isTaskReport(report)) { - // Task reports `managerID` will change when assignee is changed, in that case the old `managerID` is still present in `participantAccountIDs` - // array along with the new one. We only need the `managerID` as a participant here. + // Task reports `managerID` will change when assignee is changed, in that case the old `managerID` is still present in `participants` + // along with the new one. We only need the `managerID` as a participant here. finalParticipantAccountIDs = report?.managerID ? [report?.managerID] : []; } else { - finalParticipantAccountIDs = finalReport?.participantAccountIDs; + finalParticipantAccountIDs = Object.keys(finalReport?.participants ?? {}).map(Number); } - const reportParticipants = finalParticipantAccountIDs?.filter((accountID) => accountID !== currentLoginAccountID) ?? []; - const participantsWithoutExpensifyAccountIDs = reportParticipants.filter((participant) => !CONST.EXPENSIFY_ACCOUNT_IDS.includes(participant ?? 0)); - return participantsWithoutExpensifyAccountIDs; + const otherParticipantsWithoutExpensifyAccountIDs = finalParticipantAccountIDs.filter((accountID) => { + if (accountID === currentLoginAccountID) { + return false; + } + if (CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)) { + return false; + } + return true; + }); + + return otherParticipantsWithoutExpensifyAccountIDs; } /** @@ -2059,12 +2067,20 @@ function getIcons( return icons; } - return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails); + if (isOneOnOneChat(report)) { + const otherParticipantsAccountIDs = Object.keys(report.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID); + return getIconsForParticipants(otherParticipantsAccountIDs, personalDetails); + } + + const participantAccountIDs = Object.keys(report.participants ?? {}).map(Number); + return getIconsForParticipants(participantAccountIDs, personalDetails); } function getDisplayNamesWithTooltips( personalDetailsList: PersonalDetails[] | PersonalDetailsList | OptionData[], - isMultipleParticipantReport: boolean, + shouldUseShortForm: boolean, shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false, ): DisplayNameWithTooltips { @@ -2074,7 +2090,7 @@ function getDisplayNamesWithTooltips( .map((user) => { const accountID = Number(user?.accountID); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden, shouldAddCurrentUserPostfix) || user?.login || ''; + const displayName = getDisplayNameForParticipant(accountID, shouldUseShortForm, shouldFallbackToHidden, shouldAddCurrentUserPostfix) || user?.login || ''; const avatar = UserUtils.getDefaultAvatar(accountID); let pronouns = user?.pronouns ?? undefined; @@ -3198,12 +3214,11 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } // Not a room or PolicyExpenseChat, generate title from first 5 other participants - const participantAccountIDs = report?.participantAccountIDs?.slice(0, 6) ?? []; - const participantsWithoutCurrentUser = participantAccountIDs.filter((accountID) => accountID !== currentUserAccountID); + const participantsWithoutCurrentUser = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID) + .slice(0, 5); const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1; - if (participantsWithoutCurrentUser.length > 5) { - participantsWithoutCurrentUser.pop(); - } return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport)).join(', '); } @@ -3215,8 +3230,10 @@ function getPayeeName(report: OnyxEntry): string | undefined { return undefined; } - const participantAccountIDs = report?.participantAccountIDs ?? []; - const participantsWithoutCurrentUser = participantAccountIDs.filter((accountID) => accountID !== currentUserAccountID); + const participantsWithoutCurrentUser = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID); + if (participantsWithoutCurrentUser.length === 0) { return undefined; } @@ -3283,17 +3300,19 @@ function getParentNavigationSubtitle(report: OnyxEntry): ParentNavigatio * Navigate to the details page of a given report */ function navigateToDetailsPage(report: OnyxEntry) { - const participantAccountIDs = report?.participantAccountIDs ?? []; + const isSelfDMReport = isSelfDM(report); + const isOneOnOneChatReport = isOneOnOneChat(report); - if (isSelfDM(report)) { - Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserAccountID ?? 0)); - return; - } + // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants + const participantAccountID = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChatReport); - if (isOneOnOneChat(report)) { - Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountIDs[0])); + if (isSelfDMReport || isOneOnOneChatReport) { + Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountID[0])); return; } + if (report?.reportID) { Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID)); } @@ -3303,10 +3322,18 @@ function navigateToDetailsPage(report: OnyxEntry) { * Go back to the details page of a given report */ function goBackToDetailsPage(report: OnyxEntry) { - if (isOneOnOneChat(report)) { - Navigation.goBack(ROUTES.PROFILE.getRoute(report?.participantAccountIDs?.[0] ?? '')); + const isOneOnOneChatReport = isOneOnOneChat(report); + + // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants + const participantAccountID = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChatReport); + + if (isOneOnOneChatReport) { + Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountID[0])); return; } + Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '')); } @@ -3558,8 +3585,10 @@ function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number const personalDetails = getPersonalDetailsForAccountID(payerAccountID); const payerEmail = 'login' in personalDetails ? personalDetails.login : ''; - // When creating a report the participantsAccountIDs and visibleChatMemberAccountIDs are the same - const participantsAccountIDs = [payeeAccountID, payerAccountID]; + const participants: Participants = { + [payeeAccountID]: {hidden: false}, + [payerAccountID]: {hidden: false}, + }; return { type: CONST.REPORT.TYPE.IOU, @@ -3568,8 +3597,7 @@ function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number currency, managerID: payerAccountID, ownerAccountID: payeeAccountID, - participantAccountIDs: participantsAccountIDs, - visibleChatMemberAccountIDs: participantsAccountIDs, + participants, reportID: generateReportID(), stateNum: isSendingMoney ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: isSendingMoney ? CONST.REPORT.STATUS_NUM.REIMBURSED : CONST.REPORT.STATE_NUM.SUBMITTED, @@ -4301,10 +4329,6 @@ function buildOptimisticChatReport( ownerAccountID: ownerAccountID || CONST.REPORT.OWNER_ACCOUNT_ID_FAKE, parentReportActionID, parentReportID, - // When creating a report the participantsAccountIDs and visibleChatMemberAccountIDs are the same - participantAccountIDs: participantList, - visibleChatMemberAccountIDs: participantList, - // For group chats we need to have participants object as we are migrating away from `participantAccountIDs` and `visibleChatMemberAccountIDs`. See https://github.com/Expensify/App/issues/34692 participants, policyID, reportID: optimisticReportID || generateReportID(), @@ -4761,16 +4785,22 @@ function buildOptimisticTaskReport( policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, notificationPreference: NotificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, ): OptimisticTaskReport { - // When creating a report the participantsAccountIDs and visibleChatMemberAccountIDs are the same - const participantsAccountIDs = assigneeAccountID && assigneeAccountID !== ownerAccountID ? [assigneeAccountID] : []; + const participants: Participants = { + [ownerAccountID]: { + hidden: false, + }, + }; + + if (assigneeAccountID) { + participants[assigneeAccountID] = {hidden: false}; + } return { reportID: generateReportID(), reportName: title, description, ownerAccountID, - participantAccountIDs: participantsAccountIDs, - visibleChatMemberAccountIDs: participantsAccountIDs, + participants, managerID: assigneeAccountID, type: CONST.REPORT.TYPE.TASK, parentReportID, @@ -4944,7 +4974,7 @@ function canSeeDefaultRoom(report: OnyxEntry, policies: OnyxCollection

{ + const participantAccountIDs = Object.keys(report?.participants ?? {}); + // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it if ( - !report || - report.participantAccountIDs?.length === 0 || + participantAccountIDs.length === 0 || isChatThread(report) || isTaskReport(report) || isMoneyRequestReport(report) || @@ -5171,8 +5204,10 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec return false; } + const sortedParticipantsAccountIDs = participantAccountIDs.map(Number).sort(); + // Only return the chat if it has all the participants - return lodashIsEqual(sortedNewParticipantList, report.participantAccountIDs?.sort()); + return lodashIsEqual(sortedNewParticipantList, sortedParticipantsAccountIDs); }) ?? null ); } @@ -5204,11 +5239,15 @@ function getChatByParticipantsAndPolicy(newParticipantList: number[], policyID: newParticipantList.sort(); return ( Object.values(allReports ?? {}).find((report) => { + const participantAccountIDs = Object.keys(report?.participants ?? {}); + // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it - if (!report?.participantAccountIDs) { + if (!report || participantAccountIDs.length === 0) { return false; } - const sortedParticipantsAccountIDs = report.participantAccountIDs?.sort(); + + const sortedParticipantsAccountIDs = participantAccountIDs.map(Number).sort(); + // Only return the room if it has all the participants and is not a policy room return report.policyID === policyID && newParticipantList.every((newParticipant) => sortedParticipantsAccountIDs.includes(newParticipant)); }) ?? null @@ -5223,7 +5262,8 @@ function getAllPolicyReports(policyID: string): Array> { * Returns true if Chronos is one of the chat participants (1:1) */ function chatIncludesChronos(report: OnyxEntry | EmptyObject): boolean { - return Boolean(report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CHRONOS)); + const participantAccountIDs = Object.keys(report?.participants ?? {}).map(Number); + return participantAccountIDs.includes(CONST.ACCOUNT_ID.CHRONOS); } /** @@ -5484,7 +5524,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry currentUserPersonalDetails?.accountID !== accountID); - const hasSingleOtherParticipantInReport = otherParticipants.length === 1; + const hasSingleParticipantInReport = otherParticipants.length === 1; let options: IOUType[] = []; if (isSelfDM(report)) { @@ -5517,7 +5557,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry): Repo * Check if the report can create the expense with type is iouType */ function canCreateRequest(report: OnyxEntry, policy: OnyxEntry, iouType: (typeof CONST.IOU.TYPE)[keyof typeof CONST.IOU.TYPE]): boolean { - const participantAccountIDs = report?.participantAccountIDs ?? []; + const participantAccountIDs = Object.keys(report?.participants ?? {}).map(Number); if (!canUserPerformWriteAction(report)) { return false; } @@ -5878,6 +5918,8 @@ function getTaskAssigneeChatOnyxData( createChat: null, }, isOptimisticReport: false, + // BE will send a different participant. We clear the optimistic one to avoid duplicated entries + participants: {[assigneeAccountID]: null}, }, }); @@ -6030,7 +6072,9 @@ function isDeprecatedGroupDM(report: OnyxEntry): boolean { !isMoneyRequestReport(report) && !isArchivedRoom(report) && !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && - (report.participantAccountIDs?.length ?? 0) > 1, + Object.keys(report.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID).length > 1, ); } @@ -6056,12 +6100,7 @@ function isReportParticipant(accountID: number, report: OnyxEntry): bool return false; } - // If we have a DM AND the accountID we are checking is the current user THEN we won't find them as a participant and must assume they are a participant - if (isDM(report) && accountID === currentUserAccountID) { - return true; - } - - const possibleAccountIDs = report?.participantAccountIDs ?? []; + const possibleAccountIDs = Object.keys(report?.participants ?? {}).map(Number); if (report?.ownerAccountID) { possibleAccountIDs.push(report?.ownerAccountID); } @@ -6774,7 +6813,6 @@ export { hasOnlyHeldExpenses, hasOnlyTransactionsWithPendingRoutes, hasReportNameError, - hasSingleParticipant, hasSmartscanError, hasUpdatedTotal, hasViolations, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 8d9db078aa06..b4ae942547a1 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -44,6 +44,14 @@ Onyx.connect({ }, }); +let currentUserAccountID: number | undefined; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + currentUserAccountID = value?.accountID; + }, +}); + function compareStringDates(a: string, b: string): 0 | 1 | -1 { if (a < b) { return -1; @@ -233,12 +241,15 @@ function getOptionData({ isDeletedParentAction: false, }; - let participantAccountIDs = report.participantAccountIDs ?? []; - - // Currently, currentUser is not included in participantAccountIDs, so for selfDM we need to add the currentUser(report owner) as participants. - if (ReportUtils.isSelfDM(report)) { - participantAccountIDs = [report.ownerAccountID ?? 0]; - } + // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants + const isOneOnOneChat = ReportUtils.isOneOnOneChat(report); + const participantAccountIDs = Object.keys(report.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChat); + const visibleParticipantAccountIDs = Object.entries(report.participants ?? {}) + .filter(([, participant]) => participant && !participant.hidden) + .map(([accountID]) => Number(accountID)) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChat); const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails)) as PersonalDetails[]; const personalDetail = participantPersonalDetailList[0] ?? {}; @@ -269,7 +280,6 @@ function getOptionData({ result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); - result.tooltipText = ReportUtils.getReportParticipantsTitle(report.visibleChatMemberAccountIDs ?? []); result.hasOutstandingChildRequest = report.hasOutstandingChildRequest; result.parentReportID = report.parentReportID ?? ''; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; @@ -278,6 +288,8 @@ function getOptionData({ result.chatType = report.chatType; result.isDeletedParentAction = report.isDeletedParentAction; result.isSelfDM = ReportUtils.isSelfDM(report); + result.isOneOnOneChat = isOneOnOneChat; + result.tooltipText = ReportUtils.getReportParticipantsTitle(visibleParticipantAccountIDs); const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || ReportUtils.isExpenseReport(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8e76c781bd7f..bdf6ca823369 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -460,6 +460,7 @@ function buildOnyxDataForMoneyRequest( const outstandingChildRequest = ReportUtils.getOutstandingChildRequest(iouReport); const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; let newQuickAction: ValueOf = isScanRequest ? CONST.QUICK_ACTIONS.REQUEST_SCAN : CONST.QUICK_ACTIONS.REQUEST_MANUAL; if (TransactionUtils.isDistanceRequest(transaction)) { newQuickAction = CONST.QUICK_ACTIONS.REQUEST_DISTANCE; @@ -586,12 +587,27 @@ function buildOnyxDataForMoneyRequest( }); } + const redundantParticipants: Record = {}; if (!isEmptyObject(optimisticPersonalDetailListAction)) { + const successPersonalDetailListAction: Record = {}; + + // BE will send different participants. We clear the optimistic ones to avoid duplicated entries + Object.keys(optimisticPersonalDetailListAction).forEach((accountIDKey) => { + const accountID = Number(accountIDKey); + successPersonalDetailListAction[accountID] = null; + redundantParticipants[accountID] = null; + }); + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: optimisticPersonalDetailListAction, }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: successPersonalDetailListAction, + }); } if (!isEmptyObject(optimisticNextStep)) { @@ -602,13 +618,12 @@ function buildOnyxDataForMoneyRequest( }); } - const successData: OnyxUpdate[] = []; - if (isNewChatReport) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, value: { + participants: redundantParticipants, pendingFields: null, errorFields: null, isOptimisticReport: false, @@ -621,16 +636,20 @@ function buildOnyxDataForMoneyRequest( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, value: { + participants: redundantParticipants, pendingFields: null, errorFields: null, + isOptimisticReport: false, }, }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, value: { + participants: redundantParticipants, pendingFields: null, errorFields: null, + isOptimisticReport: false, }, }, { @@ -883,6 +902,7 @@ function buildOnyxDataForInvoice( value: null, }, ]; + const successData: OnyxUpdate[] = []; if (chatReport) { optimisticData.push({ @@ -915,29 +935,48 @@ function buildOnyxDataForInvoice( }); } + const redundantParticipants: Record = {}; if (!isEmptyObject(optimisticPersonalDetailListAction)) { + const successPersonalDetailListAction: Record = {}; + + // BE will send different participants. We clear the optimistic ones to avoid duplicated entries + Object.keys(optimisticPersonalDetailListAction).forEach((accountIDKey) => { + const accountID = Number(accountIDKey); + successPersonalDetailListAction[accountID] = null; + redundantParticipants[accountID] = null; + }); + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: optimisticPersonalDetailListAction, }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: successPersonalDetailListAction, + }); } - const successData: OnyxUpdate[] = [ + successData.push( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, value: { + participants: redundantParticipants, pendingFields: null, errorFields: null, + isOptimisticReport: false, }, }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, value: { + participants: redundantParticipants, pendingFields: null, errorFields: null, + isOptimisticReport: false, }, }, { @@ -989,13 +1028,14 @@ function buildOnyxDataForInvoice( }, }, }, - ]; + ); if (isNewChatReport) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, value: { + participants: redundantParticipants, pendingFields: null, errorFields: null, isOptimisticReport: false, @@ -1773,13 +1813,13 @@ function getMoneyRequestInformation( } if (!chatReport) { - chatReport = ReportUtils.getChatByParticipants([payerAccountID]); + chatReport = ReportUtils.getChatByParticipants([payerAccountID, payeeAccountID]); } // If we still don't have a report, it likely doens't exist and we need to build an optimistic one if (!chatReport) { isNewChatReport = true; - chatReport = ReportUtils.buildOptimisticChatReport([payerAccountID]); + chatReport = ReportUtils.buildOptimisticChatReport([payerAccountID, payeeAccountID]); } // STEP 2: Get the Expense/IOU report. If the moneyRequestReportID has been provided, we want to add the transaction to this specific report. @@ -3623,8 +3663,9 @@ function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string, // If we do not have one locally then we will search for a chat with the same participants (only for 1:1 chats). const shouldGetOrCreateOneOneDM = participants.length < 2; + const allParticipantsAccountIDs = [...participantAccountIDs, currentUserAccountID]; if (!existingSplitChatReport && shouldGetOrCreateOneOneDM) { - existingSplitChatReport = ReportUtils.getChatByParticipants(participantAccountIDs); + existingSplitChatReport = ReportUtils.getChatByParticipants(allParticipantsAccountIDs); } // We found an existing chat report we are done... @@ -3633,9 +3674,6 @@ function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string, return {existingSplitChatReport, splitChatReport: existingSplitChatReport}; } - // No existing chat by this point we need to create it - const allParticipantsAccountIDs = [...participantAccountIDs, currentUserAccountID]; - // Create a Group Chat if we have multiple participants if (participants.length > 1) { const splitChatReport = ReportUtils.buildOptimisticChatReport( @@ -3794,7 +3832,6 @@ function createSplitsAndOnyxData( value: null, }, ]; - const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -3811,11 +3848,12 @@ function createSplitsAndOnyxData( }, ]; + const redundantParticipants: Record = {}; if (!existingSplitChatReport) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${splitChatReport.reportID}`, - value: {pendingFields: {createChat: null}}, + value: {pendingFields: {createChat: null}, participants: redundantParticipants}, }); } @@ -3903,10 +3941,10 @@ function createSplitsAndOnyxData( oneOnOneChatReport = splitChatReport; shouldCreateOptimisticPersonalDetails = !existingSplitChatReport && !personalDetailExists; } else { - const existingChatReport = ReportUtils.getChatByParticipants([accountID]); + const existingChatReport = ReportUtils.getChatByParticipants([accountID, currentUserAccountID]); isNewOneOnOneChatReport = !existingChatReport; shouldCreateOptimisticPersonalDetails = isNewOneOnOneChatReport && !personalDetailExists; - oneOnOneChatReport = existingChatReport ?? ReportUtils.buildOptimisticChatReport([accountID]); + oneOnOneChatReport = existingChatReport ?? ReportUtils.buildOptimisticChatReport([accountID, currentUserAccountID]); } // STEP 2: Get existing IOU/Expense report and update its total OR build a new optimistic one @@ -3979,6 +4017,11 @@ function createSplitsAndOnyxData( } : {}; + if (shouldCreateOptimisticPersonalDetails) { + // BE will send different participants. We clear the optimistic ones to avoid duplicated entries + redundantParticipants[accountID] = null; + } + let oneOnOneReportPreviewAction = getReportPreviewAction(oneOnOneChatReport.reportID, oneOnOneIOUReport.reportID); if (oneOnOneReportPreviewAction) { oneOnOneReportPreviewAction = ReportUtils.updateReportPreview(oneOnOneIOUReport, oneOnOneReportPreviewAction as ReportPreviewAction); @@ -4352,11 +4395,12 @@ function startSplitBill({ }, ]; + const redundantParticipants: Record = {}; if (!existingSplitChatReport) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${splitChatReport.reportID}`, - value: {pendingFields: {createChat: null}}, + value: {pendingFields: {createChat: null}, participants: redundantParticipants}, }); } @@ -4445,6 +4489,8 @@ function startSplitBill({ }, }, }); + // BE will send different participants. We clear the optimistic ones to avoid duplicated entries + redundantParticipants[accountID] = null; } splits.push({ @@ -4619,9 +4665,9 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA // The workspace chat reportID is saved in the splits array when starting a split expense with a workspace oneOnOneChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`] ?? null; } else { - const existingChatReport = ReportUtils.getChatByParticipants(participant.accountID ? [participant.accountID] : []); + const existingChatReport = ReportUtils.getChatByParticipants(participant.accountID ? [participant.accountID, sessionAccountID] : []); isNewOneOnOneChatReport = !existingChatReport; - oneOnOneChatReport = existingChatReport ?? ReportUtils.buildOptimisticChatReport(participant.accountID ? [participant.accountID] : []); + oneOnOneChatReport = existingChatReport ?? ReportUtils.buildOptimisticChatReport(participant.accountID ? [participant.accountID, sessionAccountID] : []); } let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport?.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null; @@ -5425,10 +5471,10 @@ function getSendMoneyParams( idempotencyKey: Str.guid(), }); - let chatReport = !isEmptyObject(report) && report?.reportID ? report : ReportUtils.getChatByParticipants([recipientAccountID]); + let chatReport = !isEmptyObject(report) && report?.reportID ? report : ReportUtils.getChatByParticipants([recipientAccountID, managerID]); let isNewChat = false; if (!chatReport) { - chatReport = ReportUtils.buildOptimisticChatReport([recipientAccountID]); + chatReport = ReportUtils.buildOptimisticChatReport([recipientAccountID, managerID]); isNewChat = true; } const optimisticIOUReport = ReportUtils.buildOptimisticIOUReport(recipientAccountID, managerID, amount, chatReport.reportID, currency, true); @@ -5529,7 +5575,61 @@ function getSendMoneyParams( }, }; - const successData: OnyxUpdate[] = [ + const successData: OnyxUpdate[] = []; + + // Add optimistic personal details for recipient + let optimisticPersonalDetailListData: OnyxUpdate | EmptyObject = {}; + const optimisticPersonalDetailListAction = isNewChat + ? { + [recipientAccountID]: { + accountID: recipientAccountID, + avatar: UserUtils.getDefaultAvatarURL(recipient.accountID), + // Disabling this line since participant.displayName can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + displayName: recipient.displayName || recipient.login, + login: recipient.login, + }, + } + : {}; + + const redundantParticipants: Record = {}; + if (!isEmptyObject(optimisticPersonalDetailListAction)) { + const successPersonalDetailListAction: Record = {}; + + // BE will send different participants. We clear the optimistic ones to avoid duplicated entries + Object.keys(optimisticPersonalDetailListAction).forEach((accountIDKey) => { + const accountID = Number(accountIDKey); + successPersonalDetailListAction[accountID] = null; + redundantParticipants[accountID] = null; + }); + + optimisticPersonalDetailListData = { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: optimisticPersonalDetailListAction, + }; + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: successPersonalDetailListAction, + }); + } + + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticIOUReport.reportID}`, + value: { + participants: redundantParticipants, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, + value: { + participants: redundantParticipants, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticIOUReport.reportID}`, @@ -5562,7 +5662,7 @@ function getSendMoneyParams( }, }, }, - ]; + ); const failureData: OnyxUpdate[] = [ { @@ -5592,14 +5692,12 @@ function getSendMoneyParams( }, ]; - let optimisticPersonalDetailListData: OnyxUpdate | EmptyObject = {}; - // Now, let's add the data we need just when we are creating a new chat report if (isNewChat) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, - value: {pendingFields: null}, + value: {pendingFields: null, participants: redundantParticipants}, }); failureData.push( { @@ -5622,22 +5720,6 @@ function getSendMoneyParams( }, ); - // Add optimistic personal details for recipient - optimisticPersonalDetailListData = { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [recipientAccountID]: { - accountID: recipientAccountID, - avatar: UserUtils.getDefaultAvatarURL(recipient.accountID), - // Disabling this line since participant.displayName can be an empty string - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - displayName: recipient.displayName || recipient.login, - login: recipient.login, - }, - }, - }; - if (optimisticChatReportActionsData.value) { // Add an optimistic created action to the optimistic chat reportActions data optimisticChatReportActionsData.value[optimisticCreatedActionForChat.reportActionID] = optimisticCreatedActionForChat; @@ -6413,7 +6495,10 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On if (ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport) { participants = [{accountID: 0, reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}]; } else { - participants = (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); + const chatReportOtherParticipants = Object.keys(chatReport?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID); + participants = chatReportOtherParticipants.map((accountID) => ({accountID, selected: true})); if (ReportUtils.isInvoiceRoom(chatReport)) { participants = [ diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index ba7574c41ae3..7f346933873d 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -459,30 +459,23 @@ function buildAnnounceRoomMembersOnyxData(policyID: string, accountIDs: number[] return announceRoomMembers; } - if (announceReport?.participantAccountIDs) { - // Everyone in special policy rooms is visible - const participantAccountIDs = [...announceReport.participantAccountIDs, ...accountIDs]; - const pendingChatMembers = ReportUtils.getPendingChatMembers(accountIDs, announceReport?.pendingChatMembers ?? [], CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + const participantAccountIDs = [...Object.keys(announceReport.participants ?? {}).map(Number), ...accountIDs]; + const pendingChatMembers = ReportUtils.getPendingChatMembers(accountIDs, announceReport?.pendingChatMembers ?? [], CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - announceRoomMembers.onyxOptimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport?.reportID}`, - value: { - participants: ReportUtils.buildParticipantsFromAccountIDs(participantAccountIDs), - participantAccountIDs, - visibleChatMemberAccountIDs: participantAccountIDs, - pendingChatMembers, - }, - }); - } + announceRoomMembers.onyxOptimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport?.reportID}`, + value: { + participants: ReportUtils.buildParticipantsFromAccountIDs(participantAccountIDs), + pendingChatMembers, + }, + }); announceRoomMembers.onyxFailureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport?.reportID}`, value: { - participants: announceReport?.participants, - participantAccountIDs: announceReport?.participantAccountIDs, - visibleChatMemberAccountIDs: announceReport?.visibleChatMemberAccountIDs, + participants: announceReport?.participants ?? null, pendingChatMembers: announceReport?.pendingChatMembers ?? null, }, }); @@ -797,45 +790,43 @@ function removeOptimisticAnnounceRoomMembers(policyID: string, policyName: strin return announceRoomMembers; } - if (announceReport?.participantAccountIDs) { - const pendingChatMembers = ReportUtils.getPendingChatMembers(accountIDs, announceReport?.pendingChatMembers ?? [], CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + const pendingChatMembers = ReportUtils.getPendingChatMembers(accountIDs, announceReport?.pendingChatMembers ?? [], CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - announceRoomMembers.onyxOptimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, - value: { - pendingChatMembers, - ...(accountIDs.includes(sessionAccountID) - ? { - statusNum: CONST.REPORT.STATUS_NUM.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.APPROVED, - oldPolicyName: policyName, - } - : {}), - }, - }); - announceRoomMembers.onyxFailureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, - value: { - pendingChatMembers: announceReport?.pendingChatMembers ?? null, - ...(accountIDs.includes(sessionAccountID) - ? { - statusNum: announceReport.statusNum, - stateNum: announceReport.stateNum, - oldPolicyName: announceReport.oldPolicyName, - } - : {}), - }, - }); - announceRoomMembers.onyxSuccessData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, - value: { - pendingChatMembers: announceReport?.pendingChatMembers ?? null, - }, - }); - } + announceRoomMembers.onyxOptimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, + value: { + pendingChatMembers, + ...(accountIDs.includes(sessionAccountID) + ? { + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + oldPolicyName: policyName, + } + : {}), + }, + }); + announceRoomMembers.onyxFailureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, + value: { + pendingChatMembers: announceReport?.pendingChatMembers ?? null, + ...(accountIDs.includes(sessionAccountID) + ? { + statusNum: announceReport.statusNum, + stateNum: announceReport.stateNum, + oldPolicyName: announceReport.oldPolicyName, + } + : {}), + }, + }); + announceRoomMembers.onyxSuccessData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, + value: { + pendingChatMembers: announceReport?.pendingChatMembers ?? null, + }, + }); return announceRoomMembers; } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7bb85bb7998c..5bf51b9747d1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -514,7 +514,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { clientCreatedTime: file ? attachmentAction?.created : reportCommentAction?.created, }; - if (reportIDDeeplinkedFromOldDot === reportID && report?.participantAccountIDs?.length === 1 && Number(report.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE) { + if (reportIDDeeplinkedFromOldDot === reportID && ReportUtils.isConciergeChatReport(report)) { parameters.isOldDotConciergeChat = true; } @@ -843,46 +843,51 @@ function openReport( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: {[optimisticCreatedAction.reportActionID]: optimisticCreatedAction}, }); - successData.push( - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: {[optimisticCreatedAction.reportActionID]: {pendingAction: null}}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - pendingFields: { - createChat: null, - }, - errorFields: { - createChat: null, - }, - isOptimisticReport: false, - }, - }, - ); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[optimisticCreatedAction.reportActionID]: {pendingAction: null}}, + }); // Add optimistic personal details for new participants const optimisticPersonalDetails: OnyxCollection = {}; const settledPersonalDetails: OnyxCollection = {}; + const redundantParticipants: Record = {}; + const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(participantLoginList); participantLoginList.forEach((login, index) => { - const accountID = newReportObject?.participantAccountIDs?.[index]; + const accountID = participantAccountIDs[index]; + const isOptimisticAccount = !allPersonalDetails?.[accountID]; - if (!accountID) { + if (!isOptimisticAccount) { return; } - optimisticPersonalDetails[accountID] = allPersonalDetails?.[accountID] ?? { + optimisticPersonalDetails[accountID] = { login, accountID, avatar: UserUtils.getDefaultAvatarURL(accountID), displayName: login, isOptimisticPersonalDetail: true, }; + settledPersonalDetails[accountID] = null; + + // BE will send different participants. We clear the optimistic ones to avoid duplicated entries + redundantParticipants[accountID] = null; + }); - settledPersonalDetails[accountID] = allPersonalDetails?.[accountID] ?? null; + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + participants: redundantParticipants, + pendingFields: { + createChat: null, + }, + errorFields: { + createChat: null, + }, + isOptimisticReport: false, + }, }); optimisticData.push({ @@ -953,14 +958,15 @@ function navigateToAndOpenReport( // If we are not creating a new Group Chat then we are creating a 1:1 DM and will look for an existing chat if (!isGroupChat) { - chat = ReportUtils.getChatByParticipants(participantAccountIDs); + chat = ReportUtils.getChatByParticipants([...participantAccountIDs, currentUserAccountID]); } if (isEmptyObject(chat)) { if (isGroupChat) { + // If we are creating a group chat then participantAccountIDs is expected to contain currentUserAccountID newChat = ReportUtils.buildOptimisticGroupChatReport(participantAccountIDs, reportName ?? '', avatarUri ?? '', optimisticReportID); } else { - newChat = ReportUtils.buildOptimisticChatReport(participantAccountIDs); + newChat = ReportUtils.buildOptimisticChatReport([...participantAccountIDs, currentUserAccountID]); } } const report = isEmptyObject(chat) ? newChat : chat; @@ -982,9 +988,9 @@ function navigateToAndOpenReport( */ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[]) { let newChat: ReportUtils.OptimisticChatReport | EmptyObject = {}; - const chat = ReportUtils.getChatByParticipants(participantAccountIDs); + const chat = ReportUtils.getChatByParticipants([...participantAccountIDs, currentUserAccountID]); if (!chat) { - newChat = ReportUtils.buildOptimisticChatReport(participantAccountIDs); + newChat = ReportUtils.buildOptimisticChatReport([...participantAccountIDs, currentUserAccountID]); } const report = chat ?? newChat; @@ -1024,7 +1030,7 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: P parentReportID, ); - const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(newChat?.participantAccountIDs ?? []); + const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(Object.keys(newChat.participants ?? {}).map(Number)); openReport(newChat.reportID, '', participantLogins, newChat, parentReportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(newChat.reportID)); } @@ -2532,7 +2538,7 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) { Navigation.navigate(lastAccessedReportRoute, CONST.NAVIGATION.TYPE.FORCED_UP); } else { const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); - const chat = ReportUtils.getChatByParticipants(participantAccountIDs); + const chat = ReportUtils.getChatByParticipants([...participantAccountIDs, currentUserAccountID]); if (chat?.reportID) { // If it is not a chat thread we should call Navigation.goBack to pop the current route first before navigating to Concierge. if (!isChatThread) { @@ -2662,12 +2668,6 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmails const inviteeEmails = Object.keys(inviteeEmailsToAccountIDs); const inviteeAccountIDs = Object.values(inviteeEmailsToAccountIDs); - const participantAccountIDsAfterInvitation = [...new Set([...(report?.participantAccountIDs ?? []), ...inviteeAccountIDs])].filter( - (accountID): accountID is number => typeof accountID === 'number', - ); - const visibleMemberAccountIDsAfterInvitation = [...new Set([...(report?.visibleChatMemberAccountIDs ?? []), ...inviteeAccountIDs])].filter( - (accountID): accountID is number => typeof accountID === 'number', - ); const participantsAfterInvitation = inviteeAccountIDs.reduce( (reportParticipants: Participants, accountID: number) => { @@ -2692,8 +2692,6 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmails onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - participantAccountIDs: participantAccountIDsAfterInvitation, - visibleChatMemberAccountIDs: visibleMemberAccountIDsAfterInvitation, participants: participantsAfterInvitation, pendingChatMembers, }, @@ -2716,8 +2714,6 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmails onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - participantAccountIDs: report.participantAccountIDs, - visibleChatMemberAccountIDs: report.visibleChatMemberAccountIDs, participants: inviteeAccountIDs.reduce((revertedParticipants: Record, accountID) => { // eslint-disable-next-line no-param-reassign revertedParticipants[accountID] = null; @@ -2805,24 +2801,6 @@ function removeFromRoom(reportID: string, targetAccountIDs: number[]) { } const removeParticipantsData: Record = {}; - const currentParticipants = ReportUtils.getParticipants(reportID); - if (!currentParticipants) { - return; - } - - const currentParticipantAccountIDs = ReportUtils.getParticipantAccountIDs(reportID); - const participantAccountIDsAfterRemoval: number[] = []; - const visibleChatMemberAccountIDsAfterRemoval: number[] = []; - currentParticipantAccountIDs.forEach((participantAccountID: number) => { - const participant = currentParticipants[participantAccountID]; - if (!targetAccountIDs.includes(participantAccountID)) { - participantAccountIDsAfterRemoval.push(participantAccountID); - if (!participant.hidden) { - visibleChatMemberAccountIDsAfterRemoval.push(participantAccountID); - } - } - }); - targetAccountIDs.forEach((accountID) => { removeParticipantsData[accountID] = null; }); @@ -2856,8 +2834,6 @@ function removeFromRoom(reportID: string, targetAccountIDs: number[]) { key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { participants: removeParticipantsData, - participantAccountIDs: participantAccountIDsAfterRemoval, - visibleChatMemberAccountIDs: visibleChatMemberAccountIDsAfterRemoval, pendingChatMembers: report?.pendingChatMembers ?? null, }, }, @@ -3094,7 +3070,7 @@ function completeOnboarding( ) { const targetEmail = CONST.EMAIL.CONCIERGE; const actorAccountID = PersonalDetailsUtils.getAccountIDsByLogins([targetEmail])[0]; - const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID]); + const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID, currentUserAccountID]); const {reportID: targetChatReportID = '', policyID: targetChatPolicyID = ''} = targetChatReport ?? {}; // Mention message @@ -3435,7 +3411,7 @@ function completeEngagementModal(choice: OnboardingPurposeType, text?: string) { lastReadTime: currentTime, }; - const conciergeChatReport = ReportUtils.getChatByParticipants([conciergeAccountID]); + const conciergeChatReport = ReportUtils.getChatByParticipants([conciergeAccountID, currentUserAccountID]); conciergeChatReportID = conciergeChatReport?.reportID; const report = ReportUtils.getReport(conciergeChatReportID); diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 2d88060ce5ba..89d5b46408f7 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1,4 +1,4 @@ -import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -26,7 +26,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import * as Report from './Report'; -type OptimisticReport = Pick; +type OptimisticReport = Pick; type Assignee = { icons: Icon[]; displayName: string; @@ -180,6 +180,8 @@ function createTaskAndNavigate( managerID: null, }, isOptimisticReport: false, + // BE will send a different participant. We clear the optimistic one to avoid duplicated entries + participants: {[assigneeAccountID]: null}, }, }, { @@ -529,6 +531,7 @@ function editTaskAssignee( ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, }; + const successReport: NullishDeep = {pendingFields: {...(assigneeAccountID && {managerID: null})}}; const optimisticData: OnyxUpdate[] = [ { @@ -552,7 +555,7 @@ function editTaskAssignee( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, - value: {pendingFields: {...(assigneeAccountID && {managerID: null})}}, + value: successReport, }, ]; @@ -572,12 +575,7 @@ function editTaskAssignee( // If we make a change to the assignee, we want to add a comment to the assignee's chat // Check if the assignee actually changed if (assigneeAccountID && assigneeAccountID !== report.managerID && assigneeAccountID !== ownerAccountID && assigneeChatReport) { - const participants = report?.participantAccountIDs ?? []; - const visibleMembers = report.visibleChatMemberAccountIDs ?? []; - if (!visibleMembers.includes(assigneeAccountID)) { - optimisticReport.participantAccountIDs = [...participants, assigneeAccountID]; - optimisticReport.visibleChatMemberAccountIDs = [...visibleMembers, assigneeAccountID]; - } + optimisticReport.participants = {[assigneeAccountID]: {hidden: false}}; assigneeChatReportOnyxData = ReportUtils.getTaskAssigneeChatOnyxData( currentUserAccountID, @@ -589,6 +587,11 @@ function editTaskAssignee( assigneeChatReport, ); + if (assigneeChatReport?.isOptimisticReport && assigneeChatReport.pendingFields?.createChat !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + // BE will send a different participant. We clear the optimistic one to avoid duplicated entries + successReport.participants = {[assigneeAccountID]: null}; + } + optimisticData.push(...assigneeChatReportOnyxData.optimisticData); successData.push(...assigneeChatReportOnyxData.successData); failureData.push(...assigneeChatReportOnyxData.failureData); @@ -651,7 +654,7 @@ function setAssigneeChatReport(chatReport: OnyxTypes.Report) { } function setNewOptimisticAssignee(assigneeLogin: string, assigneeAccountID: number) { - const report: ReportUtils.OptimisticChatReport = ReportUtils.buildOptimisticChatReport([assigneeAccountID]); + const report: ReportUtils.OptimisticChatReport = ReportUtils.buildOptimisticChatReport([assigneeAccountID, currentUserAccountID]); // When assigning a task to a new user, by default we share the task in their DM // However, the DM doesn't exist yet - and will be created optimistically once the task is created @@ -687,7 +690,7 @@ function setAssigneeValue( if (!isCurrentUser) { // Check for the chatReport by participants IDs if (!report) { - report = ReportUtils.getChatByParticipants([assigneeAccountID]); + report = ReportUtils.getChatByParticipants([assigneeAccountID, currentUserAccountID]); } // If chat report is still not found we need to build new optimistic chat report if (!report) { @@ -769,13 +772,18 @@ function getAssignee(assigneeAccountID: number, personalDetails: OnyxEntry, personalDetails: OnyxEntry): ShareDestination { const report = reports?.[`report_${reportID}`] ?? null; - const participantAccountIDs = report?.participantAccountIDs ?? []; - const isMultipleParticipant = participantAccountIDs.length > 1; - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); + const isOneOnOneChat = ReportUtils.isOneOnOneChat(report); + + const participants = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID || !isOneOnOneChat); + + const isMultipleParticipant = participants.length > 1; + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails), isMultipleParticipant); let subtitle = ''; - if (ReportUtils.isChatReport(report) && ReportUtils.isDM(report) && ReportUtils.hasSingleParticipant(report)) { - const participantAccountID = report?.participantAccountIDs?.[0] ?? -1; + if (isOneOnOneChat) { + const participantAccountID = participants[0] ?? -1; const displayName = personalDetails?.[participantAccountID]?.displayName ?? ''; const login = personalDetails?.[participantAccountID]?.login ?? ''; diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts index 3b0829ad9ba3..3556746dca2f 100644 --- a/src/libs/migrateOnyx.ts +++ b/src/libs/migrateOnyx.ts @@ -2,6 +2,7 @@ import Log from './Log'; import CheckForPreviousReportActionID from './migrations/CheckForPreviousReportActionID'; import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID'; import NVPMigration from './migrations/NVPMigration'; +import Participants from './migrations/Participants'; import PronounsMigration from './migrations/PronounsMigration'; import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts'; import RenameCardIsVirtual from './migrations/RenameCardIsVirtual'; @@ -23,6 +24,7 @@ export default function () { RemoveEmptyReportActionsDrafts, NVPMigration, PronounsMigration, + Participants, ]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the diff --git a/src/libs/migrations/Participants.ts b/src/libs/migrations/Participants.ts new file mode 100644 index 000000000000..3dbbef486d68 --- /dev/null +++ b/src/libs/migrations/Participants.ts @@ -0,0 +1,83 @@ +import Onyx from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; +import Log from '@libs/Log'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; +import type {Participants} from '@src/types/onyx/Report'; + +type ReportKey = `${typeof ONYXKEYS.COLLECTION.REPORT}${string}`; +type OldReport = Report & {participantAccountIDs?: number[]; visibleChatMemberAccountIDs?: number[]}; +type OldReportCollection = Record>; + +function getReports(): Promise> { + return new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connectionID); + return resolve(reports); + }, + }); + }); +} + +function getCurrentUserAccountID(): Promise { + return new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (session) => { + Onyx.disconnect(connectionID); + return resolve(session?.accountID); + }, + }); + }); +} + +export default function (): Promise { + return Promise.all([getCurrentUserAccountID(), getReports()]).then(([currentUserAccountID, reports]) => { + if (!reports) { + Log.info('[Migrate Onyx] Skipped Participants migration because there are no reports'); + return; + } + + const collection = Object.entries(reports).reduce((reportsCollection, [onyxKey, report]) => { + // If we have participantAccountIDs then this report is eligible for migration + if (report?.participantAccountIDs) { + const participants: NullishDeep = {}; + + const deprecatedParticipants = new Set(report.participantAccountIDs); + const deprecatedVisibleParticipants = new Set(report.visibleChatMemberAccountIDs); + + // Check all possible participants because some of these may be invalid https://github.com/Expensify/App/pull/40254#issuecomment-2096867084 + const possibleParticipants = new Set([ + ...report.participantAccountIDs, + ...Object.keys(report.participants ?? {}).map(Number), + ...(currentUserAccountID !== undefined ? [currentUserAccountID] : []), + ]); + + possibleParticipants.forEach((accountID) => { + if (deprecatedParticipants.has(accountID) || accountID === currentUserAccountID) { + participants[accountID] = { + hidden: report.participants?.[accountID]?.hidden ?? (!deprecatedVisibleParticipants.has(accountID) && accountID !== currentUserAccountID), + }; + } else { + participants[accountID] = null; + } + }); + + // eslint-disable-next-line no-param-reassign + reportsCollection[onyxKey as ReportKey] = { + participants, + participantAccountIDs: null, + visibleChatMemberAccountIDs: null, + }; + } + + return reportsCollection; + }, {}); + + // eslint-disable-next-line rulesdir/prefer-actions-set-data + return Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, collection).then(() => Log.info('[Migrate Onyx] Ran migration Participants successfully')); + }); +} diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index f8f88e9cc3cf..d5e556b25dd0 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -64,7 +64,7 @@ const getPhoneNumber = ({login = '', displayName = ''}: PersonalDetails | EmptyO const chatReportSelector = (report: OnyxEntry): OnyxEntry => report && { reportID: report.reportID, - participantAccountIDs: report.participantAccountIDs, + participants: report.participants, parentReportID: report.parentReportID, parentReportActionID: report.parentReportActionID, type: report.type, @@ -80,7 +80,7 @@ function ProfilePage({route}: ProfilePageProps) { const reportKey = useMemo(() => { const accountID = Number(route.params?.accountID ?? 0); - const reportID = ReportUtils.getChatByParticipants([accountID], reports)?.reportID ?? ''; + const reportID = ReportUtils.getChatByParticipants(session?.accountID ? [accountID, session.accountID] : [], reports)?.reportID ?? ''; if ((Boolean(session) && Number(session?.accountID) === accountID) || SessionActions.isAnonymousUser() || !reportID) { return `${ONYXKEYS.COLLECTION.REPORT}0` as const; diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 139406249a5e..9c1178adc76c 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -64,13 +64,14 @@ function RoomInvitePage({ }, []); // Any existing participants and Expensify emails should not be eligible for invitation - const excludedUsers = useMemo( - () => - [...PersonalDetailsUtils.getLoginsByAccountIDs(report?.visibleChatMemberAccountIDs ?? []), ...CONST.EXPENSIFY_EMAILS].map((participant) => - PhoneNumber.addSMSDomainIfPhoneNumber(participant), - ), - [report], - ); + const excludedUsers = useMemo(() => { + const visibleParticipantAccountIDs = Object.entries(report.participants ?? {}) + .filter(([, participant]) => participant && !participant.hidden) + .map(([accountID]) => Number(accountID)); + return [...PersonalDetailsUtils.getLoginsByAccountIDs(visibleParticipantAccountIDs), ...CONST.EXPENSIFY_EMAILS].map((participant) => + PhoneNumber.addSMSDomainIfPhoneNumber(participant), + ); + }, [report.participants]); useEffect(() => { const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], debouncedSearchTerm, excludedUsers); diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index 488bde658c3f..51f1b0d3f999 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -167,57 +167,61 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { const getMemberOptions = (): ListItem[] => { let result: ListItem[] = []; - report?.visibleChatMemberAccountIDs?.forEach((accountID) => { - const details = personalDetails[accountID]; + Object.entries(report.participants ?? {}) + .filter(([, participant]) => participant && !participant.hidden) + .forEach(([accountIDKey]) => { + const accountID = Number(accountIDKey); - if (!details) { - Log.hmmm(`[RoomMembersPage] no personal details found for room member with accountID: ${accountID}`); - return; - } + const details = personalDetails[accountID]; - // If search value is provided, filter out members that don't match the search value - if (searchValue.trim()) { - let memberDetails = ''; - if (details.login) { - memberDetails += ` ${details.login.toLowerCase()}`; - } - if (details.firstName) { - memberDetails += ` ${details.firstName.toLowerCase()}`; - } - if (details.lastName) { - memberDetails += ` ${details.lastName.toLowerCase()}`; - } - if (details.displayName) { - memberDetails += ` ${PersonalDetailsUtils.getDisplayNameOrDefault(details).toLowerCase()}`; - } - if (details.phoneNumber) { - memberDetails += ` ${details.phoneNumber.toLowerCase()}`; + if (!details) { + Log.hmmm(`[RoomMembersPage] no personal details found for room member with accountID: ${accountID}`); + return; } - if (!OptionsListUtils.isSearchStringMatch(searchValue.trim(), memberDetails)) { - return; + // If search value is provided, filter out members that don't match the search value + if (searchValue.trim()) { + let memberDetails = ''; + if (details.login) { + memberDetails += ` ${details.login.toLowerCase()}`; + } + if (details.firstName) { + memberDetails += ` ${details.firstName.toLowerCase()}`; + } + if (details.lastName) { + memberDetails += ` ${details.lastName.toLowerCase()}`; + } + if (details.displayName) { + memberDetails += ` ${PersonalDetailsUtils.getDisplayNameOrDefault(details).toLowerCase()}`; + } + if (details.phoneNumber) { + memberDetails += ` ${details.phoneNumber.toLowerCase()}`; + } + + if (!OptionsListUtils.isSearchStringMatch(searchValue.trim(), memberDetails)) { + return; + } } - } - const pendingChatMember = report?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString()); + const pendingChatMember = report?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString()); - result.push({ - keyForList: String(accountID), - accountID, - isSelected: selectedMembers.includes(accountID), - isDisabled: accountID === session?.accountID || pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), - alternateText: details?.login ? formatPhoneNumber(details.login) : '', - icons: [ - { - source: UserUtils.getAvatar(details.avatar, accountID), - name: details.login ?? '', - type: CONST.ICON_TYPE_AVATAR, - id: Number(accountID), - }, - ], - pendingAction: pendingChatMember?.pendingAction, + result.push({ + keyForList: String(accountID), + accountID, + isSelected: selectedMembers.includes(accountID), + isDisabled: accountID === session?.accountID || pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), + alternateText: details?.login ? formatPhoneNumber(details.login) : '', + icons: [ + { + source: UserUtils.getAvatar(details.avatar, accountID), + name: details.login ?? '', + type: CONST.ICON_TYPE_AVATAR, + id: Number(accountID), + }, + ], + pendingAction: pendingChatMember?.pendingAction, + }); }); - }); result = result.sort((value1, value2) => localeCompare(value1.text ?? '', value2.text ?? '')); diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index c0111c6f85e7..7e8ff07f1a53 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -88,10 +88,16 @@ function HeaderView({ const styles = useThemeStyles(); const isSelfDM = ReportUtils.isSelfDM(report); const isGroupChat = ReportUtils.isGroupChat(report) || ReportUtils.isDeprecatedGroupDM(report); - // Currently, currentUser is not included in participantAccountIDs, so for selfDM, we need to add the currentUser as participants. - const participants = isSelfDM ? [session?.accountID ?? -1] : (report?.participantAccountIDs ?? []).slice(0, 5); - const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); + const isOneOnOneChat = ReportUtils.isOneOnOneChat(report); + + // For 1:1 chat, we don't want to include currentUser as participants in order to not mark 1:1 chats as having multiple participants + const participants = Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== session?.accountID || !isOneOnOneChat) + .slice(0, 5); const isMultipleParticipant = participants.length > 1; + + const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant, undefined, isSelfDM); const isChatThread = ReportUtils.isChatThread(report); @@ -103,7 +109,7 @@ function HeaderView({ const title = ReportUtils.getReportName(reportHeaderData); const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(reportHeaderData); - const isConcierge = ReportUtils.hasSingleParticipant(report) && participants.includes(CONST.ACCOUNT_ID.CONCIERGE); + const isConcierge = ReportUtils.isConciergeChatReport(report); const isCanceledTaskReport = ReportUtils.isCanceledTaskReport(report, parentReportAction); const isPolicyEmployee = useMemo(() => !isEmptyObject(policy), [policy]); const reportDescription = ReportUtils.getReportDescriptionText(report); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 645f508a322d..29b58971b5c5 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -199,7 +199,7 @@ function ReportScreen({ ownerAccountID: reportProp?.ownerAccountID, currency: reportProp?.currency, unheldTotal: reportProp?.unheldTotal, - participantAccountIDs: reportProp?.participantAccountIDs, + participants: reportProp?.participants, isWaitingOnBankAccount: reportProp?.isWaitingOnBankAccount, iouReportID: reportProp?.iouReportID, isOwnPolicyExpenseChat: reportProp?.isOwnPolicyExpenseChat, @@ -240,7 +240,7 @@ function ReportScreen({ reportProp?.ownerAccountID, reportProp?.currency, reportProp?.unheldTotal, - reportProp?.participantAccountIDs, + reportProp?.participants, reportProp?.isWaitingOnBankAccount, reportProp?.iouReportID, reportProp?.isOwnPolicyExpenseChat, diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 5bfa2475ee23..90d8460a133e 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -176,10 +176,12 @@ function ReportActionCompose({ const suggestionsRef = useRef(null); const composerRef = useRef(null); - const reportParticipantIDs = useMemo( - () => report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserPersonalDetails.accountID), - [currentUserPersonalDetails.accountID, report], + () => + Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserPersonalDetails.accountID), + [currentUserPersonalDetails.accountID, report?.participants], ); const shouldShowReportRecipientLocalTime = useMemo( @@ -187,7 +189,7 @@ function ReportActionCompose({ [personalDetails, report, currentUserPersonalDetails.accountID, isComposerFullSize], ); - const includesConcierge = useMemo(() => ReportUtils.chatIncludesConcierge({participantAccountIDs: report?.participantAccountIDs}), [report?.participantAccountIDs]); + const includesConcierge = useMemo(() => ReportUtils.chatIncludesConcierge({participants: report?.participants}), [report?.participants]); const userBlockedFromConcierge = useMemo(() => User.isBlockedFromConcierge(blockedFromConcierge), [blockedFromConcierge]); const isBlockedFromConcierge = useMemo(() => includesConcierge && userBlockedFromConcierge, [includesConcierge, userBlockedFromConcierge]); diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 4a7cc42587aa..b6de128866a3 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -156,8 +156,6 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< ownerAccountID?: number; ownerEmail?: string; participants?: Participants; - participantAccountIDs?: number[]; - visibleChatMemberAccountIDs?: number[]; total?: number; unheldTotal?: number; currency?: string; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 45bdc5fe743a..67525f2e2318 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -19,6 +19,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {IOUMessage, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import type {Participant} from '@src/types/onyx/Report'; import type {ReportActionBase} from '@src/types/onyx/ReportAction'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -36,18 +37,26 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ const CARLOS_EMAIL = 'cmartins@expensifail.com'; const CARLOS_ACCOUNT_ID = 1; +const CARLOS_PARTICIPANT: Participant = {hidden: false, role: 'member'}; const JULES_EMAIL = 'jules@expensifail.com'; const JULES_ACCOUNT_ID = 2; +const JULES_PARTICIPANT: Participant = {hidden: false, role: 'member'}; const RORY_EMAIL = 'rory@expensifail.com'; const RORY_ACCOUNT_ID = 3; +const RORY_PARTICIPANT: Participant = {hidden: false, role: 'admin'}; const VIT_EMAIL = 'vit@expensifail.com'; const VIT_ACCOUNT_ID = 4; +const VIT_PARTICIPANT: Participant = {hidden: false, role: 'member'}; OnyxUpdateManager(); describe('actions/IOU', () => { beforeAll(() => { Onyx.init({ keys: ONYXKEYS, + initialKeyStates: { + [ONYXKEYS.SESSION]: {accountID: RORY_ACCOUNT_ID, email: RORY_EMAIL}, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: {[RORY_ACCOUNT_ID]: {accountID: RORY_ACCOUNT_ID, login: RORY_EMAIL}}, + }, }); }); @@ -96,7 +105,7 @@ describe('actions/IOU', () => { expect(iouReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); // They should be linked together - expect(chatReport?.participantAccountIDs).toEqual([CARLOS_ACCOUNT_ID]); + expect(chatReport?.participants).toEqual({[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT}); expect(chatReport?.iouReportID).toBe(iouReport?.reportID); resolve(); @@ -249,7 +258,7 @@ describe('actions/IOU', () => { let chatReport: OnyxTypes.Report = { reportID: '1234', type: CONST.REPORT.TYPE.CHAT, - participantAccountIDs: [CARLOS_ACCOUNT_ID], + participants: {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT}, }; const createdAction: OnyxTypes.ReportAction = { reportActionID: NumberUtils.rand64(), @@ -417,7 +426,7 @@ describe('actions/IOU', () => { reportID: chatReportID, type: CONST.REPORT.TYPE.CHAT, iouReportID, - participantAccountIDs: [CARLOS_ACCOUNT_ID], + participants: {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT}, }; const createdAction: OnyxTypes.ReportAction = { reportActionID: NumberUtils.rand64(), @@ -641,7 +650,7 @@ describe('actions/IOU', () => { expect(iouReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); // They should be linked together - expect(chatReport?.participantAccountIDs).toEqual([CARLOS_ACCOUNT_ID]); + expect(chatReport?.participants).toEqual({[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT}); expect(chatReport?.iouReportID).toBe(iouReport?.reportID); resolve(); @@ -937,7 +946,7 @@ describe('actions/IOU', () => { let carlosChatReport: OnyxEntry = { reportID: NumberUtils.rand64(), type: CONST.REPORT.TYPE.CHAT, - participantAccountIDs: [CARLOS_ACCOUNT_ID], + participants: {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT}, }; const carlosCreatedAction: OnyxEntry = { reportActionID: NumberUtils.rand64(), @@ -950,7 +959,7 @@ describe('actions/IOU', () => { reportID: NumberUtils.rand64(), type: CONST.REPORT.TYPE.CHAT, iouReportID: julesIOUReportID, - participantAccountIDs: [JULES_ACCOUNT_ID], + participants: {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [JULES_ACCOUNT_ID]: JULES_PARTICIPANT}, }; const julesChatCreatedAction: OnyxEntry = { reportActionID: NumberUtils.rand64(), @@ -1128,7 +1137,9 @@ describe('actions/IOU', () => { // 5. The chat report with Rory + Vit (new) vitChatReport = Object.values(allReports ?? {}).find( - (report) => report?.type === CONST.REPORT.TYPE.CHAT && isEqual(report.participantAccountIDs, [VIT_ACCOUNT_ID]), + (report) => + report?.type === CONST.REPORT.TYPE.CHAT && + isEqual(report.participants, {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [VIT_ACCOUNT_ID]: VIT_PARTICIPANT}), ) ?? null; expect(isEmptyObject(vitChatReport)).toBe(false); expect(vitChatReport?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); @@ -1144,7 +1155,12 @@ describe('actions/IOU', () => { Object.values(allReports ?? {}).find( (report) => report?.type === CONST.REPORT.TYPE.CHAT && - isEqual(report.participantAccountIDs, [CARLOS_ACCOUNT_ID, JULES_ACCOUNT_ID, VIT_ACCOUNT_ID, RORY_ACCOUNT_ID]), + isEqual(report.participants, { + [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT, + [JULES_ACCOUNT_ID]: JULES_PARTICIPANT, + [VIT_ACCOUNT_ID]: VIT_PARTICIPANT, + [RORY_ACCOUNT_ID]: RORY_PARTICIPANT, + }), ) ?? null; expect(isEmptyObject(groupChat)).toBe(false); expect(groupChat?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); @@ -2454,7 +2470,8 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); // Given User logins from the participant accounts - const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(participantAccountIDs); // When Opening a thread report with the given details Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); @@ -2537,7 +2554,8 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); // Given User logins from the participant accounts - const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(participantAccountIDs); // When Opening a thread report with the given details Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); @@ -2627,7 +2645,8 @@ describe('actions/IOU', () => { expect(thread.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); - const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(participantAccountIDs); jest.advanceTimersByTime(10); Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); await waitForBatchedUpdates(); @@ -2723,7 +2742,8 @@ describe('actions/IOU', () => { await waitForBatchedUpdates(); jest.advanceTimersByTime(10); - const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(participantAccountIDs); Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); await waitForBatchedUpdates(); @@ -2958,7 +2978,8 @@ describe('actions/IOU', () => { await waitForBatchedUpdates(); jest.advanceTimersByTime(10); - const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(participantAccountIDs); Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); await waitForBatchedUpdates(); diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 44b69bcb86fe..3e11f0a67938 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -5,11 +5,13 @@ import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import * as Policy from '@src/libs/actions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy as PolicyType, Report, ReportAction, ReportActions} from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/Report'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; const ESH_EMAIL = 'eshgupta1217@gmail.com'; const ESH_ACCOUNT_ID = 1; +const ESH_PARTICIPANT: Participant = {hidden: false, role: 'admin'}; const WORKSPACE_NAME = "Esh's Workspace"; OnyxUpdateManager(); @@ -77,7 +79,7 @@ describe('actions/Policy', () => { expect(workspaceReports.length).toBe(3); workspaceReports.forEach((report) => { expect(report?.pendingFields?.addWorkspaceRoom).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(report?.participantAccountIDs).toEqual([ESH_ACCOUNT_ID]); + expect(report?.participants).toEqual({[ESH_ACCOUNT_ID]: ESH_PARTICIPANT}); switch (report?.chatType) { case CONST.REPORT.CHAT_TYPE.POLICY_ADMINS: { adminReportID = report.reportID; diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 5218c1e696c9..3e2c1b419070 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -228,7 +228,7 @@ function signInAndGetAppWithUnreadChat(): Promise { lastReadTime: reportAction3CreatedDate, lastVisibleActionCreated: reportAction9CreatedDate, lastMessageText: 'Test', - participantAccountIDs: [USER_B_ACCOUNT_ID], + participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, lastActorAccountID: USER_B_ACCOUNT_ID, type: CONST.REPORT.TYPE.CHAT, }); @@ -389,7 +389,7 @@ describe('Unread Indicators', () => { lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()), lastMessageText: 'Comment 1', lastActorAccountID: USER_C_ACCOUNT_ID, - participantAccountIDs: [USER_C_ACCOUNT_ID], + participants: {[USER_C_ACCOUNT_ID]: {hidden: false}}, type: CONST.REPORT.TYPE.CHAT, }, }, diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 44932ee557e6..ae8a93efda96 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -19,13 +19,12 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.015', isPinned: false, reportID: '1', - participantAccountIDs: [2, 1], - visibleChatMemberAccountIDs: [2, 1], participants: { 2: {}, 1: {}, + 5: {}, }, - reportName: 'Iron Man, Mister Fantastic', + reportName: 'Iron Man, Mister Fantastic, Invisible Woman', type: CONST.REPORT.TYPE.CHAT, }, '2': { @@ -33,9 +32,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.016', isPinned: false, reportID: '2', - participantAccountIDs: [3], - visibleChatMemberAccountIDs: [3], participants: { + 2: {}, 3: {}, }, reportName: 'Spider-Man', @@ -48,9 +46,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.170', isPinned: true, reportID: '3', - participantAccountIDs: [1], - visibleChatMemberAccountIDs: [1], participants: { + 2: {}, 1: {}, }, reportName: 'Mister Fantastic', @@ -61,9 +58,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.180', isPinned: false, reportID: '4', - participantAccountIDs: [4], - visibleChatMemberAccountIDs: [4], participants: { + 2: {}, 4: {}, }, reportName: 'Black Panther', @@ -74,9 +70,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.019', isPinned: false, reportID: '5', - participantAccountIDs: [5], - visibleChatMemberAccountIDs: [5], participants: { + 2: {}, 5: {}, }, reportName: 'Invisible Woman', @@ -87,9 +82,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.020', isPinned: false, reportID: '6', - participantAccountIDs: [6], - visibleChatMemberAccountIDs: [6], participants: { + 2: {}, 6: {}, }, reportName: 'Thor', @@ -102,9 +96,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:03.999', isPinned: false, reportID: '7', - participantAccountIDs: [7], - visibleChatMemberAccountIDs: [7], participants: { + 2: {}, 7: {}, }, reportName: 'Captain America', @@ -117,9 +110,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.000', isPinned: false, reportID: '8', - participantAccountIDs: [12], - visibleChatMemberAccountIDs: [12], participants: { + 2: {}, 12: {}, }, reportName: 'Silver Surfer', @@ -132,9 +124,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.998', isPinned: false, reportID: '9', - participantAccountIDs: [8], - visibleChatMemberAccountIDs: [8], participants: { + 2: {}, 8: {}, }, reportName: 'Mister Sinister', @@ -148,8 +139,6 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.001', reportID: '10', isPinned: false, - participantAccountIDs: [2, 7], - visibleChatMemberAccountIDs: [2, 7], participants: { 2: {}, 7: {}, @@ -242,9 +231,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, reportID: '11', - participantAccountIDs: [999], - visibleChatMemberAccountIDs: [999], participants: { + 2: {}, 999: {}, }, reportName: 'Concierge', @@ -259,9 +247,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, reportID: '12', - participantAccountIDs: [1000], - visibleChatMemberAccountIDs: [1000], participants: { + 2: {}, 1000: {}, }, reportName: 'Chronos', @@ -276,9 +263,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, reportID: '13', - participantAccountIDs: [1001], - visibleChatMemberAccountIDs: [1001], participants: { + 2: {}, 1001: {}, }, reportName: 'Receipts', @@ -293,9 +279,8 @@ describe('OptionsListUtils', () => { lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, reportID: '14', - participantAccountIDs: [1, 10, 3], - visibleChatMemberAccountIDs: [1, 10, 3], participants: { + 2: {}, 1: {}, 10: {}, 3: {}, @@ -308,16 +293,15 @@ describe('OptionsListUtils', () => { }, }; - const REPORTS_WITH_CHAT_ROOM = { + const REPORTS_WITH_CHAT_ROOM: OnyxCollection = { ...REPORTS, 15: { lastReadTime: '2021-01-14 11:25:39.301', lastVisibleActionCreated: '2022-11-22 03:26:02.000', isPinned: false, reportID: '15', - participantAccountIDs: [3, 4], - visibleChatMemberAccountIDs: [3, 4], participants: { + 2: {}, 3: {}, 4: {}, }, @@ -436,7 +420,7 @@ describe('OptionsListUtils', () => { // Value with latest lastVisibleActionCreated should be at the top. expect(results.recentReports.length).toBe(2); expect(results.recentReports[0].text).toBe('Mister Fantastic'); - expect(results.recentReports[1].text).toBe('Mister Fantastic'); + expect(results.recentReports[1].text).toBe('Mister Fantastic, Invisible Woman'); return waitForBatchedUpdates() .then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, PERSONAL_DETAILS_WITH_PERIODS)) @@ -2688,7 +2672,7 @@ describe('OptionsListUtils', () => { expect(filteredOptions.recentReports[0].text).toBe('Invisible Woman'); expect(filteredOptions.recentReports[1].text).toBe('Spider-Man'); expect(filteredOptions.recentReports[2].text).toBe('Black Widow'); - expect(filteredOptions.recentReports[3].text).toBe('Mister Fantastic'); + expect(filteredOptions.recentReports[3].text).toBe('Mister Fantastic, Invisible Woman'); expect(filteredOptions.recentReports[4].text).toBe("SHIELD's workspace (archived)"); }); @@ -2761,7 +2745,7 @@ describe('OptionsListUtils', () => { expect(filteredOptions.recentReports.length).toBe(2); expect(filteredOptions.recentReports[0].text).toBe('Mister Fantastic'); - expect(filteredOptions.recentReports[1].text).toBe('Mister Fantastic'); + expect(filteredOptions.recentReports[1].text).toBe('Mister Fantastic, Invisible Woman'); }); }); }); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 1cab8f563b24..d0005c59d39a 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -119,7 +119,7 @@ describe('ReportUtils', () => { expect( ReportUtils.getReportName({ reportID: '', - participantAccountIDs: [currentUserAccountID, 1], + participants: ReportUtils.buildParticipantsFromAccountIDs([currentUserAccountID, 1]), }), ).toBe('Ragnar Lothbrok'); }); @@ -128,7 +128,7 @@ describe('ReportUtils', () => { expect( ReportUtils.getReportName({ reportID: '', - participantAccountIDs: [currentUserAccountID, 2], + participants: ReportUtils.buildParticipantsFromAccountIDs([currentUserAccountID, 2]), }), ).toBe('floki@vikings.net'); }); @@ -137,7 +137,7 @@ describe('ReportUtils', () => { expect( ReportUtils.getReportName({ reportID: '', - participantAccountIDs: [currentUserAccountID, 4], + participants: ReportUtils.buildParticipantsFromAccountIDs([currentUserAccountID, 4]), }), ).toBe('(833) 240-3627'); }); @@ -147,7 +147,7 @@ describe('ReportUtils', () => { expect( ReportUtils.getReportName({ reportID: '', - participantAccountIDs: [currentUserAccountID, 1, 2, 3, 4], + participants: ReportUtils.buildParticipantsFromAccountIDs([currentUserAccountID, 1, 2, 3, 4]), }), ).toBe('Ragnar, floki@vikings.net, Lagertha, (833) 240-3627'); }); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 2a9f9074125b..d35eb61feb35 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -10,6 +10,7 @@ import {CurrentReportIDContextProvider} from '@components/withCurrentReportID'; import {EnvironmentProvider} from '@components/withEnvironment'; import {ReportIDsContextProvider} from '@hooks/useReportIDs'; import DateUtils from '@libs/DateUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import ReportActionItemSingle from '@pages/home/report/ReportActionItemSingle'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import CONST from '@src/CONST'; @@ -128,7 +129,7 @@ function getFakeReport(participantAccountIDs = [1, 2], millisecondsInThePast = 0 reportName: 'Report', lastVisibleActionCreated, lastReadTime: isUnread ? DateUtils.subtractMillisecondsFromDateTime(lastVisibleActionCreated, 1) : lastVisibleActionCreated, - participantAccountIDs, + participants: ReportUtils.buildParticipantsFromAccountIDs(participantAccountIDs), }; }