Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/hooks/useSidebarOrderedReports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -49,7 +50,14 @@ type SidebarOrderedReportsActionsContextValue = {
setStickyReportID: (reportID: string) => void;
};

type ReportsToDisplayInLHN = Record<string, OnyxTypes.Report & {hasErrorsOtherThanFailedReceipt?: boolean; requiresAttention?: boolean; isUnreadReport?: boolean}>;
type ReportsToDisplayInLHN = Record<
string,
OnyxTypes.Report & {
hasErrorsOtherThanFailedReceipt?: boolean;
requiresAttention?: boolean;
isUnreadReport?: boolean;
}
>;

const SidebarOrderedReportsStateContext = createContext<SidebarOrderedReportsStateContextValue>({
filteredReports: [],
Expand Down Expand Up @@ -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<OnyxTypes.PersonalDetailsList>) => 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<ReportsToDisplayInLHN>({});
Expand Down Expand Up @@ -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) {
Expand All @@ -232,6 +257,7 @@ function SidebarOrderedReportsContextProvider({
currentUserLogin: currentUserLogin ?? '',
currentUserAccountID: accountID,
conciergeReportID,
guidesEmailsByReport,
});
} else {
Log.info('[useSidebarOrderedReports] building reportsToDisplay from scratch');
Expand All @@ -249,6 +275,7 @@ function SidebarOrderedReportsContextProvider({
reportNameValuePairs,
reportAttributes,
conciergeReportID,
guidesEmailsByReport,
});
}

Expand All @@ -270,6 +297,9 @@ function SidebarOrderedReportsContextProvider({
currentUserLogin,
accountID,
conciergeReportID,
guidesEmailsByReport,
guidesEmailsByReportKey,
prevGuidesEmailsByReportKey,
]);

