Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
010fde6
recalculate only changed options in option context provider
TMisiukiewicz Apr 14, 2025
ea2c3d8
fix processing changed options
TMisiukiewicz Apr 15, 2025
dd1e057
Merge remote-tracking branch 'upstream/main' into perf/recalculate-ch…
TMisiukiewicz Apr 15, 2025
7fa608c
Merge branch 'main' into perf/recalculate-changed-options
TMisiukiewicz Apr 15, 2025
f30027e
Merge branch 'main' into perf/recalculate-changed-options
TMisiukiewicz Apr 15, 2025
a77ca6c
Merge branch 'main' into perf/recalculate-changed-options
TMisiukiewicz Apr 16, 2025
1a6c0b9
update canBeMissing
TMisiukiewicz Apr 16, 2025
aa62626
unify usage of creating options
TMisiukiewicz Apr 16, 2025
cd599df
recreate options after report attributes are regenerated
TMisiukiewicz Apr 16, 2025
e0c8858
Merge branch 'main' into perf/recalculate-changed-options
kacper-mikolajczak Apr 18, 2025
d83b803
Merge branch 'main' into perf/recalculate-changed-options
TMisiukiewicz Apr 22, 2025
02cec20
fix lint changed
TMisiukiewicz Apr 22, 2025
bc2ad1b
Merge remote-tracking branch 'upstream/main' into perf/recalculate-ch…
TMisiukiewicz Apr 23, 2025
4b7a275
Merge remote-tracking branch 'upstream/main' into perf/recalculate-ch…
TMisiukiewicz Apr 25, 2025
a208f34
Merge remote-tracking branch 'origin/main' into perf/recalculate-chan…
TMisiukiewicz Apr 28, 2025
dfad5d1
Merge remote-tracking branch 'upstream/main' into perf/recalculate-ch…
TMisiukiewicz Apr 28, 2025
4fa7376
fix typo
TMisiukiewicz Apr 28, 2025
9d61b4a
Merge remote-tracking branch 'upstream/main' into perf/recalculate-ch…
TMisiukiewicz Apr 29, 2025
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
88 changes: 63 additions & 25 deletions src/components/OptionListContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import {useOnyx} from 'react-native-onyx';
import usePrevious from '@hooks/usePrevious';
import {createOptionFromReport, createOptionList} from '@libs/OptionsListUtils';
import {createOptionFromReport, createOptionList, processReport} from '@libs/OptionsListUtils';
import type {OptionList, SearchOption} from '@libs/OptionsListUtils';
import {isSelfDM} from '@libs/ReportUtils';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -46,34 +46,80 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
reports: [],
personalDetails: [],
});
const [preferredLocale] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE, {canBeMissing: true});
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true});

const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true});
const prevReportAttributesLocale = usePrevious(reportAttributes?.locale);
const [reports, {sourceValue: changedReports}] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true});
const personalDetails = usePersonalDetails();
const prevPersonalDetails = usePrevious(personalDetails);
const hasInitialData = useMemo(() => Object.keys(personalDetails ?? {}).length > 0, [personalDetails]);

const loadOptions = useCallback(() => {
const optionLists = createOptionList(personalDetails, reports);
setOptions({
reports: optionLists.reports,
personalDetails: optionLists.personalDetails,
});
}, [personalDetails, reports]);

/**
* This effect is used to update the options list when reports change.
* This effect is responsible for generating the options list when their data is not yet initialized
*/
useEffect(() => {
// there is no need to update the options if the options are not initialized
if (!areOptionsInitialized.current || !reports) {
if (!areOptionsInitialized.current || !reports || hasInitialData) {
return;
}

loadOptions();
}, [reports, personalDetails, hasInitialData, loadOptions]);

/**
* This effect is responsible for generating the options list when the locale changes
* Since options might use report attributes, it's necessary to call this after report attributes are loaded with the new locale to make sure the options are generated in a proper language
*/
useEffect(() => {
if (reportAttributes?.locale === prevReportAttributesLocale) {
return;
}

loadOptions();
}, [prevReportAttributesLocale, loadOptions, reportAttributes?.locale]);

/**
* This effect is responsible for updating the options only for changed reports
*/
useEffect(() => {
if (!changedReports || !areOptionsInitialized.current) {
return;
}
// Since reports updates can happen in bulk, and some reports depend on other reports, we need to recreate the whole list from scratch.
const newReports = createOptionList(personalDetails, reports).reports;

setOptions((prevOptions) => {
const newOptions = {
const changedReportEntries = Object.values(changedReports);
if (changedReportEntries.length === 0) {
return prevOptions;
}

const updatedReportsMap = new Map(prevOptions.reports.map((report) => [report.reportID, report]));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #74230, the report can be undefined, so we need to add the safeguard here

changedReportEntries.forEach((report) => {
if (!report) {
return;
}

const reportID = report?.reportID;
const {reportOption} = processReport(report, personalDetails);

if (reportOption) {
updatedReportsMap.set(reportID, reportOption);
} else {
updatedReportsMap.delete(reportID);
}
});

return {
...prevOptions,
reports: newReports,
reports: Array.from(updatedReportsMap.values()),
};

return newOptions;
});

// eslint-disable-next-line react-compiler/react-compiler
}, [reports, personalDetails, preferredLocale]);
}, [changedReports, personalDetails]);

