diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 1a9e6f558198..f084db4f13a5 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -11,6 +11,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import {createGuidesEmailsByReportSelector} from '@selectors/PersonalDetails'; import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {useCurrentReportIDState} from './useCurrentReportID'; @@ -49,7 +50,14 @@ type SidebarOrderedReportsActionsContextValue = { setStickyReportID: (reportID: string) => void; }; -type ReportsToDisplayInLHN = Record; +type ReportsToDisplayInLHN = Record< + string, + OnyxTypes.Report & { + hasErrorsOtherThanFailedReceipt?: boolean; + requiresAttention?: boolean; + isUnreadReport?: boolean; + } +>; const SidebarOrderedReportsStateContext = createContext({ filteredReports: [], @@ -100,6 +108,16 @@ function SidebarOrderedReportsContextProvider({ const [reportNameValuePairs, {sourceValue: reportNameValuePairsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS); const [reportsDrafts, {sourceValue: reportsDraftsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); const [betas] = useOnyx(ONYXKEYS.BETAS); + const computeGuidesEmailsByReport = useMemo(() => createGuidesEmailsByReportSelector(chatReports), [chatReports]); + const guidesEmailsByReportSelector = useCallback( + (personalDetailsList: OnyxEntry) => computeGuidesEmailsByReport(personalDetailsList), + [computeGuidesEmailsByReport], + ); + const [guidesEmailsByReport] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + selector: guidesEmailsByReportSelector, + }); + const guidesEmailsByReportKey = useMemo(() => JSON.stringify(guidesEmailsByReport ?? {}), [guidesEmailsByReport]); + const prevGuidesEmailsByReportKey = usePrevious(guidesEmailsByReportKey); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const reportAttributes = useReportAttributes(); const [currentReportsToDisplay, setCurrentReportsToDisplay] = useState({}); @@ -212,7 +230,14 @@ function SidebarOrderedReportsContextProvider({ // When reportAttributes changes (e.g. on startup hydration) but no report-specific keys were // updated, getUpdatedReports() returns []. Rather than falling through to a full scan of all // reports, recheck only the already-displayed reports with the new reportAttributes. - const effectiveUpdatedReports = updatedReports.length === 0 && hasCachedReports ? Object.keys(currentReportsToDisplay) : updatedReports; + let effectiveUpdatedReports = updatedReports.length === 0 && hasCachedReports ? Object.keys(currentReportsToDisplay) : updatedReports; + + // When guide personal details hydrate after the reports collection, guidesEmailsByReport changes but + // getUpdatedReports() returns no report keys. Re-evaluate all reports so domain rooms previously + // filtered out can appear in the LHN. + if (hasCachedReports && prevGuidesEmailsByReportKey !== undefined && guidesEmailsByReportKey !== prevGuidesEmailsByReportKey) { + effectiveUpdatedReports = Object.keys(chatReports ?? {}); + } const shouldDoIncrementalUpdate = effectiveUpdatedReports.length > 0 && hasCachedReports; let reportsToDisplay = {}; if (shouldDoIncrementalUpdate) { @@ -232,6 +257,7 @@ function SidebarOrderedReportsContextProvider({ currentUserLogin: currentUserLogin ?? '', currentUserAccountID: accountID, conciergeReportID, + guidesEmailsByReport, }); } else { Log.info('[useSidebarOrderedReports] building reportsToDisplay from scratch'); @@ -249,6 +275,7 @@ function SidebarOrderedReportsContextProvider({ reportNameValuePairs, reportAttributes, conciergeReportID, + guidesEmailsByReport, }); } @@ -270,6 +297,9 @@ function SidebarOrderedReportsContextProvider({ currentUserLogin, accountID, conciergeReportID, + guidesEmailsByReport, + guidesEmailsByReportKey, + prevGuidesEmailsByReportKey, ]); // Derive a stable boolean map indicating which reports have drafts. diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 6f2f456831f5..9426e53d3967 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -1467,6 +1467,7 @@ function getReasonForShowingRowInLHN({ currentUserLogin, currentUserAccountID, conciergeReportID, + hasGuidesEmails, }: { report: OnyxEntry; chatReport: OnyxEntry; @@ -1479,6 +1480,7 @@ function getReasonForShowingRowInLHN({ currentUserLogin?: string; currentUserAccountID?: number; conciergeReportID?: string; + hasGuidesEmails: boolean; }): TranslationPaths | null { if (!report) { return null; @@ -1499,6 +1501,7 @@ function getReasonForShowingRowInLHN({ currentUserLogin, currentUserAccountID, conciergeReportID, + hasGuidesEmails, }); if (!([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && hasRBR) { diff --git a/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx index 0eea27b8ee6d..767a2be900d0 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx @@ -54,7 +54,8 @@ function ReportsSplitNavigator({route}: PlatformStackScreenProps, policy: OnyxEntry, config: IsValidReportsConfig, draftComment: string | undefined, chatReport: OnyxEntry): boolean { +function isValidReport( + option: SearchOption, + policy: OnyxEntry, + config: IsValidReportsConfig, + draftComment: string | undefined, + chatReport: OnyxEntry, + hasGuidesEmails: boolean, +): boolean { const { betas = [], includeMultipleParticipantReports = false, @@ -2158,6 +2167,7 @@ function isValidReport(option: SearchOption, policy: OnyxEntry, currentUserLogin, currentUserAccountID, conciergeReportID, + hasGuidesEmails, }); if (!shouldBeInOptionList) { @@ -2543,6 +2553,8 @@ function getValidOptions( }, draftComment, chatReport, + // TODO: Pass personalDetailsList once callers are fully migrated — PR 33 (https://github.com/Expensify/App/issues/66413); hasExpensifyGuidesEmails falls back to allPersonalDetails + isDefaultRoom(report.item) ? hasExpensifyGuidesEmails(Object.keys(report.item?.participants ?? {}).map(Number), undefined) : false, ); }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 67f6087879e8..9754c2008f00 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2409,8 +2409,31 @@ function isHiddenForCurrentUser(reportOrPreference: OnyxEntry | string | * by cross-referencing the accountIDs with personalDetails since guides that are participants * of the user's chats should have their personal details in Onyx. */ -function hasExpensifyGuidesEmails(accountIDs: number[]): boolean { - return accountIDs.some((accountID) => Str.extractEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN); +function hasExpensifyGuidesEmails(accountIDs: number[], personalDetailsList: OnyxEntry): boolean { + // TODO: Remove fallback once all callers pass personalDetailsList (https://github.com/Expensify/App/issues/66413) + const resolvedPersonalDetails = personalDetailsList ?? allPersonalDetails; + return accountIDs.some((accountID) => Str.extractEmailDomain(resolvedPersonalDetails?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN); +} + +/** + * Resolves whether a report has guide participants, preferring pre-computed values when available + * and falling back to module-level personal details while selectors/maps are still loading. + */ +function resolveHasGuidesEmails({ + participantAccountIDs, + hasGuidesEmails, + guidesEmailsByReport, + reportID, +}: { + participantAccountIDs: number[]; + hasGuidesEmails?: boolean; + guidesEmailsByReport?: Record; + reportID?: string; +}): boolean { + if (reportID && guidesEmailsByReport && reportID in guidesEmailsByReport) { + return guidesEmailsByReport[reportID]; + } + return hasGuidesEmails ?? hasExpensifyGuidesEmails(participantAccountIDs, undefined); } function getMostRecentlyVisitedReport(reports: Array>, lastVisitTimes: Record): OnyxEntry { @@ -2429,6 +2452,7 @@ function getMostRecentlyVisitedReport(reports: Array>, lastVis */ function findLastAccessedReport( ignoreDomainRooms: boolean, + guidesEmailsByReport: Record | undefined, openOnAdminRoom = false, excludeReportID?: string, reportNameValuePairs?: OnyxCollection, @@ -2457,7 +2481,16 @@ function findLastAccessedReport( // We allow public announce rooms, admins, and announce rooms through since we bypass the default rooms beta for them. // Check where 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. - if (ignoreDomainRooms && isDomainRoom(report) && !hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number))) { + // When guidesEmailsByReport is undefined or missing this reportID, fall back to hasExpensifyGuidesEmails → allPersonalDetails (https://github.com/Expensify/App/issues/66413) + if ( + ignoreDomainRooms && + isDomainRoom(report) && + !resolveHasGuidesEmails({ + participantAccountIDs: Object.keys(report?.participants ?? {}).map(Number), + guidesEmailsByReport, + reportID: report?.reportID, + }) + ) { return false; } @@ -2567,17 +2600,19 @@ function isClosedReport(report: OnyxInputOrEntry): boolean { /** * Whether the provided report is the admin's room */ -function isJoinRequestInAdminRoom(report: OnyxEntry): boolean { +function isJoinRequestInAdminRoom(report: OnyxEntry, currentUserLogin: string | undefined): boolean { if (!report) { return false; } + // TODO: Remove fallback once all callers pass currentUserLogin (https://github.com/Expensify/App/issues/66413) + const resolvedCurrentUserLogin = currentUserLogin ?? currentUserPersonalDetails?.login; // If this policy isn't owned by Expensify, // Account manager/guide should not have the workspace join request pinned to their LHN, // since they are not a part of the company, and should not action it on their behalf. if (report.policyID) { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 const policy = getPolicy(report.policyID); - if (!isExpensifyTeam(policy?.owner) && isExpensifyTeam(currentUserPersonalDetails?.login)) { + if (!isExpensifyTeam(policy?.owner) && isExpensifyTeam(resolvedCurrentUserLogin)) { return false; } } @@ -4385,7 +4420,7 @@ function getReasonAndReportActionThatRequiresAttention( return null; } - if (isJoinRequestInAdminRoom(optionOrReport)) { + if (isJoinRequestInAdminRoom(optionOrReport, currentUserLogin)) { return { reason: CONST.REQUIRES_ATTENTION_REASONS.HAS_JOIN_REQUEST, reportAction: getActionableJoinRequestPendingReportAction(optionOrReport.reportID), @@ -9002,14 +9037,14 @@ function isIOUOwnedByCurrentUser(report: OnyxEntry, allReportsDict?: Ony * Assuming the passed in report is a default room, lets us know whether we can see it or not, based on permissions and * the various subsets of users we've allowed to use default rooms. */ -function canSeeDefaultRoom(report: OnyxEntry, betas: OnyxEntry, isReportArchived = false): boolean { +function canSeeDefaultRoom(report: OnyxEntry, betas: OnyxEntry, hasGuidesEmails: boolean, isReportArchived = false): boolean { // Include archived rooms if (isArchivedNonExpenseReport(report, isReportArchived)) { return true; } // If the room has an assigned guide, it can be seen. - if (hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number))) { + if (hasGuidesEmails) { return true; } @@ -9022,9 +9057,9 @@ function canSeeDefaultRoom(report: OnyxEntry, betas: OnyxEntry, return Permissions.isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS, betas ?? []); } -function canAccessReport(report: OnyxEntry, betas: OnyxEntry, isReportArchived = false): boolean { +function canAccessReport(report: OnyxEntry, betas: OnyxEntry, hasGuidesEmails: boolean, isReportArchived = false): boolean { // We hide default rooms (it's basically just domain rooms now) from people who aren't on the defaultRooms beta. - if (isDefaultRoom(report) && !canSeeDefaultRoom(report, betas, isReportArchived)) { + if (isDefaultRoom(report) && !canSeeDefaultRoom(report, betas, hasGuidesEmails, isReportArchived)) { return false; } @@ -9477,6 +9512,7 @@ type ShouldReportBeInOptionListParams = { conciergeReportID?: string; /** Pre-computed value from reportAttributes derived value. When provided, skips the expensive requiresAttentionFromCurrentUser recomputation. */ requiresAttention?: boolean; + hasGuidesEmails: boolean; }; function reasonForReportToBeInOptionList({ @@ -9496,6 +9532,7 @@ function reasonForReportToBeInOptionList({ isReportArchived, conciergeReportID, requiresAttention, + hasGuidesEmails, }: ShouldReportBeInOptionListParams): ValueOf | null { const isInDefaultMode = !isInFocusMode; @@ -9547,7 +9584,7 @@ function reasonForReportToBeInOptionList({ return null; } - if (!canAccessReport(report, betas, isReportArchived)) { + if (!canAccessReport(report, betas, hasGuidesEmails, isReportArchived)) { return null; } @@ -10716,8 +10753,8 @@ function isReportParticipant(accountID: number | undefined, report: OnyxEntry, betas: OnyxEntry, isReportArchived = false): boolean { - return (isReportParticipant(deprecatedCurrentUserAccountID, report) || isPublicRoom(report)) && canAccessReport(report, betas, isReportArchived); +function canCurrentUserOpenReport(report: OnyxEntry, betas: OnyxEntry, hasGuidesEmails: boolean, isReportArchived = false): boolean { + return (isReportParticipant(deprecatedCurrentUserAccountID, report) || isPublicRoom(report)) && canAccessReport(report, betas, hasGuidesEmails, isReportArchived); } function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { @@ -13677,6 +13714,8 @@ export { getIntegrationIcon, canBeExported, isExported, + hasExpensifyGuidesEmails, + resolveHasGuidesEmails, hasExportError, hasOnlyNonReimbursableTransactions, getReportLastMessage, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 8f28be104bd6..a04a71195986 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -204,6 +204,7 @@ import { isUnread, isUnreadWithMention, isWorkspaceTaskReport, + resolveHasGuidesEmails, shouldReportBeInOptionList, shouldReportShowSubscript, } from './ReportUtils'; @@ -292,6 +293,7 @@ type ShouldDisplayReportInLHNParams = { currentUserLogin: string; currentUserAccountID: number; conciergeReportID?: string; + hasGuidesEmails: boolean; }; function shouldDisplayReportInLHN({ @@ -309,6 +311,7 @@ function shouldDisplayReportInLHN({ currentUserAccountID, currentUserLogin, conciergeReportID, + hasGuidesEmails, }: ShouldDisplayReportInLHNParams) { if (!report) { return {shouldDisplay: false}; @@ -369,6 +372,7 @@ function shouldDisplayReportInLHN({ currentUserLogin, currentUserAccountID, conciergeReportID, + hasGuidesEmails, }); return {shouldDisplay}; @@ -388,6 +392,7 @@ function getReportsToDisplayInLHN({ reportNameValuePairs, reportAttributes, conciergeReportID, + guidesEmailsByReport, }: { currentReportId: string | undefined; reports: OnyxCollection; @@ -402,6 +407,7 @@ function getReportsToDisplayInLHN({ reportNameValuePairs?: OnyxCollection; reportAttributes?: ReportAttributesDerivedValue['reports']; conciergeReportID?: string; + guidesEmailsByReport?: Record; }) { const isInFocusMode = priorityMode === CONST.PRIORITY_MODE.GSD; const allReportsDictValues = reports ?? {}; @@ -428,6 +434,11 @@ function getReportsToDisplayInLHN({ isReportArchived, reportAttributes, currentUserLogin, + hasGuidesEmails: resolveHasGuidesEmails({ + participantAccountIDs: Object.keys(report.participants ?? {}).map(Number), + guidesEmailsByReport, + reportID: report.reportID, + }), currentUserAccountID, conciergeReportID, }); @@ -459,6 +470,7 @@ type UpdateReportsToDisplayInLHNProps = { currentUserLogin: string; currentUserAccountID: number; conciergeReportID?: string; + guidesEmailsByReport?: Record; }; function updateReportsToDisplayInLHN({ @@ -477,6 +489,7 @@ function updateReportsToDisplayInLHN({ currentUserLogin, currentUserAccountID, conciergeReportID, + guidesEmailsByReport, }: UpdateReportsToDisplayInLHNProps) { // Use a lazy copy to avoid creating a new object reference when no entries actually change. let displayedReportsCopy: ReportsToDisplayInLHN | undefined; @@ -514,6 +527,11 @@ function updateReportsToDisplayInLHN({ isReportArchived, reportAttributes, currentUserLogin, + hasGuidesEmails: resolveHasGuidesEmails({ + participantAccountIDs: Object.keys(report.participants ?? {}).map(Number), + guidesEmailsByReport, + reportID: report.reportID, + }), currentUserAccountID, conciergeReportID, }); @@ -1362,7 +1380,7 @@ function getOptionData({ result.isIOUReportOwner = isIOUOwnedByCurrentUser(result as Report); - if (isJoinRequestInAdminRoom(report)) { + if (isJoinRequestInAdminRoom(report, currentUserLogin)) { result.isUnread = true; } diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index 0cbfcf4b7151..d5494f6c3fbd 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -101,6 +101,8 @@ function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, cur draftComment, currentUserLogin, currentUserAccountID, + // TODO: Pass personalDetailsList once callers are fully migrated — PR 33 (https://github.com/Expensify/App/issues/66413); hasExpensifyGuidesEmails falls back to allPersonalDetails + hasGuidesEmails: ReportUtils.isDefaultRoom(report) ? ReportUtils.hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number), undefined) : false, }); }); } diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index bce700a623cb..a4404a2e045c 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -363,7 +363,8 @@ function openReportFromDeepLink( const report = reportParam ?? reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; // If the report does not exist, navigate to the last accessed report or Concierge chat if (reportID && (!report?.reportID || report.errorFields?.notFound)) { - const lastAccessedReportID = findLastAccessedReport(false, shouldOpenOnAdminRoom(), reportID)?.reportID; + // TODO: Pass guidesEmailsByReport map once callers are fully migrated — PR 33 (https://github.com/Expensify/App/issues/66413); findLastAccessedReport falls back to hasExpensifyGuidesEmails → allPersonalDetails + const lastAccessedReportID = findLastAccessedReport(false, undefined, shouldOpenOnAdminRoom(), reportID)?.reportID; if (lastAccessedReportID) { const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID); Navigation.navigate(lastAccessedReportRoute, {forceReplace: Navigation.getTopmostReportId() === reportID, waitForTransition: true}); diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 195934fa6605..e9e93ede6f41 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4673,7 +4673,8 @@ function navigateToMostRecentReport( introSelected: OnyxEntry, betas: OnyxEntry, ) { - const lastAccessedReportID = findLastAccessedReport(false, false, currentReport?.reportID)?.reportID; + // TODO: Pass guidesEmailsByReport map once callers are fully migrated — PR 30 (https://github.com/Expensify/App/issues/66413); findLastAccessedReport falls back to hasExpensifyGuidesEmails → allPersonalDetails + const lastAccessedReportID = findLastAccessedReport(false, undefined, false, currentReport?.reportID)?.reportID; if (lastAccessedReportID) { // Check if route exists for super wide RHP vs regular full screen report @@ -4714,7 +4715,8 @@ function getSearchThreadLeaveRoute(report: Report, activeRoute: string): Route | } function getMostRecentReportID(currentReport: OnyxEntry, conciergeReportID: string | undefined) { - const lastAccessedReportID = findLastAccessedReport(false, false, currentReport?.reportID)?.reportID; + // TODO: Pass guidesEmailsByReport map once callers are fully migrated — PR 30 (https://github.com/Expensify/App/issues/66413); findLastAccessedReport falls back to hasExpensifyGuidesEmails → allPersonalDetails + const lastAccessedReportID = findLastAccessedReport(false, undefined, false, currentReport?.reportID)?.reportID; return lastAccessedReportID ?? conciergeReportID; } diff --git a/src/libs/navigateAfterOnboarding.ts b/src/libs/navigateAfterOnboarding.ts index 9790863fab8b..e5df7f129f91 100644 --- a/src/libs/navigateAfterOnboarding.ts +++ b/src/libs/navigateAfterOnboarding.ts @@ -49,7 +49,8 @@ function getReportIDAfterOnboarding( return undefined; } - const lastAccessedReport = findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom() && !shouldPreventOpenAdminRoom, undefined, reportNameValuePairs); + // TODO: Pass guidesEmailsByReport map once callers are fully migrated — PR 33 (https://github.com/Expensify/App/issues/66413); findLastAccessedReport falls back to hasExpensifyGuidesEmails → allPersonalDetails + const lastAccessedReport = findLastAccessedReport(!canUseDefaultRooms, undefined, shouldOpenOnAdminRoom() && !shouldPreventOpenAdminRoom, undefined, reportNameValuePairs); const lastAccessedReportID = lastAccessedReport?.reportID; // When the user goes through the onboarding flow, a workspace can be created if the user selects specific options. The user should be taken to the #admins room for that workspace because it is the most natural place for them to start their experience in the app. diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index 451a752cb6cd..d9a8dcd6230b 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -21,7 +21,7 @@ import DebugTabNavigator from '@libs/Navigation/DebugTabNavigator'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; -import {getViolatingReportIDForRBRInLHN} from '@libs/ReportUtils'; +import {getViolatingReportIDForRBRInLHN, resolveHasGuidesEmails} from '@libs/ReportUtils'; import DebugDetails from '@pages/Debug/DebugDetails'; import DebugJSON from '@pages/Debug/DebugJSON'; @@ -38,7 +38,7 @@ import type {ReportAttributesDerivedValue} from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; import {hasSeenTourSelector} from '@selectors/Onboarding'; -import {conciergePersonalDetailSelector, personalDetailsSelector} from '@selectors/PersonalDetails'; +import {conciergePersonalDetailSelector, hasExpensifyGuidesEmailsSelector, personalDetailsSelector} from '@selectors/PersonalDetails'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; @@ -84,14 +84,22 @@ function DebugReportPage({ const [betas] = useOnyx(ONYXKEYS.BETAS); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { + selector: hasSeenTourSelector, + }); const currentUserPersonalDetail = useCurrentUserPersonalDetails(); const {accountID: currentUserAccountID, login: currentUserLogin} = currentUserPersonalDetail; - const [conciergePersonalDetail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: conciergePersonalDetailSelector}); + const [conciergePersonalDetail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + selector: conciergePersonalDetailSelector, + }); const reportOwnerSelector = useMemo(() => personalDetailsSelector(report?.ownerAccountID), [report?.ownerAccountID]); const [reportOwnerPersonalDetail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: reportOwnerSelector}, [reportOwnerSelector]); const transactionID = DebugUtils.getTransactionID(report, reportActions); const isReportArchived = useReportIsArchived(reportID); + const participantAccountIDs = useMemo(() => Object.keys(report?.participants ?? {}).map(Number), [report?.participants]); + const guidesEmailsSelector = useMemo(() => hasExpensifyGuidesEmailsSelector(participantAccountIDs), [participantAccountIDs]); + const [hasGuidesEmails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: guidesEmailsSelector}, [guidesEmailsSelector]); + const resolvedHasGuidesEmails = useMemo(() => resolveHasGuidesEmails({participantAccountIDs, hasGuidesEmails}), [participantAccountIDs, hasGuidesEmails]); const metadata = useMemo(() => { if (!report) { @@ -128,6 +136,7 @@ function DebugReportPage({ currentUserLogin: currentUserLogin ?? '', currentUserAccountID, conciergeReportID, + hasGuidesEmails: resolvedHasGuidesEmails, }); return [ @@ -189,6 +198,7 @@ function DebugReportPage({ draftComment, translate, conciergeReportID, + resolvedHasGuidesEmails, ]); const icons = useMemoizedLazyExpensifyIcons(['Eye']); diff --git a/src/pages/inbox/ReportRouteParamHandler.tsx b/src/pages/inbox/ReportRouteParamHandler.tsx index c422a711c3b3..ba7a2fe62af3 100644 --- a/src/pages/inbox/ReportRouteParamHandler.tsx +++ b/src/pages/inbox/ReportRouteParamHandler.tsx @@ -40,8 +40,10 @@ function ReportRouteParamHandler() { return; } + // TODO: Pass guidesEmailsByReport map once callers are fully migrated — PR 33 (https://github.com/Expensify/App/issues/66413); findLastAccessedReport falls back to hasExpensifyGuidesEmails → allPersonalDetails const lastAccessedReportID = findLastAccessedReport( !isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), + undefined, 'openOnAdminRoom' in route.params && !!route.params.openOnAdminRoom, undefined, reportNameValuePairs, diff --git a/src/pages/inbox/report/AncestorReportActionItem.tsx b/src/pages/inbox/report/AncestorReportActionItem.tsx index 97c40ea31924..7098fc9ce58c 100644 --- a/src/pages/inbox/report/AncestorReportActionItem.tsx +++ b/src/pages/inbox/report/AncestorReportActionItem.tsx @@ -8,7 +8,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isTripPreview} from '@libs/ReportActionsUtils'; -import {canCurrentUserOpenReport, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isArchivedReport, navigateToLinkedReportAction} from '@libs/ReportUtils'; +import { + canCurrentUserOpenReport, + canUserPerformWriteAction as canUserPerformWriteActionReportUtils, + isArchivedReport, + navigateToLinkedReportAction, + resolveHasGuidesEmails, +} from '@libs/ReportUtils'; import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report'; @@ -19,8 +25,8 @@ import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {personalDetailsSelector} from '@selectors/PersonalDetails'; -import React from 'react'; +import {hasExpensifyGuidesEmailsSelector, personalDetailsSelector} from '@selectors/PersonalDetails'; +import React, {useMemo} from 'react'; import ReportActionItem from './ReportActionItem'; import ThreadDivider from './ThreadDivider'; @@ -95,12 +101,18 @@ function AncestorReportActionItem({ }: AncestorReportActionItemProps) { const styles = useThemeStyles(); const currentUserPersonalDetail = useCurrentUserPersonalDetails(); - const [reportOwnerPersonalDetail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsSelector(report?.ownerAccountID)}); + const [reportOwnerPersonalDetail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + selector: personalDetailsSelector(report?.ownerAccountID), + }); + const participantAccountIDs = useMemo(() => Object.keys(report?.participants ?? {}).map(Number), [report?.participants]); + const guidesEmailsSelector = useMemo(() => hasExpensifyGuidesEmailsSelector(participantAccountIDs), [participantAccountIDs]); + const [hasGuidesEmails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: guidesEmailsSelector}, [guidesEmailsSelector]); + const resolvedHasGuidesEmails = useMemo(() => resolveHasGuidesEmails({participantAccountIDs, hasGuidesEmails}), [participantAccountIDs, hasGuidesEmails]); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.chatReportID)}`, {selector: getStableReportSelector}); const shouldDisplayThreadDivider = !isTripPreview(reportAction); const isAncestorReportArchived = isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`]); - const canOpenAncestorReport = canCurrentUserOpenReport(report, allBetas, isAncestorReportArchived); + const canOpenAncestorReport = canCurrentUserOpenReport(report, allBetas, resolvedHasGuidesEmails, isAncestorReportArchived); const {isOffline} = useNetwork(); const {isInNarrowPaneModal} = useResponsiveLayout(); diff --git a/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx b/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx index ae7731286548..b181f870b786 100644 --- a/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx @@ -10,7 +10,7 @@ import getComponentDisplayName from '@libs/getComponentDisplayName'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {FlagCommentNavigatorParamList, SplitDetailsNavigatorParamList} from '@libs/Navigation/types'; -import {canAccessReport} from '@libs/ReportUtils'; +import {canAccessReport, resolveHasGuidesEmails} from '@libs/ReportUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -23,6 +23,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {ComponentType} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; +import {hasExpensifyGuidesEmailsSelector} from '@selectors/PersonalDetails'; import React, {useEffect, useMemo} from 'react'; type WithReportAndReportActionOrNotFoundProps = PlatformStackScreenProps< @@ -52,6 +53,10 @@ export default function Object.keys(report?.participants ?? {}).map(Number), [report?.participants]); + const guidesEmailsSelector = useMemo(() => hasExpensifyGuidesEmailsSelector(participantAccountIDs), [participantAccountIDs]); + const [hasGuidesEmails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: guidesEmailsSelector}, [guidesEmailsSelector]); + const resolvedHasGuidesEmails = useMemo(() => resolveHasGuidesEmails({participantAccountIDs, hasGuidesEmails}), [participantAccountIDs, hasGuidesEmails]); const parentReportAction = useParentReportAction(report); const linkedReportAction = useMemo(() => { @@ -73,7 +78,11 @@ export default function Object.keys(report?.participants ?? {}).map(Number), [report?.participants]); + const guidesEmailsSelector = useCallback( + (personalDetailsList: OnyxEntry) => hasExpensifyGuidesEmailsSelector(participantAccountIDs)(personalDetailsList), + [participantAccountIDs], + ); + const [hasGuidesEmails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: guidesEmailsSelector}, [guidesEmailsSelector]); + const resolvedHasGuidesEmails = useMemo(() => resolveHasGuidesEmails({participantAccountIDs, hasGuidesEmails}), [participantAccountIDs, hasGuidesEmails]); const isFocused = useIsFocused(); const contentShown = React.useRef(false); const isReportIdInRoute = !!props.route.params.reportID?.length; @@ -102,13 +111,17 @@ export default function (shouldRequireReportID = true): (personalDetailsList: OnyxEntry) => getPersonalDetailsByID(accountID, personalDetailsList); @@ -46,7 +48,12 @@ const createDisplayDetailsByAccountIDsSelector = if (!detail) { continue; } - result[accountID] = {accountID: detail.accountID, displayName: detail.displayName, login: detail.login, avatar: detail.avatar}; + result[accountID] = { + accountID: detail.accountID, + displayName: detail.displayName, + login: detail.login, + avatar: detail.avatar, + }; } return result; }; @@ -80,6 +87,61 @@ const isOptimisticPersonalDetailSelector = return isPersonalDetailOptimistic(personalDetailsList[accountID]); }; +const hasExpensifyGuidesEmailsSelector = + (participantAccountIDs: number[]) => + (personalDetailsList: OnyxEntry): boolean => + participantAccountIDs.some((accountID) => Str.extractEmailDomain(personalDetailsList?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN); + +type ReportWithParticipantIDs = { + reportID: string; + participantIDs: number[]; +}; + +/** + * Creates a selector that returns a per-report map of whether participants include Expensify Guides emails. + * The returned selector only produces a new map when participant logins change, so subscribers + * are not notified on unrelated personal-details updates. + */ +const createGuidesEmailsByReportSelector = (chatReports: OnyxCollection) => { + const reportsWithParticipants: ReportWithParticipantIDs[] = []; + const allParticipantAccountIDs: number[] = []; + const accountIDSet = new Set(); + + for (const report of Object.values(chatReports ?? {})) { + if (!report) { + continue; + } + const participantIDs = Object.keys(report.participants ?? {}).map(Number); + reportsWithParticipants.push({reportID: report.reportID, participantIDs}); + for (const accountID of participantIDs) { + if (accountIDSet.has(accountID)) { + continue; + } + accountIDSet.add(accountID); + allParticipantAccountIDs.push(accountID); + } + } + + let cachedParticipantLoginsKey = ''; + let cachedGuidesEmailsByReport: Record = {}; + + return (personalDetailsList: OnyxEntry): Record => { + const participantLoginsKey = allParticipantAccountIDs.map((accountID) => personalDetailsList?.[accountID]?.login ?? '').join('\0'); + + if (participantLoginsKey === cachedParticipantLoginsKey) { + return cachedGuidesEmailsByReport; + } + + cachedParticipantLoginsKey = participantLoginsKey; + const map: Record = {}; + for (const {reportID, participantIDs} of reportsWithParticipants) { + map[reportID] = participantIDs.some((accountID) => Str.extractEmailDomain(personalDetailsList?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN); + } + cachedGuidesEmailsByReport = map; + return map; + }; +}; + export { personalDetailsSelector, multiPersonalDetailsSelector, @@ -92,4 +154,6 @@ export { accountIDToLoginSelector, isOptimisticPersonalDetailSelector, createDisplayDetailsByAccountIDsSelector, + hasExpensifyGuidesEmailsSelector, + createGuidesEmailsByReportSelector, }; diff --git a/tests/actions/IOUTest/TrackExpenseTest.ts b/tests/actions/IOUTest/TrackExpenseTest.ts index fbb7c58dfb5f..87ec33dc8ceb 100644 --- a/tests/actions/IOUTest/TrackExpenseTest.ts +++ b/tests/actions/IOUTest/TrackExpenseTest.ts @@ -178,6 +178,7 @@ describe('actions/IOU/TrackExpense', () => { currentUserAccountID: RORY_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, }); await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${distanceTransaction.transactionID}`, distanceTransaction); @@ -231,6 +232,7 @@ describe('actions/IOU/TrackExpense', () => { currentUserAccountID: RORY_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, }); expect(hiddenReportsToDisplay).not.toHaveProperty(selfDMReportKey); diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index 3cc41fffc789..ec9dbd25647b 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -95,7 +95,7 @@ describe('ReportUtils', () => { }); await waitForBatchedUpdates(); - await measureFunction(() => findLastAccessedReport(ignoreDomainRooms, openOnAdminRoom)); + await measureFunction(() => findLastAccessedReport(ignoreDomainRooms, undefined, openOnAdminRoom)); }); test('[ReportUtils] canDeleteReportAction on 1k reports and policies', async () => { @@ -193,6 +193,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: undefined, isReportArchived: false, + hasGuidesEmails: false, }), ); }); diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 118cbb2ef732..4568359b9d2e 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -111,6 +111,7 @@ describe('SidebarUtils', () => { currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: 1, reportNameValuePairs: {}, + guidesEmailsByReport: {}, }), ); }); @@ -130,6 +131,7 @@ describe('SidebarUtils', () => { currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: 1, reportNameValuePairs: {}, + guidesEmailsByReport: {}, }), ); }); diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index a411494a8706..0710d9b119aa 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -752,6 +752,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBeNull(); }); @@ -763,6 +764,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: 'Hello world!', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasDraftComment'); }); @@ -777,6 +779,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasGBR'); }); @@ -790,6 +793,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.pinnedByUser'); }); @@ -807,6 +811,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasAddWorkspaceRoomErrors'); }); @@ -832,6 +837,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.isUnread'); }); @@ -850,6 +856,7 @@ describe('DebugUtils', () => { isReportArchived: isReportArchived.current, doesReportHaveViolations: false, draftComment: '', + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.isArchived'); }); @@ -863,6 +870,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.isSelfDM'); }); @@ -873,6 +881,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.isFocused'); }); @@ -932,6 +941,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); @@ -991,6 +1001,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); @@ -1002,6 +1013,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); diff --git a/tests/unit/PersonalDetailsSelectorTest.ts b/tests/unit/PersonalDetailsSelectorTest.ts index 0525730d4ac3..8daa840eca53 100644 --- a/tests/unit/PersonalDetailsSelectorTest.ts +++ b/tests/unit/PersonalDetailsSelectorTest.ts @@ -204,7 +204,9 @@ describe('PersonalDetailsSelector', () => { pronouns: 'they/them', timezone: {selected: 'UTC'}, } as unknown as PersonalDetails; - const listWithAvatar = {[accountID]: fullDetails} as unknown as PersonalDetailsList; + const listWithAvatar = { + [accountID]: fullDetails, + } as unknown as PersonalDetailsList; it('should return only the display detail fields for present account IDs', () => { const result = createDisplayDetailsByAccountIDsSelector([accountID])(listWithAvatar); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 868848e8c17e..caad679b37e8 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -132,6 +132,8 @@ import { hasActionWithErrorsForTransaction, hasEmptyReportsForPolicy, hasExportError, + hasExpensifyGuidesEmails, + resolveHasGuidesEmails, hasReceiptError, hasSmartscanError, hasVisibleReportFieldViolations, @@ -144,6 +146,7 @@ import { isConciergeChatReport, isDeprecatedGroupDM, isHarvestCreatedExpenseReport, + isJoinRequestInAdminRoom, isMoneyRequestReportEligibleForMerge, isPayer, isReportOutstanding, @@ -6220,6 +6223,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -6244,6 +6248,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -6273,6 +6278,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6326,6 +6332,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -6349,6 +6356,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -6372,6 +6380,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: 'fake draft', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -6395,6 +6404,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -6431,6 +6441,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -6462,6 +6473,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: isReportArchived.current, draftComment: '', + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -6493,6 +6505,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: isReportArchived.current, draftComment: '', + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6518,6 +6531,7 @@ describe('ReportUtils', () => { includeSelfDM, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -6545,6 +6559,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6565,6 +6580,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6588,6 +6604,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6631,6 +6648,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6651,6 +6669,7 @@ describe('ReportUtils', () => { excludeEmptyChats: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6679,6 +6698,7 @@ describe('ReportUtils', () => { excludeEmptyChats: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.DEFAULT); }); @@ -6706,6 +6726,7 @@ describe('ReportUtils', () => { draftComment: '', isReportArchived: undefined, conciergeReportID, + hasGuidesEmails: false, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.DEFAULT); }); @@ -6732,6 +6753,7 @@ describe('ReportUtils', () => { draftComment: '', isReportArchived: undefined, conciergeReportID: 'some-other-report-id', + hasGuidesEmails: false, }), ).toBeNull(); }); @@ -6754,6 +6776,7 @@ describe('ReportUtils', () => { includeDomainEmail: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6800,6 +6823,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6820,6 +6844,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6854,6 +6879,7 @@ describe('ReportUtils', () => { draftComment: '', betas: undefined, isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6885,6 +6911,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.IS_UNREAD); }); @@ -6914,6 +6941,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -6936,6 +6964,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS); }); @@ -6980,6 +7009,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -7031,6 +7061,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.PINNED_BY_USER); }); @@ -7053,6 +7084,7 @@ describe('ReportUtils', () => { includeSelfDM: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeNull(); }); @@ -7074,6 +7106,7 @@ describe('ReportUtils', () => { excludeEmptyChats: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeTruthy(); }); @@ -7095,6 +7128,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -7116,6 +7150,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -7137,6 +7172,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -7153,6 +7189,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }), ).toBeFalsy(); }); @@ -7209,6 +7246,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + + hasGuidesEmails: false, }); expect(reason).not.toBe(CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS); @@ -8017,7 +8056,7 @@ describe('ReportUtils', () => { const reportNameValuePairsCollection = { [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${archivedReport.reportID}`]: {private_isArchived: DateUtils.getDBTime()}, }; - const result = findLastAccessedReport(false, false, undefined, reportNameValuePairsCollection); + const result = findLastAccessedReport(false, undefined, false, undefined, reportNameValuePairsCollection); // Even though the archived report has a more recent lastVisitTime, // the function should filter it out and return the normal report @@ -8070,7 +8109,7 @@ describe('ReportUtils', () => { }); it('findLastAccessedReport should return owned report if no reports was accessed before', () => { - const result = findLastAccessedReport(false); + const result = findLastAccessedReport(false, undefined); // Even though the archived report has a more recent lastVisitTime, // the function should filter it out and return the normal report @@ -11532,7 +11571,7 @@ describe('ReportUtils', () => { type: CONST.REPORT.TYPE.CHAT, participants: buildParticipantsFromAccountIDs([currentUserAccountID, 1]), }; - expect(canSeeDefaultRoom(report, betas, true)).toBe(true); + expect(canSeeDefaultRoom(report, betas, false, true)).toBe(true); }); it('should return true if the room has an assigned guide', () => { const betas = [CONST.BETAS.DEFAULT_ROOMS]; @@ -11541,15 +11580,124 @@ describe('ReportUtils', () => { participants: buildParticipantsFromAccountIDs([currentUserAccountID, 8]), }; Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails).then(() => { - expect(canSeeDefaultRoom(report, betas, false)).toBe(true); + expect(canSeeDefaultRoom(report, betas, true, false)).toBe(true); }); }); it('should return true if the report is admin room', () => { const betas = [CONST.BETAS.DEFAULT_ROOMS]; const report: Report = createRandomReport(40002, CONST.REPORT.CHAT_TYPE.POLICY_ADMINS); Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails).then(() => { - expect(canSeeDefaultRoom(report, betas, false)).toBe(true); + expect(canSeeDefaultRoom(report, betas, false, false)).toBe(true); + }); + }); + }); + + describe('hasExpensifyGuidesEmails', () => { + it('should use the passed personalDetailsList when provided', () => { + expect(hasExpensifyGuidesEmails([8], personalDetails)).toBe(true); + expect(hasExpensifyGuidesEmails([1], personalDetails)).toBe(false); + }); + + it('should fall back to module-level allPersonalDetails when personalDetailsList is undefined', async () => { + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails); + await waitForBatchedUpdates(); + expect(hasExpensifyGuidesEmails([8], undefined)).toBe(true); + await Onyx.clear(); + }); + }); + + describe('resolveHasGuidesEmails', () => { + it('should prefer the pre-computed hasGuidesEmails value when provided', () => { + expect(resolveHasGuidesEmails({participantAccountIDs: [8], hasGuidesEmails: true})).toBe(true); + expect(resolveHasGuidesEmails({participantAccountIDs: [8], hasGuidesEmails: false})).toBe(false); + }); + + it('should prefer the guidesEmailsByReport map entry when available', () => { + expect( + resolveHasGuidesEmails({ + participantAccountIDs: [8], + guidesEmailsByReport: {'123': true}, + reportID: '123', + }), + ).toBe(true); + expect( + resolveHasGuidesEmails({ + participantAccountIDs: [8], + guidesEmailsByReport: {'123': false}, + reportID: '123', + }), + ).toBe(false); + }); + + it('should fall back to module-level allPersonalDetails when selector value and map entry are unavailable', async () => { + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails); + await waitForBatchedUpdates(); + expect(resolveHasGuidesEmails({participantAccountIDs: [8]})).toBe(true); + await Onyx.clear(); + }); + }); + + describe('isJoinRequestInAdminRoom', () => { + it('should use the passed currentUserLogin instead of the module-level fallback', async () => { + const policyID = '50500'; + const adminReport = {...createAdminRoom(50500), policyID}; + const joinRequestReportAction = createMock({ + ...createRandomReportAction(50500), + originalMessage: { + // @ts-expect-error pending join requests use an empty choice until the admin responds + choice: '', + policyID, + }, + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST, + }); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + ...createRandomPolicy(50500, CONST.POLICY.TYPE.TEAM), + owner: 'owner@test.com', + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${adminReport.reportID}`, adminReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminReport.reportID}`, { + [joinRequestReportAction.reportActionID]: joinRequestReportAction, + }); + await waitForBatchedUpdates(); + + expect(isJoinRequestInAdminRoom(adminReport, `guide@${CONST.EMAIL.GUIDES_DOMAIN}`)).toBe(false); + expect(isJoinRequestInAdminRoom(adminReport, 'owner@test.com')).toBe(true); + await Onyx.clear(); + }); + + it('should fall back to module-level currentUserPersonalDetails when currentUserLogin is undefined', async () => { + const policyID = '50501'; + const adminReport = {...createAdminRoom(50501), policyID}; + const joinRequestReportAction = createMock({ + ...createRandomReportAction(50501), + originalMessage: { + // @ts-expect-error pending join requests use an empty choice until the admin responds + choice: '', + policyID, + }, + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST, }); + + await Onyx.merge(ONYXKEYS.SESSION, {email: currentUserEmail, accountID: currentUserAccountID}); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [currentUserAccountID]: { + accountID: currentUserAccountID, + login: `guide@${CONST.EMAIL.GUIDES_DOMAIN}`, + }, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + ...createRandomPolicy(50501, CONST.POLICY.TYPE.TEAM), + owner: 'owner@test.com', + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${adminReport.reportID}`, adminReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminReport.reportID}`, { + [joinRequestReportAction.reportActionID]: joinRequestReportAction, + }); + await waitForBatchedUpdates(); + + expect(isJoinRequestInAdminRoom(adminReport, undefined)).toBe(false); + await Onyx.clear(); }); }); @@ -13462,6 +13610,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + + hasGuidesEmails: false, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -13520,6 +13670,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + + hasGuidesEmails: false, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.DEFAULT); @@ -13627,6 +13779,8 @@ describe('ReportUtils', () => { draftComment: undefined, betas: undefined, isReportArchived: undefined, + + hasGuidesEmails: false, }); expect(reasonForOptionList).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -13670,6 +13824,8 @@ describe('ReportUtils', () => { draftComment: undefined, betas: undefined, isReportArchived: undefined, + + hasGuidesEmails: false, }); expect(reasonForOptionList).toBe(null); @@ -14196,6 +14352,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + + hasGuidesEmails: false, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -14379,6 +14537,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + + hasGuidesEmails: false, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.DEFAULT); @@ -14564,6 +14724,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: archiveState.current, + + hasGuidesEmails: false, }); expect(reasonBeforeDelete).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -14586,6 +14748,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: archiveState.current, + + hasGuidesEmails: false, }); expect(reasonAfterDelete).toBe(CONST.REPORT_IN_LHN_REASONS.IS_ARCHIVED); @@ -14687,6 +14851,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: isReportArchivedBefore.current, + + hasGuidesEmails: false, }); expect(reasonBeforeRemoval).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -14727,6 +14893,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: isReportArchivedAfter.current, + + hasGuidesEmails: false, }); expect(reasonAfterRemoval).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -15381,6 +15549,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + + hasGuidesEmails: false, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -15448,6 +15618,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + + hasGuidesEmails: false, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -15519,6 +15691,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + + hasGuidesEmails: false, }); expect(reason).not.toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -15589,6 +15763,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + + hasGuidesEmails: false, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -17653,6 +17829,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + + hasGuidesEmails: false, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -17722,6 +17900,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + + hasGuidesEmails: false, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index f18474089139..878dd0238667 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -908,6 +908,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, }); expect(result).toStrictEqual({shouldDisplay: true, hasErrorsOtherThanFailedReceipt: true}); @@ -1019,6 +1020,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, }); expect(result).toStrictEqual({shouldDisplay: true, hasErrorsOtherThanFailedReceipt: true}); @@ -1037,6 +1039,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, }); expect(result).toStrictEqual({shouldDisplay: false}); @@ -1058,6 +1061,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, }); expect(result).toStrictEqual({shouldDisplay: false}); @@ -1081,6 +1085,7 @@ describe('SidebarUtils', () => { isOffline: true, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, }); expect(result).toBeDefined(); @@ -1105,6 +1110,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, }); expect(result).toBeDefined(); @@ -4111,6 +4117,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, }); expect(result).toBe(displayedReports); @@ -4135,6 +4142,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, }); expect(result).not.toBe(displayedReports); @@ -4157,6 +4165,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, }); expect(result).toEqual({}); @@ -4176,6 +4185,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, }); expect(result).toEqual({}); @@ -4199,6 +4209,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, }); expect(result).toEqual({}); @@ -4226,6 +4237,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, }); // The function should run without errors with isOffline=false @@ -4254,6 +4266,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, }); // The function should run without errors with isOffline=true @@ -4282,6 +4295,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, }); expect(result[`${ONYXKEYS.COLLECTION.REPORT}1`]).toBeUndefined(); @@ -4320,6 +4334,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, }); // Should not throw and should return a valid result @@ -4349,6 +4364,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, }); // No changes expected since the updated key doesn't exist in reports @@ -4376,6 +4392,7 @@ describe('SidebarUtils', () => { isOffline: true, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, }); // No changes expected since the updated key doesn't exist in reports @@ -4401,6 +4418,7 @@ describe('SidebarUtils', () => { isOffline: true, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, }); expect(result).not.toBe(displayedReports); @@ -4437,6 +4455,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, }); // Should not throw and should return a valid result diff --git a/tests/unit/navigateAfterOnboardingTest.ts b/tests/unit/navigateAfterOnboardingTest.ts index 312199815a36..14665fef36ac 100644 --- a/tests/unit/navigateAfterOnboardingTest.ts +++ b/tests/unit/navigateAfterOnboardingTest.ts @@ -133,7 +133,7 @@ describe('navigateAfterOnboarding', () => { navigateAfterOnboarding(true, true, '', reportNameValuePairs, ONBOARDING_POLICY_ID, ONBOARDING_ADMINS_CHAT_REPORT_ID); - expect(mockFindLastAccessedReport).toHaveBeenCalledWith(false, false, undefined, reportNameValuePairs); + expect(mockFindLastAccessedReport).toHaveBeenCalledWith(false, undefined, false, undefined, reportNameValuePairs); }); it('should navigate to Concierge room if user uses a test email', () => { diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index ff3126861362..6917cbe5aa8f 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -125,8 +125,14 @@ describe('useSidebarOrderedReports', () => { it('should prevent unnecessary re-renders when reports have same content but different references', async () => { // Given reports with same content but different object references const reportsContent = { - report1: {reportName: 'Chat 1', lastVisibleActionCreated: '2024-01-01 10:00:00'}, - report2: {reportName: 'Chat 2', lastVisibleActionCreated: '2024-01-01 11:00:00'}, + report1: { + reportName: 'Chat 1', + lastVisibleActionCreated: '2024-01-01 10:00:00', + }, + report2: { + reportName: 'Chat 2', + lastVisibleActionCreated: '2024-01-01 11:00:00', + }, }; // When the initial reports are set @@ -318,4 +324,113 @@ describe('useSidebarOrderedReports', () => { // Then sortReportsToDisplayInLHN should be called when priority mode changes expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalled(); }); + + it('should recompute all reports when personal details hydrate and guide emails become available', async () => { + const guideAccountID = '8'; + const displayedReports = createMockReports({ + report1: {reportName: 'Chat A'}, + }); + const domainRoomReport = { + reportID: '2', + reportName: 'Domain Room', + lastVisibleActionCreated: '2024-01-01 10:00:00', + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.DOMAIN_ALL, + participants: { + [guideAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + }, + } as Report; + + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(displayedReports); + mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation(({displayedReports: reports}) => reports); + + await act(async () => { + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}1`, displayedReports['1']); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}2`, domainRoomReport); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {}); + }); + + renderHook(() => useSidebarOrderedReports(), { + wrapper: TestWrapper, + }); + + await waitForBatchedUpdatesWithAct(); + + mockSidebarUtils.updateReportsToDisplayInLHN.mockClear(); + + await act(async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [guideAccountID]: { + accountID: Number(guideAccountID), + login: `guide@${CONST.EMAIL.GUIDES_DOMAIN}`, + }, + }); + }); + + await waitForBatchedUpdatesWithAct(); + + expect(mockSidebarUtils.updateReportsToDisplayInLHN).toHaveBeenCalledWith( + expect.objectContaining({ + updatedReportsKeys: expect.arrayContaining([`${ONYXKEYS.COLLECTION.REPORT}1`, `${ONYXKEYS.COLLECTION.REPORT}2`]), + }), + ); + }); + + it('should not recompute all reports when unrelated personal details change', async () => { + const participantAccountID = '8'; + const displayedReports = createMockReports({ + report1: {reportName: 'Chat A'}, + }); + + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(displayedReports); + mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation(({displayedReports: reports}) => reports); + + await act(async () => { + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}1`, { + ...displayedReports['1'], + participants: { + [participantAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + }, + }); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [participantAccountID]: { + accountID: Number(participantAccountID), + login: 'user@expensify.com', + }, + }); + }); + + renderHook(() => useSidebarOrderedReports(), { + wrapper: TestWrapper, + }); + + await waitForBatchedUpdatesWithAct(); + + mockSidebarUtils.updateReportsToDisplayInLHN.mockClear(); + + const unrelatedAccountID = '99'; + + await act(async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [unrelatedAccountID]: { + accountID: Number(unrelatedAccountID), + login: 'other@expensify.com', + }, + }); + }); + + await waitForBatchedUpdatesWithAct(); + + const updateCalls = mockSidebarUtils.updateReportsToDisplayInLHN.mock.calls; + const fullRecomputeCall = updateCalls.find((call) => { + const updatedReportsKeys = call[0]?.updatedReportsKeys ?? []; + return updatedReportsKeys.length > 1; + }); + + expect(fullRecomputeCall).toBeUndefined(); + }); });