// Derive a stable boolean map indicating which reports have drafts.
Expand Down
3 changes: 3 additions & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,7 @@ function getReasonForShowingRowInLHN({
currentUserLogin,
currentUserAccountID,
conciergeReportID,
hasGuidesEmails,
}: {
report: OnyxEntry<Report>;
chatReport: OnyxEntry<Report>;
Expand All @@ -1479,6 +1480,7 @@ function getReasonForShowingRowInLHN({
currentUserLogin?: string;
currentUserAccountID?: number;
conciergeReportID?: string;
hasGuidesEmails: boolean;
}): TranslationPaths | null {
if (!report) {
return null;
Expand All @@ -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<typeof reason>).includes(reason) && hasRBR) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ function ReportsSplitNavigator({route}: PlatformStackScreenProps<TabNavigatorPar
return '';
}

const initialReport = ReportUtils.findLastAccessedReport(!isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), isOpenOnAdminRoom, 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
Comment thread
Krishna2323 marked this conversation as resolved.
const initialReport = ReportUtils.findLastAccessedReport(!isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), undefined, isOpenOnAdminRoom, undefined, reportNameValuePairs);
// eslint-disable-next-line rulesdir/no-default-id-values
return initialReport?.reportID ?? '';
});
Expand Down
14 changes: 13 additions & 1 deletion src/libs/OptionsListUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,11 @@ import {
getReportTransactions,
getUnreportedTransactionMessage,
getViolatingReportIDForRBRInLHN,
hasExpensifyGuidesEmails,
hasIOUWaitingOnCurrentUserBankAccount,
isArchivedNonExpenseReport,
isChatThread,
isDefaultRoom,
isDM,
isExpenseReport,
isHiddenForCurrentUser,
Expand Down Expand Up @@ -2114,7 +2116,14 @@ function getUserToInviteOption({
return userToInvite;
}

function isValidReport(option: SearchOption<Report>, policy: OnyxEntry<Policy>, config: IsValidReportsConfig, draftComment: string | undefined, chatReport: OnyxEntry<Report>): boolean {
function isValidReport(
option: SearchOption<Report>,
policy: OnyxEntry<Policy>,
config: IsValidReportsConfig,
draftComment: string | undefined,
chatReport: OnyxEntry<Report>,
hasGuidesEmails: boolean,
): boolean {
const {
betas = [],
includeMultipleParticipantReports = false,
Expand Down Expand Up @@ -2158,6 +2167,7 @@ function isValidReport(option: SearchOption<Report>, policy: OnyxEntry<Policy>,
currentUserLogin,
currentUserAccountID,
conciergeReportID,
hasGuidesEmails,
});

if (!shouldBeInOptionList) {
Expand Down Expand Up @@ -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
Comment thread
Krishna2323 marked this conversation as resolved.
isDefaultRoom(report.item) ? hasExpensifyGuidesEmails(Object.keys(report.item?.participants ?? {}).map(Number), undefined) : false,
);
};

Expand Down
65 changes: 52 additions & 13 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2409,8 +2409,31 @@ function isHiddenForCurrentUser(reportOrPreference: OnyxEntry<Report> | 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<PersonalDetailsList>): boolean {
Comment thread
Krishna2323 marked this conversation as resolved.
// 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<string, boolean>;
reportID?: string;
}): boolean {
if (reportID && guidesEmailsByReport && reportID in guidesEmailsByReport) {
return guidesEmailsByReport[reportID];
}
return hasGuidesEmails ?? hasExpensifyGuidesEmails(participantAccountIDs, undefined);
}

function getMostRecentlyVisitedReport(reports: Array<OnyxEntry<Report>>, lastVisitTimes: Record<string, string>): OnyxEntry<Report> {
Expand All @@ -2429,6 +2452,7 @@ function getMostRecentlyVisitedReport(reports: Array<OnyxEntry<Report>>, lastVis
*/
function findLastAccessedReport(
ignoreDomainRooms: boolean,
guidesEmailsByReport: Record<string, boolean> | undefined,
openOnAdminRoom = false,
excludeReportID?: string,
reportNameValuePairs?: OnyxCollection<ReportNameValuePairs>,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -2567,17 +2600,19 @@ function isClosedReport(report: OnyxInputOrEntry<Report>): boolean {
/**
* Whether the provided report is the admin's room
*/
function isJoinRequestInAdminRoom(report: OnyxEntry<Report>): boolean {
function isJoinRequestInAdminRoom(report: OnyxEntry<Report>, currentUserLogin: string | undefined): boolean {
if (!report) {
return false;
}
// TODO: Remove fallback once all callers pass currentUserLogin (https://github.com/Expensify/App/issues/66413)
Comment thread
Krishna2323 marked this conversation as resolved.
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;
}
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -9002,14 +9037,14 @@ function isIOUOwnedByCurrentUser(report: OnyxEntry<Report>, 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<Report>, betas: OnyxEntry<Beta[]>, isReportArchived = false): boolean {
function canSeeDefaultRoom(report: OnyxEntry<Report>, betas: OnyxEntry<Beta[]>, 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;
}

Expand All @@ -9022,9 +9057,9 @@ function canSeeDefaultRoom(report: OnyxEntry<Report>, betas: OnyxEntry<Beta[]>,
return Permissions.isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS, betas ?? []);
}

function canAccessReport(report: OnyxEntry<Report>, betas: OnyxEntry<Beta[]>, isReportArchived = false): boolean {
function canAccessReport(report: OnyxEntry<Report>, betas: OnyxEntry<Beta[]>, 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;
}

Expand Down Expand Up @@ -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({
Expand All @@ -9496,6 +9532,7 @@ function reasonForReportToBeInOptionList({
isReportArchived,
conciergeReportID,
requiresAttention,
hasGuidesEmails,
}: ShouldReportBeInOptionListParams): ValueOf<typeof CONST.REPORT_IN_LHN_REASONS> | null {
const isInDefaultMode = !isInFocusMode;

Expand Down Expand Up @@ -9547,7 +9584,7 @@ function reasonForReportToBeInOptionList({
return null;
}

if (!canAccessReport(report, betas, isReportArchived)) {
if (!canAccessReport(report, betas, hasGuidesEmails, isReportArchived)) {
return null;
}

Expand Down Expand Up @@ -10716,8 +10753,8 @@ function isReportParticipant(accountID: number | undefined, report: OnyxEntry<Re
/**
* Check to see if the current user has access to view the report.
*/
function canCurrentUserOpenReport(report: OnyxEntry<Report>, betas: OnyxEntry<Beta[]>, isReportArchived = false): boolean {
return (isReportParticipant(deprecatedCurrentUserAccountID, report) || isPublicRoom(report)) && canAccessReport(report, betas, isReportArchived);
function canCurrentUserOpenReport(report: OnyxEntry<Report>, betas: OnyxEntry<Beta[]>, hasGuidesEmails: boolean, isReportArchived = false): boolean {
return (isReportParticipant(deprecatedCurrentUserAccountID, report) || isPublicRoom(report)) && canAccessReport(report, betas, hasGuidesEmails, isReportArchived);
}

function shouldUseFullTitleToDisplay(report: OnyxEntry<Report>): boolean {
Expand Down Expand Up @@ -13677,6 +13714,8 @@ export {
getIntegrationIcon,
canBeExported,
isExported,
hasExpensifyGuidesEmails,
resolveHasGuidesEmails,
hasExportError,
hasOnlyNonReimbursableTransactions,
getReportLastMessage,
Expand Down
Loading
Loading