/**
* This effect is used to update the options list when personal details change.
Expand Down Expand Up @@ -142,14 +188,6 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [personalDetails]);

const loadOptions = useCallback(() => {
const optionLists = createOptionList(personalDetails, reports);
setOptions({
reports: optionLists.reports,
personalDetails: optionLists.personalDetails,
});
}, [personalDetails, reports]);

const initializeOptions = useCallback(() => {
loadOptions();
areOptionsInitialized.current = true;
Expand Down Expand Up @@ -182,7 +220,7 @@ const useOptionsListContext = () => useContext(OptionsListContext);
const useOptionsList = (options?: {shouldInitialize: boolean}) => {
const {shouldInitialize = true} = options ?? {};
const {initializeOptions, options: optionsList, areOptionsInitialized, resetOptions} = useOptionsListContext();
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true});
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: false});

useEffect(() => {
if (!shouldInitialize || areOptionsInitialized || isLoadingApp) {
Expand Down
57 changes: 38 additions & 19 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1123,35 +1123,53 @@ function isReportSelected(reportOption: OptionData, selectedOptions: Array<Parti
return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID));
}

function processReport(
report: OnyxEntry<Report>,
personalDetails: OnyxEntry<PersonalDetailsList>,
): {
reportMapEntry?: [number, Report]; // The entry to add to reportMapForAccountIDs if applicable
reportOption: SearchOption<Report> | null; // The report option to add to allReportOptions if applicable
} {
if (!report) {
return {reportOption: null};
}

const isOneOnOneChat = reportUtilsIsOneOnOneChat(report);
const accountIDs = getParticipantsAccountIDsForDisplay(report);
const isChatRoom = reportUtilsIsChatRoom(report);

if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) {
return {reportOption: null};
}

// Determine if this report should be mapped to a personal detail
const reportMapEntry = accountIDs.length <= 1 && isOneOnOneChat ? ([accountIDs.at(0), report] as [number, Report]) : undefined;

return {
reportMapEntry,
reportOption: {
item: report,
...createOption(accountIDs, personalDetails, report, {}),
},
};
}

function createOptionList(personalDetails: OnyxEntry<PersonalDetailsList>, reports?: OnyxCollection<Report>) {
const reportMapForAccountIDs: Record<number, Report> = {};
const allReportOptions: Array<SearchOption<Report>> = [];

if (reports) {
Object.values(reports).forEach((report) => {
if (!report) {
return;
}

const isOneOnOneChat = reportUtilsIsOneOnOneChat(report);
const accountIDs = getParticipantsAccountIDsForDisplay(report);
const {reportMapEntry, reportOption} = processReport(report, personalDetails);

const isChatRoom = reportUtilsIsChatRoom(report);
if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) {
return;
if (reportMapEntry) {
const [accountID, reportValue] = reportMapEntry;
reportMapForAccountIDs[accountID] = reportValue;
}

// Save the report in the map if this is a single participant so we can associate the reportID with the
// personal detail option later. Individuals should not be associated with single participant
// policyExpenseChats or chatRooms since those are not people.
if (accountIDs.length <= 1 && isOneOnOneChat) {
reportMapForAccountIDs[accountIDs[0]] = report;
if (reportOption) {
allReportOptions.push(reportOption);
}

allReportOptions.push({
item: report,
...createOption(accountIDs, personalDetails, report, {}),
});
});
}

Expand Down Expand Up @@ -2383,6 +2401,7 @@ export {
getIsUserSubmittedExpenseOrScannedReceipt,
getManagerMcTestParticipant,
shouldShowLastActorDisplayName,
processReport,
};

export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, Option, OptionTree, ReportAndPersonalDetailOptions};