diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index fc30ea183e5b..dbe56db9ee36 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -10,6 +10,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {ValueOf} from 'type-fest'; +import {createGuidesEmailsByReportSelector} from '@selectors/PersonalDetails'; import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import useCollectionDelta from './useCollectionDelta'; @@ -44,7 +45,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: [], @@ -95,6 +103,12 @@ function SidebarOrderedReportsContextProvider({ const [reportsDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); const reportsDraftsUpdates = useCollectionDelta(reportsDrafts); const [betas] = useOnyx(ONYXKEYS.BETAS); + const guidesEmailsByReportSelector = useMemo(() => createGuidesEmailsByReportSelector(chatReports), [chatReports]); + 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({}); @@ -202,7 +216,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) { @@ -222,6 +243,7 @@ function SidebarOrderedReportsContextProvider({ currentUserLogin: currentUserLogin ?? '', currentUserAccountID: accountID, conciergeReportID, + guidesEmailsByReport, }); } else { Log.info('[useSidebarOrderedReports] building reportsToDisplay from scratch'); @@ -239,6 +261,7 @@ function SidebarOrderedReportsContextProvider({ reportNameValuePairs, reportAttributes, conciergeReportID, + guidesEmailsByReport, }); } @@ -260,6 +283,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 5d715ffccfec..268c0ff383da 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -1465,6 +1465,7 @@ function getReasonForShowingRowInLHN({ currentUserLogin, currentUserAccountID, conciergeReportID, + hasGuidesEmails, }: { report: OnyxEntry; chatReport: OnyxEntry; @@ -1476,6 +1477,7 @@ function getReasonForShowingRowInLHN({ draftComment: string | undefined; currentUserLogin?: string; currentUserAccountID?: number; + hasGuidesEmails: boolean; conciergeReportID: string | undefined; }): TranslationPaths | null { if (!report) { @@ -1497,6 +1499,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 503588f7fe2c..e79b1d214306 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx @@ -58,7 +58,8 @@ function ReportsSplitNavigator({navigation, 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, @@ -2361,6 +2370,7 @@ function isValidReport(option: SearchOption, policy: OnyxEntry, currentUserLogin, currentUserAccountID, conciergeReportID, + hasGuidesEmails, }); if (!shouldBeInOptionList) { @@ -2748,6 +2758,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 1d962fb411f3..2dd2b46079d5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2408,8 +2408,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 { @@ -2428,6 +2451,7 @@ function getMostRecentlyVisitedReport(reports: Array>, lastVis */ function findLastAccessedReport( ignoreDomainRooms: boolean, + guidesEmailsByReport: Record | undefined, openOnAdminRoom = false, excludeReportID?: string, reportNameValuePairs?: OnyxCollection, @@ -2456,7 +2480,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; } @@ -2563,7 +2596,7 @@ 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; } @@ -2573,7 +2606,7 @@ function isJoinRequestInAdminRoom(report: OnyxEntry): boolean { 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(currentUserLogin)) { return false; } } @@ -4458,7 +4491,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), @@ -9395,14 +9428,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; } @@ -9415,9 +9448,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; } @@ -9887,6 +9920,7 @@ type ShouldReportBeInOptionListParams = { conciergeReportID: string | undefined; /** Pre-computed value from reportAttributes derived value. When provided, skips the expensive requiresAttentionFromCurrentUser recomputation. */ requiresAttention?: boolean; + hasGuidesEmails: boolean; }; function reasonForReportToBeInOptionList({ @@ -9906,6 +9940,7 @@ function reasonForReportToBeInOptionList({ isReportArchived, conciergeReportID, requiresAttention, + hasGuidesEmails, }: ShouldReportBeInOptionListParams): ValueOf | null { const isInDefaultMode = !isInFocusMode; @@ -9957,7 +9992,7 @@ function reasonForReportToBeInOptionList({ return null; } - if (!canAccessReport(report, betas, isReportArchived)) { + if (!canAccessReport(report, betas, hasGuidesEmails, isReportArchived)) { return null; } @@ -11145,8 +11180,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 { @@ -14204,6 +14239,8 @@ export { getIntegrationIcon, canBeExported, isExported, + hasExpensifyGuidesEmails, + resolveHasGuidesEmails, hasExportError, hasOnlyNonReimbursableTransactions, getReportLastMessage, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 6c239d541190..b6fa02829687 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -208,6 +208,7 @@ import { isUnread, isUnreadWithMention, isWorkspaceTaskReport, + resolveHasGuidesEmails, shouldReportBeInOptionList, shouldReportShowSubscript, } from './ReportUtils'; @@ -295,6 +296,7 @@ type ShouldDisplayReportInLHNParams = { reportAttributes?: ReportAttributesDerivedValue['reports']; currentUserLogin: string; currentUserAccountID: number; + hasGuidesEmails: boolean; conciergeReportID: string | undefined; }; @@ -313,6 +315,7 @@ function shouldDisplayReportInLHN({ currentUserAccountID, currentUserLogin, conciergeReportID, + hasGuidesEmails, }: ShouldDisplayReportInLHNParams) { if (!report) { return {shouldDisplay: false}; @@ -377,6 +380,7 @@ function shouldDisplayReportInLHN({ currentUserLogin, currentUserAccountID, conciergeReportID, + hasGuidesEmails, }); return {shouldDisplay}; @@ -396,6 +400,7 @@ function getReportsToDisplayInLHN({ reportNameValuePairs, reportAttributes, conciergeReportID, + guidesEmailsByReport, }: { currentReportId: string | undefined; reports: OnyxCollection; @@ -409,6 +414,7 @@ function getReportsToDisplayInLHN({ currentUserAccountID: number; reportNameValuePairs?: OnyxCollection; reportAttributes?: ReportAttributesDerivedValue['reports']; + guidesEmailsByReport?: Record; conciergeReportID: string | undefined; }) { const isInFocusMode = priorityMode === CONST.PRIORITY_MODE.GSD; @@ -436,6 +442,11 @@ function getReportsToDisplayInLHN({ isReportArchived, reportAttributes, currentUserLogin, + hasGuidesEmails: resolveHasGuidesEmails({ + participantAccountIDs: Object.keys(report.participants ?? {}).map(Number), + guidesEmailsByReport, + reportID: report.reportID, + }), currentUserAccountID, conciergeReportID, }); @@ -466,6 +477,7 @@ type UpdateReportsToDisplayInLHNProps = { isOffline: boolean; currentUserLogin: string; currentUserAccountID: number; + guidesEmailsByReport?: Record; conciergeReportID: string | undefined; }; @@ -485,6 +497,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; @@ -522,6 +535,11 @@ function updateReportsToDisplayInLHN({ isReportArchived, reportAttributes, currentUserLogin, + hasGuidesEmails: resolveHasGuidesEmails({ + participantAccountIDs: Object.keys(report.participants ?? {}).map(Number), + guidesEmailsByReport, + reportID: report.reportID, + }), currentUserAccountID, conciergeReportID, }); @@ -1386,7 +1404,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 c73fe9378b47..def476e052b7 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -108,6 +108,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, conciergeReportID, }); }); diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index 1f5804f652c3..7b1193653238 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -603,7 +603,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 da9cf10cfde7..b0d933cb556f 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4828,7 +4828,8 @@ function navigateToMostRecentReport( isSelfTourViewed: boolean | undefined, 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 @@ -4868,7 +4869,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 67156c392ea6..1ea38dceb120 100644 --- a/src/libs/navigateAfterOnboarding.ts +++ b/src/libs/navigateAfterOnboarding.ts @@ -56,7 +56,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 5fcf434f013b..08b5a051506e 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -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'; @@ -79,13 +79,19 @@ 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 [reportOwnerPersonalDetail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsSelector(report?.ownerAccountID)}); const transactionID = DebugUtils.getTransactionID(report, reportActions); const isReportArchived = useReportIsArchived(reportID); + const participantAccountIDs = Object.keys(report?.participants ?? {}).map(Number); + const [hasGuidesEmails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + selector: hasExpensifyGuidesEmailsSelector(participantAccountIDs), + }); const metadata = useMemo(() => { if (!report) { @@ -123,6 +129,7 @@ function DebugReportPage({ currentUserLogin: currentUserLogin ?? '', currentUserAccountID, conciergeReportID, + hasGuidesEmails: hasGuidesEmails ?? false, }); return [ @@ -184,6 +191,7 @@ function DebugReportPage({ draftComment, translate, conciergeReportID, + hasGuidesEmails, ]); 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..f38d448e999a 100644 --- a/src/pages/inbox/report/AncestorReportActionItem.tsx +++ b/src/pages/inbox/report/AncestorReportActionItem.tsx @@ -19,7 +19,7 @@ import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {personalDetailsSelector} from '@selectors/PersonalDetails'; +import {hasExpensifyGuidesEmailsSelector, personalDetailsSelector} from '@selectors/PersonalDetails'; import React from 'react'; import ReportActionItem from './ReportActionItem'; @@ -95,12 +95,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 = Object.keys(report?.participants ?? {}).map(Number); + const [hasGuidesEmails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + selector: hasExpensifyGuidesEmailsSelector(participantAccountIDs), + }); 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, hasGuidesEmails ?? false, isAncestorReportArchived); const {isOffline} = useNetwork(); const {isInNarrowPaneModal} = useResponsiveLayout(); diff --git a/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx b/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx index 33048d801e4c..b637f602f6c4 100644 --- a/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx @@ -24,6 +24,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} from 'react'; type WithReportAndReportActionOrNotFoundProps = PlatformStackScreenProps< @@ -57,6 +58,10 @@ function WithReportOrNotFoundImpl = reportActions?.[`${props.route.params.reportActionID}`]; @@ -83,7 +88,7 @@ function WithReportOrNotFoundImpl Object.keys(report?.participants ?? {}).map(Number), [report?.participants]); + const guidesEmailsSelector = useMemo(() => hasExpensifyGuidesEmailsSelector(participantAccountIDs), [participantAccountIDs]); + const [hasGuidesEmails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + selector: guidesEmailsSelector, + }); const isFocused = useIsFocused(); const contentShown = React.useRef(false); const isReportIdInRoute = !!reportID?.length; @@ -117,7 +125,7 @@ export default function (shouldRequireReportID = true): (personalDetailsList: OnyxEntry) => getPersonalDetailsByID(accountID, personalDetailsList); @@ -102,6 +104,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; + }; +}; + const newAccountIDsAndLoginsSelector = (invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs | undefined) => (personalDetailsList: OnyxEntry) => getNewAccountIDsAndLogins(invitedEmailsToAccountIDs, personalDetailsList); @@ -119,5 +176,7 @@ export { accountIDToLoginSelector, isOptimisticPersonalDetailSelector, createDisplayDetailsByAccountIDsSelector, + hasExpensifyGuidesEmailsSelector, + createGuidesEmailsByReportSelector, newAccountIDsAndLoginsSelector, }; diff --git a/tests/actions/IOUTest/TrackExpenseTest.ts b/tests/actions/IOUTest/TrackExpenseTest.ts index 3998416ec3cf..dac79c0bd79e 100644 --- a/tests/actions/IOUTest/TrackExpenseTest.ts +++ b/tests/actions/IOUTest/TrackExpenseTest.ts @@ -176,6 +176,7 @@ describe('actions/IOU/TrackExpense', () => { currentUserAccountID: RORY_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -233,6 +234,7 @@ describe('actions/IOU/TrackExpense', () => { currentUserAccountID: RORY_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index 7c946f02bc1f..55458e37cbf5 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, conciergeReportID: undefined, }), ); diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 225cc15825e6..c9b1535b5fec 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -112,6 +112,7 @@ describe('SidebarUtils', () => { currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: 1, reportNameValuePairs: {}, + guidesEmailsByReport: {}, conciergeReportID: undefined, }), ); @@ -132,6 +133,7 @@ describe('SidebarUtils', () => { currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: 1, reportNameValuePairs: {}, + guidesEmailsByReport: {}, conciergeReportID: undefined, }), ); diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index 0e64fd3f0622..3396fd28c04a 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -752,6 +752,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBeNull(); @@ -764,6 +765,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: 'Hello world!', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasDraftComment'); @@ -779,6 +781,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasGBR'); @@ -793,6 +796,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.pinnedByUser'); @@ -811,6 +815,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasAddWorkspaceRoomErrors'); @@ -837,6 +842,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.isUnread'); @@ -856,6 +862,7 @@ describe('DebugUtils', () => { isReportArchived: isReportArchived.current, doesReportHaveViolations: false, draftComment: '', + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.isArchived'); @@ -870,6 +877,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.isSelfDM'); @@ -881,6 +889,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.isFocused'); @@ -941,6 +950,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); @@ -1001,6 +1011,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); @@ -1013,6 +1024,7 @@ describe('DebugUtils', () => { doesReportHaveViolations: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 1ed6adbe8f4a..d983d97be625 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -146,6 +146,8 @@ import { hasActionWithErrorsForTransaction, hasEmptyReportsForPolicy, hasExportError, + hasExpensifyGuidesEmails, + resolveHasGuidesEmails, hasNonReimbursableTransactions, hasReceiptError, hasSmartscanError, @@ -163,6 +165,7 @@ import { isDeprecatedGroupDM, isGroupPolicyExpenseReport, isHarvestCreatedExpenseReport, + isJoinRequestInAdminRoom, isMoneyRequestReportEligibleForMerge, isOneOnOneChat, isPayer, @@ -7172,6 +7175,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -7197,6 +7201,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -7227,6 +7232,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7281,6 +7287,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -7305,6 +7312,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -7329,6 +7337,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: 'fake draft', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -7353,6 +7362,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -7390,6 +7400,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -7422,6 +7433,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: isReportArchived.current, draftComment: '', + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -7454,6 +7466,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: isReportArchived.current, draftComment: '', + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7480,6 +7493,7 @@ describe('ReportUtils', () => { includeSelfDM, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -7508,6 +7522,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7529,6 +7544,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7553,6 +7569,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7597,6 +7614,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7618,6 +7636,7 @@ describe('ReportUtils', () => { excludeEmptyChats: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7647,6 +7666,7 @@ describe('ReportUtils', () => { excludeEmptyChats: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.DEFAULT); @@ -7675,6 +7695,7 @@ describe('ReportUtils', () => { draftComment: '', isReportArchived: undefined, conciergeReportID, + hasGuidesEmails: false, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.DEFAULT); }); @@ -7701,6 +7722,7 @@ describe('ReportUtils', () => { draftComment: '', isReportArchived: undefined, conciergeReportID: 'some-other-report-id', + hasGuidesEmails: false, }), ).toBeNull(); }); @@ -7727,6 +7749,7 @@ describe('ReportUtils', () => { excludeEmptyChats: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, }; // When the param identifies this report as Concierge, the empty chat is kept in the option list... @@ -7753,6 +7776,7 @@ describe('ReportUtils', () => { includeDomainEmail: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7800,6 +7824,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7821,6 +7846,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7856,6 +7882,7 @@ describe('ReportUtils', () => { draftComment: '', betas: undefined, isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7888,6 +7915,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.IS_UNREAD); @@ -7918,6 +7946,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -7941,6 +7970,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS); @@ -7986,6 +8016,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -8038,6 +8069,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBe(CONST.REPORT_IN_LHN_REASONS.PINNED_BY_USER); @@ -8061,6 +8093,7 @@ describe('ReportUtils', () => { includeSelfDM: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeNull(); @@ -8083,6 +8116,7 @@ describe('ReportUtils', () => { excludeEmptyChats: true, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeTruthy(); @@ -8105,6 +8139,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -8127,6 +8162,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -8149,6 +8185,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -8167,6 +8204,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }), ).toBeFalsy(); @@ -8224,6 +8262,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -9036,7 +9075,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 @@ -9089,7 +9128,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 @@ -13041,7 +13080,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]; @@ -13050,18 +13089,93 @@ 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(); + }); + }); + describe('getAllReportActionsErrorsAndReportActionThatRequiresAttention', () => { const report: Report = { ...createRandomReport(40003, undefined), @@ -14924,6 +15038,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -14983,6 +15099,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -15175,6 +15293,8 @@ describe('ReportUtils', () => { draftComment: undefined, betas: undefined, isReportArchived: undefined, + + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -15219,6 +15339,8 @@ describe('ReportUtils', () => { draftComment: undefined, betas: undefined, isReportArchived: undefined, + + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -15829,6 +15951,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -16013,6 +16137,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: undefined, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -16199,6 +16324,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: archiveState.current, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -16222,6 +16348,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: archiveState.current, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -16324,6 +16451,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: isReportArchivedBefore.current, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -16365,6 +16493,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, draftComment: '', isReportArchived: isReportArchivedAfter.current, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -17315,6 +17444,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -17383,6 +17513,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -17455,6 +17586,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).not.toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -17526,6 +17658,7 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -19564,6 +19697,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); @@ -19634,6 +19769,8 @@ describe('ReportUtils', () => { excludeEmptyChats: false, isReportArchived: false, draftComment: '', + + hasGuidesEmails: false, conciergeReportID: undefined, }); expect(reason).toBe(CONST.REPORT_IN_LHN_REASONS.HAS_GBR); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 9d635261f1a6..5c9e9e1c971c 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1054,6 +1054,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -1167,6 +1168,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -1187,6 +1189,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -1209,6 +1212,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -1233,6 +1237,7 @@ describe('SidebarUtils', () => { isOffline: true, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -1258,6 +1263,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -1305,6 +1311,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID, }); @@ -1344,6 +1351,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: 'a-different-report-id', }); @@ -1393,6 +1401,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -1424,6 +1433,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -1455,6 +1465,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + hasGuidesEmails: false, conciergeReportID: undefined, }); @@ -4595,6 +4606,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4620,6 +4632,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4643,6 +4656,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4663,6 +4677,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4687,6 +4702,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4715,6 +4731,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4744,6 +4761,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4773,6 +4791,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4812,6 +4831,7 @@ describe('SidebarUtils', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, reportNameValuePairs: {}, reportAttributes: undefined, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4842,6 +4862,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4870,6 +4891,7 @@ describe('SidebarUtils', () => { isOffline: true, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4896,6 +4918,7 @@ describe('SidebarUtils', () => { isOffline: true, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); @@ -4933,6 +4956,7 @@ describe('SidebarUtils', () => { isOffline: false, currentUserLogin: CURRENT_USER_LOGIN, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + guidesEmailsByReport: {}, conciergeReportID: undefined, }); diff --git a/tests/unit/navigateAfterOnboardingTest.ts b/tests/unit/navigateAfterOnboardingTest.ts index 73a19c0529fe..16a7d472f768 100644 --- a/tests/unit/navigateAfterOnboardingTest.ts +++ b/tests/unit/navigateAfterOnboardingTest.ts @@ -172,7 +172,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(); + }); });