diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index d74fac25e195..c5b9f8f9c40b 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -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'; @@ -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])); + 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. @@ -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; @@ -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) { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 5b8028c9a3ed..16b536c51527 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1123,35 +1123,53 @@ function isReportSelected(reportOption: OptionData, selectedOptions: Array (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } +function processReport( + report: OnyxEntry, + personalDetails: OnyxEntry, +): { + reportMapEntry?: [number, Report]; // The entry to add to reportMapForAccountIDs if applicable + reportOption: SearchOption | 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, reports?: OnyxCollection) { const reportMapForAccountIDs: Record = {}; const allReportOptions: Array> = []; 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, {}), - }); }); } @@ -2383,6 +2401,7 @@ export { getIsUserSubmittedExpenseOrScannedReceipt, getManagerMcTestParticipant, shouldShowLastActorDisplayName, + processReport, }; export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, Option, OptionTree, ReportAndPersonalDetailOptions};