From 840ac30b22dda0a10b34dc88031d9eb7aee8a1bf Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 11 Mar 2024 12:17:28 +0100 Subject: [PATCH 01/41] add function to create all reports and personal details options --- src/libs/OptionsListUtils.ts | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3dd23752d5db..9d309877ae75 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1329,6 +1329,65 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } +function createOptionList(reports: OnyxCollection, personalDetails: OnyxEntry, {reportActions = {}}: Partial) { + const reportMapForAccountIDs: Record = {}; + // Sorting the reports works like this: + // - Order everything by the last message timestamp (descending) + // - All archived reports should remain at the bottom + const orderedReports = lodashSortBy(reports, (report) => { + if (ReportUtils.isArchivedRoom(report)) { + return CONST.DATE.UNIX_EPOCH; + } + + return report?.lastVisibleActionCreated; + }); + orderedReports.reverse(); + const allReportOptions: ReportUtils.OptionData[] = []; + + orderedReports.forEach((report) => { + if (!report) { + return; + } + + const isSelfDM = ReportUtils.isSelfDM(report); + // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. + const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; + + if (!accountIDs || accountIDs.length === 0) { + return; + } + + // 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) { + reportMapForAccountIDs[accountIDs[0]] = report; + } + + allReportOptions.push( + createOption(accountIDs, personalDetails, report, reportActions, { + showChatPreviewLine: true, + forcePolicyNamePreview: true, + }), + ); + }); + + const havingLoginPersonalDetails = Object.fromEntries( + Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail), + ); + const allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => + createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], reportActions, { + showChatPreviewLine: true, + forcePolicyNamePreview: true, + }), + ); + + return { + reportOptions: allReportOptions, + personalDetailsOptions: allPersonalDetailsOptions, + }; +} + /** * Build the options */ @@ -2063,6 +2122,7 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + createOptionList, }; export type {MemberForList, CategorySection, GetOptions}; From 68e364c017cca6df15f913e8c0c218093801db1d Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 11 Mar 2024 13:14:25 +0100 Subject: [PATCH 02/41] use context to initialize all options --- src/App.tsx | 2 + src/components/OnyxProvider.tsx | 5 ++ src/components/OptionListContextProvider.tsx | 60 ++++++++++++++++++++ src/libs/OptionsListUtils.ts | 34 +++++++---- src/pages/SearchPage/index.js | 7 ++- 5 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 src/components/OptionListContextProvider.tsx diff --git a/src/App.tsx b/src/App.tsx index 0e247d5faa53..e53ba387d4d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import ErrorBoundary from './components/ErrorBoundary'; import HTMLEngineProvider from './components/HTMLEngineProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; +import {OptionsListContextProvider} from './components/OptionListContextProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; @@ -78,6 +79,7 @@ function App({url}: AppProps) { PlaybackContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, + OptionsListContextProvider, ]} > diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx index 0bc9130ea4a8..c57c38c3eba1 100644 --- a/src/components/OnyxProvider.tsx +++ b/src/components/OnyxProvider.tsx @@ -15,6 +15,7 @@ const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME); const [withFrequentlyUsedEmojis, FrequentlyUsedEmojisProvider, , useFrequentlyUsedEmojis] = createOnyxContext(ONYXKEYS.FREQUENTLY_USED_EMOJIS); const [withPreferredEmojiSkinTone, PreferredEmojiSkinToneProvider, PreferredEmojiSkinToneContext] = createOnyxContext(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE); +const [withReports, ReportsProvider, ReportsContext, useReports] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT); const [, SessionProvider, , useSession] = createOnyxContext(ONYXKEYS.SESSION); type OnyxProviderProps = { @@ -37,6 +38,7 @@ function OnyxProvider(props: OnyxProviderProps) { FrequentlyUsedEmojisProvider, PreferredEmojiSkinToneProvider, SessionProvider, + ReportsProvider, ]} > {props.children} @@ -69,4 +71,7 @@ export { useBlockedFromConcierge, useReportActionsDrafts, useSession, + withReports, + ReportsContext, + useReports, }; diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx new file mode 100644 index 000000000000..6c7e96d68078 --- /dev/null +++ b/src/components/OptionListContextProvider.tsx @@ -0,0 +1,60 @@ +import React, {createContext, useCallback, useContext, useMemo, useRef, useState} from 'react'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import type {OptionData} from '@libs/ReportUtils'; +import {usePersonalDetails, useReports} from './OnyxProvider'; + +type Options = { + reports: OptionData[]; + personalDetails: OptionData[]; +}; + +type OptionsListContextProps = { + options: Options; + initializeOptions: () => void; +}; + +type OptionsListProviderProps = { + /** Actual content wrapped by this component */ + children: React.ReactNode; +}; + +const OptionsListContext = createContext({ + options: { + reports: [], + personalDetails: [], + }, + initializeOptions: () => {}, +}); + +function OptionsListContextProvider({children}: OptionsListProviderProps) { + const areOptionsInitialized = useRef(false); + const [options, setOptions] = useState({ + reports: [], + personalDetails: [], + }); + const personalDetails = usePersonalDetails(); + const reports = useReports(); + + const loadOptions = useCallback(() => { + const optionLists = OptionsListUtils.createOptionList(reports, personalDetails); + setOptions({ + reports: optionLists.reports, + personalDetails: optionLists.personalDetails, + }); + }, [personalDetails, reports]); + + const initializeOptions = useCallback(() => { + if (areOptionsInitialized.current) { + return; + } + + areOptionsInitialized.current = true; + loadOptions(); + }, [loadOptions]); + + return ({options, initializeOptions}), [options, initializeOptions])}>{children}; +} + +const useOptionsListContext = () => useContext(OptionsListContext); + +export {OptionsListContextProvider, useOptionsListContext}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 9d309877ae75..3b08aa923aba 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1329,7 +1329,7 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } -function createOptionList(reports: OnyxCollection, personalDetails: OnyxEntry, {reportActions = {}}: Partial) { +function createOptionList(reports: OnyxCollection, personalDetails: OnyxEntry) { const reportMapForAccountIDs: Record = {}; // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) @@ -1365,10 +1365,16 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx } allReportOptions.push( - createOption(accountIDs, personalDetails, report, reportActions, { - showChatPreviewLine: true, - forcePolicyNamePreview: true, - }), + createOption( + accountIDs, + personalDetails, + report, + {}, + { + showChatPreviewLine: true, + forcePolicyNamePreview: true, + }, + ), ); }); @@ -1376,15 +1382,21 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail), ); const allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => - createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], reportActions, { - showChatPreviewLine: true, - forcePolicyNamePreview: true, - }), + createOption( + [personalDetail?.accountID ?? -1], + personalDetails, + reportMapForAccountIDs[personalDetail?.accountID ?? -1], + {}, + { + showChatPreviewLine: true, + forcePolicyNamePreview: true, + }, + ), ); return { - reportOptions: allReportOptions, - personalDetailsOptions: allPersonalDetailsOptions, + reports: allReportOptions, + personalDetails: allPersonalDetailsOptions, }; } diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 2a6308d27294..e0f547ba4eae 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsListContext} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -63,7 +64,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); const personalDetails = usePersonalDetails(); - + const {initializeOptions} = useOptionsListContext(); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); @@ -96,6 +97,10 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { return {...options, headerMessage: header}; }, [debouncedSearchValue, reports, personalDetails, betas, isScreenTransitionEnd]); + useEffect(() => { + initializeOptions(); + }, [initializeOptions]); + const sections = useMemo(() => { const newSections = []; let indexOffset = 0; From d32dd3a0fdc796c97308df658164ce978f63d449 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 11 Mar 2024 16:15:09 +0100 Subject: [PATCH 03/41] update getOptions, use options generation in search --- src/components/OptionListContextProvider.tsx | 8 +- src/libs/OptionsListUtils.ts | 239 +++++++++---------- src/pages/SearchPage/index.js | 20 +- 3 files changed, 132 insertions(+), 135 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 6c7e96d68078..d218e4873210 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -11,6 +11,7 @@ type Options = { type OptionsListContextProps = { options: Options; initializeOptions: () => void; + areOptionsInitialized: boolean; }; type OptionsListProviderProps = { @@ -24,6 +25,7 @@ const OptionsListContext = createContext({ personalDetails: [], }, initializeOptions: () => {}, + areOptionsInitialized: false, }); function OptionsListContextProvider({children}: OptionsListProviderProps) { @@ -52,7 +54,11 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { loadOptions(); }, [loadOptions]); - return ({options, initializeOptions}), [options, initializeOptions])}>{children}; + return ( + ({options, initializeOptions, areOptionsInitialized: areOptionsInitialized.current}), [options, initializeOptions])}> + {children} + + ); } const useOptionsListContext = () => useContext(OptionsListContext); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3b08aa923aba..a142f8abcfc3 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1329,6 +1329,10 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } +type ReportOption = ReportUtils.OptionData & { + item: Report | PersonalDetails; +}; + function createOptionList(reports: OnyxCollection, personalDetails: OnyxEntry) { const reportMapForAccountIDs: Record = {}; // Sorting the reports works like this: @@ -1342,7 +1346,7 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx return report?.lastVisibleActionCreated; }); orderedReports.reverse(); - const allReportOptions: ReportUtils.OptionData[] = []; + const allReportOptions: ReportOption[] = []; orderedReports.forEach((report) => { if (!report) { @@ -1364,8 +1368,9 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx reportMapForAccountIDs[accountIDs[0]] = report; } - allReportOptions.push( - createOption( + allReportOptions.push({ + item: report, + ...createOption( accountIDs, personalDetails, report, @@ -1375,14 +1380,15 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx forcePolicyNamePreview: true, }, ), - ); + }); }); const havingLoginPersonalDetails = Object.fromEntries( Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail), ); - const allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => - createOption( + const allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => ({ + item: personalDetail, + ...createOption( [personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], @@ -1392,7 +1398,7 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx forcePolicyNamePreview: true, }, ), - ); + })); return { reports: allReportOptions, @@ -1401,11 +1407,13 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx } /** - * Build the options + * filter options based on specific conditions */ function getOptions( - reports: OnyxCollection, - personalDetails: OnyxEntry, + options: { + reports: ReportOption[]; + personalDetails: ReportOption[]; + }, { reportActions = {}, betas = [], @@ -1483,26 +1491,13 @@ function getOptions( }; } - if (!isPersonalDetailsReady(personalDetails)) { - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - currentUserOption: null, - categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], - }; - } - - let recentReportOptions = []; - let personalDetailsOptions: ReportUtils.OptionData[] = []; - const reportMapForAccountIDs: Record = {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); + const topmostReportId = Navigation.getTopmostReportId() ?? ''; // Filter out all the reports that shouldn't be displayed - const filteredReports = Object.values(reports ?? {}).filter((report) => { + const filteredReportOptions = options.reports.filter((option) => { + const report = option.item as Report; const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null; @@ -1511,7 +1506,7 @@ function getOptions( return ReportUtils.shouldReportBeInOptionList({ report, - currentReportId: Navigation.getTopmostReportId() ?? '', + currentReportId: topmostReportId, betas, policies, doesReportHaveViolations, @@ -1524,23 +1519,24 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReports = lodashSortBy(filteredReports, (report) => { + const orderedReportOptions = lodashSortBy(filteredReportOptions, (option) => { + const report = option.item as Report; if (ReportUtils.isArchivedRoom(report)) { return CONST.DATE.UNIX_EPOCH; } return report?.lastVisibleActionCreated; }); - orderedReports.reverse(); + orderedReportOptions.reverse(); + + const allReportOptions = orderedReportOptions.filter((option) => { + const report = option.item as Report; - const allReportOptions: ReportUtils.OptionData[] = []; - orderedReports.forEach((report) => { if (!report) { return; } const isThread = ReportUtils.isChatThread(report); - const isChatRoom = ReportUtils.isChatRoom(report); const isTaskReport = ReportUtils.isTaskReport(report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); @@ -1582,33 +1578,12 @@ function getOptions( return; } - // 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 && !isPolicyExpenseChat && !isChatRoom) { - reportMapForAccountIDs[accountIDs[0]] = report; - } - - allReportOptions.push( - createOption(accountIDs, personalDetails, report, reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + return option; }); - // We're only picking personal details that have logins set - // This is a temporary fix for all the logic that's been breaking because of the new privacy changes - // See https://github.com/Expensify/Expensify/issues/293465 for more context - // Moreover, we should not override the personalDetails object, otherwise the createOption util won't work properly, it returns incorrect tooltipText - const havingLoginPersonalDetails = !includeP2P - ? {} - : Object.fromEntries(Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail)); - let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => - createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + + // HERE + + let allPersonalDetailsOptions = options.personalDetails; if (sortPersonalDetailsByAlphaAsc) { // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 @@ -1629,6 +1604,9 @@ function getOptions( optionsToExclude.push({login}); }); + let recentReportOptions = []; + let personalDetailsOptions: ReportUtils.OptionData[] = []; + if (includeRecentReports) { for (const reportOption of allReportOptions) { // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value @@ -1794,10 +1772,17 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: Record, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { +function getSearchOptions( + options: { + reports: ReportOption[]; + personalDetails: ReportOption[]; + }, + searchValue = '', + betas: Beta[] = [], +): GetOptions { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); - const options = getOptions(reports, personalDetails, { + const optionList = getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1816,11 +1801,18 @@ function getSearchOptions(reports: Record, personalDetails: Onyx Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); - return options; + return optionList; } -function getShareLogOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { - return getOptions(reports, personalDetails, { +function getShareLogOptions( + options: { + reports: ReportOption[]; + personalDetails: ReportOption[]; + }, + searchValue = '', + betas: Beta[] = [], +): GetOptions { + return getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1869,8 +1861,8 @@ function getIOUConfirmationOptionsFromParticipants(participants: Participant[], * Build the options for the New Group view */ function getFilteredOptions( - reports: OnyxCollection, - personalDetails: OnyxEntry, + reports: ReportOption[] = [], + personalDetails: ReportOption[] = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1889,28 +1881,31 @@ function getFilteredOptions( taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - includeRecentReports: true, - includePersonalDetails: true, - maxRecentReportsToShow: 5, - excludeLogins, - includeOwnedWorkspaceChats, - includeP2P, - includeCategories, - categories, - recentlyUsedCategories, - includeTags, - tags, - recentlyUsedTags, - canInviteUser, - includeSelectedOptions, - includeTaxRates, - taxRates, - includeSelfDM, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + includeRecentReports: true, + includePersonalDetails: true, + maxRecentReportsToShow: 5, + excludeLogins, + includeOwnedWorkspaceChats, + includeP2P, + includeCategories, + categories, + recentlyUsedCategories, + includeTags, + tags, + recentlyUsedTags, + canInviteUser, + includeSelectedOptions, + includeTaxRates, + taxRates, + includeSelfDM, + }, + ); } /** @@ -1918,8 +1913,8 @@ function getFilteredOptions( */ function getShareDestinationOptions( - reports: Record, - personalDetails: OnyxEntry, + reports: ReportOption[], + personalDetails: ReportOption[], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1927,24 +1922,27 @@ function getShareDestinationOptions( includeOwnedWorkspaceChats = true, excludeUnknownUsers = true, ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - maxRecentReportsToShow: 0, // Unlimited - includeRecentReports: true, - includeMultipleParticipantReports: true, - includePersonalDetails: false, - showChatPreviewLine: true, - forcePolicyNamePreview: true, - includeThreads: true, - includeMoneyRequests: true, - includeTasks: true, - excludeLogins, - includeOwnedWorkspaceChats, - excludeUnknownUsers, - includeSelfDM: true, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + maxRecentReportsToShow: 0, // Unlimited + includeRecentReports: true, + includeMultipleParticipantReports: true, + includePersonalDetails: false, + showChatPreviewLine: true, + forcePolicyNamePreview: true, + includeThreads: true, + includeMoneyRequests: true, + includeTasks: true, + excludeLogins, + includeOwnedWorkspaceChats, + excludeUnknownUsers, + includeSelfDM: true, + }, + ); } /** @@ -1976,21 +1974,18 @@ function formatMemberForList(member: ReportUtils.OptionData): MemberForList { /** * Build the options for the Workspace Member Invite view */ -function getMemberInviteOptions( - personalDetails: OnyxEntry, - betas: Beta[] = [], - searchValue = '', - excludeLogins: string[] = [], - includeSelectedOptions = false, -): GetOptions { - return getOptions({}, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - includePersonalDetails: true, - excludeLogins, - sortPersonalDetailsByAlphaAsc: true, - includeSelectedOptions, - }); +function getMemberInviteOptions(personalDetails: ReportOption[], betas: Beta[] = [], searchValue = '', excludeLogins: string[] = [], includeSelectedOptions = false): GetOptions { + return getOptions( + {reports: [], personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + includePersonalDetails: true, + excludeLogins, + sortPersonalDetailsByAlphaAsc: true, + includeSelectedOptions, + }, + ); } /** diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index e0f547ba4eae..bd0650c1d231 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -17,7 +17,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -30,9 +29,6 @@ const propTypes = { /** Beta features list */ betas: PropTypes.arrayOf(PropTypes.string), - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - /** Whether or not we are searching for reports on the server */ isSearchingForReports: PropTypes.bool, @@ -46,7 +42,6 @@ const propTypes = { const defaultProps = { betas: [], - reports: {}, isSearchingForReports: false, navigation: {}, }; @@ -58,13 +53,13 @@ const setPerformanceTimersEnd = () => { const SearchPageFooterInstance = ; -function SearchPage({betas, reports, isSearchingForReports, navigation}) { +function SearchPage({betas, isSearchingForReports, navigation}) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); const personalDetails = usePersonalDetails(); - const {initializeOptions} = useOptionsListContext(); + const {options, initializeOptions, areOptionsInitialized} = useOptionsListContext(); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); @@ -84,7 +79,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { userToInvite, headerMessage, } = useMemo(() => { - if (!isScreenTransitionEnd) { + if (!areOptionsInitialized && !isScreenTransitionEnd) { return { recentReports: {}, personalDetails: {}, @@ -92,10 +87,11 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { headerMessage: '', }; } - const options = OptionsListUtils.getSearchOptions(reports, personalDetails, debouncedSearchValue.trim(), betas); - const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); - return {...options, headerMessage: header}; - }, [debouncedSearchValue, reports, personalDetails, betas, isScreenTransitionEnd]); + + const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas); + const header = OptionsListUtils.getHeaderMessage(options.reports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); + return {...optionList, headerMessage: header}; + }, [areOptionsInitialized, isScreenTransitionEnd, options, betas, debouncedSearchValue]); useEffect(() => { initializeOptions(); From 50c823c7ab61d2743dd5ebec6e1c8dc666057123 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 12 Mar 2024 13:36:08 +0100 Subject: [PATCH 04/41] update usage in search page --- src/components/OptionListContextProvider.tsx | 11 ++--- src/libs/OptionsListUtils.ts | 42 +++++++------------- src/pages/SearchPage/index.tsx | 18 +++------ 3 files changed, 24 insertions(+), 47 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index d218e4873210..36ce61ec615f 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -1,15 +1,10 @@ import React, {createContext, useCallback, useContext, useMemo, useRef, useState} from 'react'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import type {OptionData} from '@libs/ReportUtils'; +import type {OptionList} from '@libs/OptionsListUtils'; import {usePersonalDetails, useReports} from './OnyxProvider'; -type Options = { - reports: OptionData[]; - personalDetails: OptionData[]; -}; - type OptionsListContextProps = { - options: Options; + options: OptionList; initializeOptions: () => void; areOptionsInitialized: boolean; }; @@ -30,7 +25,7 @@ const OptionsListContext = createContext({ function OptionsListContextProvider({children}: OptionsListProviderProps) { const areOptionsInitialized = useRef(false); - const [options, setOptions] = useState({ + const [options, setOptions] = useState({ reports: [], personalDetails: [], }); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 2d18010fc434..95c99fa2f890 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -53,6 +53,15 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +type ReportOption = ReportUtils.OptionData & { + item: Report | PersonalDetails; +}; + +type OptionList = { + reports: ReportOption[]; + personalDetails: ReportOption[]; +}; + type Tag = { enabled: boolean; name: string; @@ -1329,10 +1338,6 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } -type ReportOption = ReportUtils.OptionData & { - item: Report | PersonalDetails; -}; - function createOptionList(reports: OnyxCollection, personalDetails: OnyxEntry) { const reportMapForAccountIDs: Record = {}; // Sorting the reports works like this: @@ -1402,7 +1407,7 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx return { reports: allReportOptions, - personalDetails: allPersonalDetailsOptions, + personalDetails: allPersonalDetailsOptions as ReportOption[], }; } @@ -1410,10 +1415,7 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx * filter options based on specific conditions */ function getOptions( - options: { - reports: ReportOption[]; - personalDetails: ReportOption[]; - }, + options: OptionList, { reportActions = {}, betas = [], @@ -1704,7 +1706,7 @@ function getOptions( // Generates an optimistic account ID for new users not yet saved in Onyx const optimisticAccountID = UserUtils.generateAccountID(searchValue); const personalDetailsExtended = { - ...personalDetails, + ...allPersonalDetails, [optimisticAccountID]: { accountID: optimisticAccountID, login: searchValue, @@ -1772,14 +1774,7 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions( - options: { - reports: ReportOption[]; - personalDetails: ReportOption[]; - }, - searchValue = '', - betas: Beta[] = [], -): GetOptions { +function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); const optionList = getOptions(options, { @@ -1804,14 +1799,7 @@ function getSearchOptions( return optionList; } -function getShareLogOptions( - options: { - reports: ReportOption[]; - personalDetails: ReportOption[]; - }, - searchValue = '', - betas: Beta[] = [], -): GetOptions { +function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { return getOptions(options, { betas, searchInputValue: searchValue.trim(), @@ -2132,4 +2120,4 @@ export { createOptionList, }; -export type {MemberForList, CategorySection, GetOptions}; +export type {MemberForList, CategorySection, GetOptions, OptionList}; diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index 8d2080995dd2..0fec77506793 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -1,7 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxProvider'; @@ -31,9 +31,6 @@ type SearchPageOnyxProps = { /** Beta features list */ betas: OnyxEntry; - /** All reports shared with the user */ - reports: OnyxCollection; - /** Whether or not we are searching for reports on the server */ isSearchingForReports: OnyxEntry; }; @@ -55,7 +52,7 @@ const setPerformanceTimersEnd = () => { const SearchPageFooterInstance = ; -function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchPageProps) { +function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -89,8 +86,8 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP headerMessage: '', }; } - const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas); - const header = OptionsListUtils.getHeaderMessage(options.reports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); + const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas ?? []); + const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), debouncedSearchValue); return {...optionList, headerMessage: header}; }, [areOptionsInitialized, isScreenTransitionEnd, options, debouncedSearchValue, betas]); @@ -166,7 +163,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP /> - sections={didScreenTransitionEnd && isOptionsDataReady ? sections : CONST.EMPTY_ARRAY} + sections={(areOptionsInitialized || didScreenTransitionEnd) && isOptionsDataReady ? sections : CONST.EMPTY_ARRAY} ListItem={UserListItem} textInputValue={searchValue} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} @@ -176,7 +173,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP onLayout={setPerformanceTimersEnd} autoFocus onSelectRow={selectReport} - showLoadingPlaceholder={!didScreenTransitionEnd || !isOptionsDataReady} + showLoadingPlaceholder={(!areOptionsInitialized && !didScreenTransitionEnd) || !isOptionsDataReady} // showLoadingPlaceholder={(!areOptionsInitialized && !didScreenTransitionEnd) || !isOptionsDataReady} footerContent={SearchPageFooterInstance} isLoadingNewOptions={isSearchingForReports ?? undefined} /> @@ -190,9 +187,6 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP SearchPage.displayName = 'SearchPage'; export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, betas: { key: ONYXKEYS.BETAS, }, From 2c7d2313828ba9c9190fd5edb41bd16c734ae24e Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 12 Mar 2024 14:23:56 +0100 Subject: [PATCH 05/41] update options list utils --- src/libs/OptionsListUtils.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 95c99fa2f890..b6804d0a6ff1 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1500,6 +1500,7 @@ function getOptions( // Filter out all the reports that shouldn't be displayed const filteredReportOptions = options.reports.filter((option) => { const report = option.item as Report; + const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null; @@ -1583,8 +1584,6 @@ function getOptions( return option; }); - // HERE - let allPersonalDetailsOptions = options.personalDetails; if (sortPersonalDetailsByAlphaAsc) { @@ -1611,6 +1610,11 @@ function getOptions( if (includeRecentReports) { for (const reportOption of allReportOptions) { + // update the alternate text if needed + if ((!!reportOption.isChatRoom || reportOption.isPolicyExpenseChat) && forcePolicyNamePreview) { + reportOption.alternateText = ReportUtils.getChatRoomSubtitle(reportOption.item as Report); + } + // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { break; From b1bd11fa5b43ab4419376511fa6037d68dc62a59 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 12 Mar 2024 14:29:53 +0100 Subject: [PATCH 06/41] create reusable initializer for search pages --- src/components/OptionListContextProvider.tsx | 18 ++++++++++++++++-- src/pages/SearchPage/index.tsx | 8 ++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 36ce61ec615f..15f63eccda72 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -1,4 +1,4 @@ -import React, {createContext, useCallback, useContext, useMemo, useRef, useState} from 'react'; +import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import type {OptionList} from '@libs/OptionsListUtils'; import {usePersonalDetails, useReports} from './OnyxProvider'; @@ -58,4 +58,18 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { const useOptionsListContext = () => useContext(OptionsListContext); -export {OptionsListContextProvider, useOptionsListContext}; +// Hook to use the OptionsListContext with an initializer to load the options +const useOptionsList = () => { + const {initializeOptions, ...optionsListContext} = useOptionsListContext(); + + useEffect(() => { + initializeOptions(); + }, [initializeOptions, optionsListContext]); + + return { + initializeOptions, + ...optionsListContext, + }; +}; + +export {OptionsListContextProvider, useOptionsListContext, useOptionsList}; diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index 0fec77506793..b588fa88d0e4 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -5,7 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxProvider'; -import {useOptionsListContext} from '@components/OptionListContextProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -58,7 +58,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); const personalDetails = usePersonalDetails(); - const {options, initializeOptions, areOptionsInitialized} = useOptionsListContext(); + const {options, areOptionsInitialized} = useOptionsList(); const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); @@ -91,10 +91,6 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) return {...optionList, headerMessage: header}; }, [areOptionsInitialized, isScreenTransitionEnd, options, debouncedSearchValue, betas]); - useEffect(() => { - initializeOptions(); - }, [initializeOptions]); - const sections = useMemo((): SearchPageSectionList => { const newSections: SearchPageSectionList = []; let indexOffset = 0; From 499eb8406364e1432546eed700d7411a86b09f02 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 12 Mar 2024 14:42:31 +0100 Subject: [PATCH 07/41] add options to new chat page --- src/pages/NewChatPage.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 72393e89ae1a..807df128a590 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -4,6 +4,7 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; +import {useOptionsList} from '@components/OptionListContextProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; @@ -48,6 +49,7 @@ const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== C function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { const {translate} = useLocalize(); + const {options} = useOptionsList(); const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); const [filteredRecentReports, setFilteredRecentReports] = useState([]); @@ -131,8 +133,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF personalDetails: newChatPersonalDetails, userToInvite, } = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports ?? [], + options.personalDetails ?? [], betas ?? [], searchTerm, newSelectedOptions, @@ -185,8 +187,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF personalDetails: newChatPersonalDetails, userToInvite, } = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports ?? [], + options.personalDetails ?? [], betas ?? [], searchTerm, selectedOptions, From 122a6fb97bd8ec04a7b79618f32161077fb40db8 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 12 Mar 2024 15:35:15 +0100 Subject: [PATCH 08/41] update other occurencies of getoptions --- src/components/CategoryPicker.tsx | 4 +-- src/components/TaxPicker.tsx | 2 +- src/libs/OptionsListUtils.ts | 2 +- src/pages/RoomInvitePage.tsx | 4 ++- .../ShareLogList/BaseShareLogList.tsx | 8 +++-- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 19 +++++------- .../TaskShareDestinationSelectorModal.tsx | 29 ++++++++----------- src/pages/workspace/WorkspaceInvitePage.tsx | 4 ++- 8 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 3033bf118e8f..0307b67114e5 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -47,8 +47,8 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC const [sections, headerMessage, shouldShowTextInput] = useMemo(() => { const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter((p) => !isEmptyObject(p)); const {categoryOptions} = OptionsListUtils.getFilteredOptions( - {}, - {}, + [], + [], [], debouncedSearchValue, selectedOptions, diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index 664aa741c400..5e65a2c940be 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -52,7 +52,7 @@ function TaxPicker({selectedTaxRate = '', taxRates, insets, onSubmit}: TaxPicker }, [selectedTaxRate]); const sections = useMemo(() => { - const {taxRatesOptions} = OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], false, {}, [], false, false, true, taxRates); + const {taxRatesOptions} = OptionsListUtils.getFilteredOptions([], [], [], searchValue, selectedOptions, [], false, false, false, {}, [], false, {}, [], false, false, true, taxRates); return taxRatesOptions; }, [taxRates, searchValue, selectedOptions]); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b6804d0a6ff1..3987d6ebe626 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2124,4 +2124,4 @@ export { createOptionList, }; -export type {MemberForList, CategorySection, GetOptions, OptionList}; +export type {MemberForList, CategorySection, GetOptions, OptionList, ReportOption}; diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 7bcd64397e20..1c0597a34ed9 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -9,6 +9,7 @@ import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {Section} from '@components/SelectionList/types'; @@ -51,6 +52,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa const [userToInvite, setUserToInvite] = useState(null); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const navigation: StackNavigationProp = useNavigation(); + const {options} = useOptionsList(); // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers = useMemo( @@ -62,7 +64,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa ); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetails, betas ?? [], searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers); // Update selectedOptions with the latest personalDetails information const detailsMap: Record = {}; diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 1a7b23477349..d200e5312016 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -17,7 +18,7 @@ import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import type {BaseShareLogListOnyxProps, BaseShareLogListProps} from './types'; -function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogListProps) { +function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { const [searchValue, setSearchValue] = useState(''); const [searchOptions, setSearchOptions] = useState>({ recentReports: [], @@ -30,20 +31,21 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis const styles = useThemeStyles(); const isMounted = useRef(false); const personalDetails = usePersonalDetails(); + const {options} = useOptionsList(); const updateOptions = useCallback(() => { const { recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, - } = OptionsListUtils.getShareLogOptions(reports, personalDetails, searchValue.trim(), betas ?? []); + } = OptionsListUtils.getShareLogOptions(options, searchValue.trim(), betas ?? []); setSearchOptions({ recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, }); - }, [betas, personalDetails, reports, searchValue]); + }, [betas, options, searchValue]); const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 0ffb33b7590b..ef097ffcfd71 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -7,7 +7,8 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useBetas, usePersonalDetails, useSession} from '@components/OnyxProvider'; +import {useBetas, useSession} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; @@ -37,22 +38,18 @@ type TaskAssigneeSelectorModalOnyxProps = { task: OnyxEntry; }; -type UseOptions = { - reports: OnyxCollection; -}; - type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps; -function useOptions({reports}: UseOptions) { - const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; +function useOptions() { const betas = useBetas(); const [isLoading, setIsLoading] = useState(true); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const {options: optionsList} = useOptionsList(); const options = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( - reports, - allPersonalDetails, + optionsList.reports, + optionsList.personalDetails, betas, debouncedSearchValue.trim(), [], @@ -85,7 +82,7 @@ function useOptions({reports}: UseOptions) { currentUserOption, headerMessage, }; - }, [debouncedSearchValue, allPersonalDetails, isLoading, betas, reports]); + }, [optionsList.reports, optionsList.personalDetails, betas, debouncedSearchValue, isLoading]); return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; } @@ -96,7 +93,7 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro const {translate} = useLocalize(); const session = useSession(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports}); + const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions(); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 5b56e58752ac..4d3b9d8a573b 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,9 +1,9 @@ import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -22,8 +22,6 @@ import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; type TaskShareDestinationSelectorModalOnyxProps = { - reports: OnyxCollection; - isSearchingForReports: OnyxEntry; }; @@ -40,28 +38,28 @@ const selectReportHandler = (option: unknown) => { Navigation.goBack(ROUTES.NEW_TASK); }; -const reportFilter = (reports: OnyxCollection) => - Object.keys(reports ?? {}).reduce((filtered, reportKey) => { - const report: OnyxEntry = reports?.[reportKey] ?? null; +const reportFilter = (reportOptions: OptionsListUtils.ReportOption[]) => + (reportOptions ?? []).reduce((filtered: OptionsListUtils.ReportOption[], option) => { + const report = option.item as Report; if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { - return {...filtered, [reportKey]: report}; + filtered.push(option); } return filtered; - }, {}); + }, []); -function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: TaskShareDestinationSelectorModalProps) { +function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDestinationSelectorModalProps) { const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); - const personalDetails = usePersonalDetails(); const {isOffline} = useNetwork(); + const {options: optionList} = useOptionsList(); const textInputHint = useMemo(() => (isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''), [isOffline, translate]); const options = useMemo(() => { - const filteredReports = reportFilter(reports); + const filteredReports = reportFilter(optionList.reports); - const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); + const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, optionList.personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue); @@ -84,7 +82,7 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: Tas : []; return {sections, headerMessage}; - }, [personalDetails, reports, debouncedSearchValue]); + }, [optionList.reports, optionList.personalDetails, debouncedSearchValue]); useEffect(() => { ReportActions.searchInServer(debouncedSearchValue); @@ -124,9 +122,6 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: Tas TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal'; export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, isSearchingForReports: { key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 67bf6f8064da..7984a47bee35 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -8,6 +8,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {Section} from '@components/SelectionList/types'; @@ -72,6 +73,7 @@ function WorkspaceInvitePage({ const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; + const {options} = useOptionsList(); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -107,7 +109,7 @@ function WorkspaceInvitePage({ const newPersonalDetailsDict: Record = {}; const newSelectedOptionsDict: Record = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap: Record = {}; From 4c0cb3eb75f47ce1c638d23b42b3ea25bd52af7a Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 13 Mar 2024 09:52:13 +0100 Subject: [PATCH 09/41] update initializing in search page --- src/components/OptionListContextProvider.tsx | 11 ++++++--- src/pages/SearchPage/index.tsx | 24 +++++++++----------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 15f63eccda72..dbaafa7f6215 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -45,8 +45,8 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { return; } - areOptionsInitialized.current = true; loadOptions(); + areOptionsInitialized.current = true; }, [loadOptions]); return ( @@ -59,12 +59,17 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { const useOptionsListContext = () => useContext(OptionsListContext); // Hook to use the OptionsListContext with an initializer to load the options -const useOptionsList = () => { +const useOptionsList = (options?: {shouldInitialize: boolean}) => { + const {shouldInitialize = true} = options ?? {}; const {initializeOptions, ...optionsListContext} = useOptionsListContext(); useEffect(() => { + if (!shouldInitialize || optionsListContext.areOptionsInitialized) { + return; + } + initializeOptions(); - }, [initializeOptions, optionsListContext]); + }, [shouldInitialize, initializeOptions, optionsListContext.areOptionsInitialized]); return { initializeOptions, diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index b588fa88d0e4..3374a6f410c2 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -4,7 +4,6 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -18,7 +17,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {RootStackParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; -import * as ReportUtils from '@libs/ReportUtils'; +import type {OptionData} from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -38,7 +37,7 @@ type SearchPageOnyxProps = { type SearchPageProps = SearchPageOnyxProps & StackScreenProps; type SearchPageSectionItem = { - data: ReportUtils.OptionData[]; + data: OptionData[]; shouldShow: boolean; indexOffset: number; }; @@ -57,8 +56,9 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) const {translate} = useLocalize(); const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); - const personalDetails = usePersonalDetails(); - const {options, areOptionsInitialized} = useOptionsList(); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: isScreenTransitionEnd, + }); const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); @@ -78,7 +78,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) userToInvite, headerMessage, } = useMemo(() => { - if (!areOptionsInitialized && !isScreenTransitionEnd) { + if (!isScreenTransitionEnd) { return { recentReports: [], personalDetails: [], @@ -89,7 +89,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas ?? []); const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), debouncedSearchValue); return {...optionList, headerMessage: header}; - }, [areOptionsInitialized, isScreenTransitionEnd, options, debouncedSearchValue, betas]); + }, [isScreenTransitionEnd, options, debouncedSearchValue, betas]); const sections = useMemo((): SearchPageSectionList => { const newSections: SearchPageSectionList = []; @@ -124,7 +124,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) return newSections; }, [localPersonalDetails, recentReports, userToInvite]); - const selectReport = (option: ReportUtils.OptionData) => { + const selectReport = (option: OptionData) => { if (!option) { return; } @@ -141,8 +141,6 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) setIsScreenTransitionEnd(true); }; - const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - return ( - - sections={(areOptionsInitialized || didScreenTransitionEnd) && isOptionsDataReady ? sections : CONST.EMPTY_ARRAY} + + sections={didScreenTransitionEnd || areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} ListItem={UserListItem} textInputValue={searchValue} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} @@ -169,7 +167,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) onLayout={setPerformanceTimersEnd} autoFocus onSelectRow={selectReport} - showLoadingPlaceholder={(!areOptionsInitialized && !didScreenTransitionEnd) || !isOptionsDataReady} // showLoadingPlaceholder={(!areOptionsInitialized && !didScreenTransitionEnd) || !isOptionsDataReady} + showLoadingPlaceholder={!didScreenTransitionEnd} footerContent={SearchPageFooterInstance} isLoadingNewOptions={isSearchingForReports ?? undefined} /> From 0a7925e747fcc406269572a7eee3dc7e3cf88d70 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 13 Mar 2024 10:08:29 +0100 Subject: [PATCH 10/41] update types for options --- src/libs/OptionsListUtils.ts | 38 +++++++++++-------- .../TaskShareDestinationSelectorModal.tsx | 6 +-- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3987d6ebe626..d0b79d752c74 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -53,13 +53,13 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type ReportOption = ReportUtils.OptionData & { - item: Report | PersonalDetails; +type SearchOption = ReportUtils.OptionData & { + item: T; }; type OptionList = { - reports: ReportOption[]; - personalDetails: ReportOption[]; + reports: Array>; + personalDetails: Array>; }; type Tag = { @@ -1351,7 +1351,7 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx return report?.lastVisibleActionCreated; }); orderedReports.reverse(); - const allReportOptions: ReportOption[] = []; + const allReportOptions: Array> = []; orderedReports.forEach((report) => { if (!report) { @@ -1407,7 +1407,7 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx return { reports: allReportOptions, - personalDetails: allPersonalDetailsOptions as ReportOption[], + personalDetails: allPersonalDetailsOptions as Array>, }; } @@ -1499,7 +1499,7 @@ function getOptions( // Filter out all the reports that shouldn't be displayed const filteredReportOptions = options.reports.filter((option) => { - const report = option.item as Report; + const report = option.item; const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; @@ -1523,7 +1523,7 @@ function getOptions( // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom const orderedReportOptions = lodashSortBy(filteredReportOptions, (option) => { - const report = option.item as Report; + const report = option.item; if (ReportUtils.isArchivedRoom(report)) { return CONST.DATE.UNIX_EPOCH; } @@ -1533,7 +1533,7 @@ function getOptions( orderedReportOptions.reverse(); const allReportOptions = orderedReportOptions.filter((option) => { - const report = option.item as Report; + const report = option.item; if (!report) { return; @@ -1612,7 +1612,7 @@ function getOptions( for (const reportOption of allReportOptions) { // update the alternate text if needed if ((!!reportOption.isChatRoom || reportOption.isPolicyExpenseChat) && forcePolicyNamePreview) { - reportOption.alternateText = ReportUtils.getChatRoomSubtitle(reportOption.item as Report); + reportOption.alternateText = ReportUtils.getChatRoomSubtitle(reportOption.item); } // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value @@ -1853,8 +1853,8 @@ function getIOUConfirmationOptionsFromParticipants(participants: Participant[], * Build the options for the New Group view */ function getFilteredOptions( - reports: ReportOption[] = [], - personalDetails: ReportOption[] = [], + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1905,8 +1905,8 @@ function getFilteredOptions( */ function getShareDestinationOptions( - reports: ReportOption[], - personalDetails: ReportOption[], + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1966,7 +1966,13 @@ function formatMemberForList(member: ReportUtils.OptionData): MemberForList { /** * Build the options for the Workspace Member Invite view */ -function getMemberInviteOptions(personalDetails: ReportOption[], betas: Beta[] = [], searchValue = '', excludeLogins: string[] = [], includeSelectedOptions = false): GetOptions { +function getMemberInviteOptions( + personalDetails: Array>, + betas: Beta[] = [], + searchValue = '', + excludeLogins: string[] = [], + includeSelectedOptions = false, +): GetOptions { return getOptions( {reports: [], personalDetails}, { @@ -2124,4 +2130,4 @@ export { createOptionList, }; -export type {MemberForList, CategorySection, GetOptions, OptionList, ReportOption}; +export type {MemberForList, CategorySection, GetOptions, OptionList, SearchOption}; diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 4d3b9d8a573b..bf640fe5982a 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -38,9 +38,9 @@ const selectReportHandler = (option: unknown) => { Navigation.goBack(ROUTES.NEW_TASK); }; -const reportFilter = (reportOptions: OptionsListUtils.ReportOption[]) => - (reportOptions ?? []).reduce((filtered: OptionsListUtils.ReportOption[], option) => { - const report = option.item as Report; +const reportFilter = (reportOptions: Array>) => + (reportOptions ?? []).reduce((filtered: Array>, option) => { + const report = option.item; if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { filtered.push(option); } From 47795b96cc535193e6499132db672d7da702591a Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 13 Mar 2024 12:28:15 +0100 Subject: [PATCH 11/41] update loading in search page --- src/pages/SearchPage/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index 3374a6f410c2..6e06024521fd 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -78,7 +78,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) userToInvite, headerMessage, } = useMemo(() => { - if (!isScreenTransitionEnd) { + if (!areOptionsInitialized) { return { recentReports: [], personalDetails: [], @@ -89,7 +89,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas ?? []); const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), debouncedSearchValue); return {...optionList, headerMessage: header}; - }, [isScreenTransitionEnd, options, debouncedSearchValue, betas]); + }, [areOptionsInitialized, options, debouncedSearchValue, betas]); const sections = useMemo((): SearchPageSectionList => { const newSections: SearchPageSectionList = []; @@ -157,7 +157,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) /> - sections={didScreenTransitionEnd || areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} + sections={(!areOptionsInitialized && didScreenTransitionEnd) || areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} ListItem={UserListItem} textInputValue={searchValue} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} @@ -167,7 +167,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) onLayout={setPerformanceTimersEnd} autoFocus onSelectRow={selectReport} - showLoadingPlaceholder={!didScreenTransitionEnd} + showLoadingPlaceholder={areOptionsInitialized && debouncedSearchValue.trim() === '' ? sections.length === 0 : !didScreenTransitionEnd} footerContent={SearchPageFooterInstance} isLoadingNewOptions={isSearchingForReports ?? undefined} /> From ffaee2a78035b85cd78c54fe0e3db6a83c28ab13 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 13 Mar 2024 12:47:40 +0100 Subject: [PATCH 12/41] update new chat page skeleton displaying --- src/pages/NewChatPage.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 807df128a590..ab24d641fe90 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -16,7 +16,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import doInteractionTask from '@libs/DoInteractionTask'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; @@ -49,12 +48,12 @@ const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== C function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { const {translate} = useLocalize(); - const {options} = useOptionsList(); + const {options, areOptionsInitialized} = useOptionsList(); const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); - const [filteredRecentReports, setFilteredRecentReports] = useState([]); - const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); - const [filteredUserToInvite, setFilteredUserToInvite] = useState(); + const [filteredRecentReports, setFilteredRecentReports] = useState([]); + const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); + const [filteredUserToInvite, setFilteredUserToInvite] = useState(); const [selectedOptions, setSelectedOptions] = useState([]); const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -71,8 +70,6 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF selectedOptions.some((participant) => participant?.searchText?.toLowerCase().includes(searchTerm.trim().toLowerCase())), ); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - const sections = useMemo((): OptionsListUtils.CategorySection[] => { const sectionsList: OptionsListUtils.CategorySection[] = []; let indexOffset = 0; @@ -265,7 +262,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF headerMessage={headerMessage} boldStyle shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - shouldShowOptions={isOptionsDataReady && didScreenTransitionEnd} + shouldShowOptions={areOptionsInitialized && didScreenTransitionEnd} shouldShowConfirmButton shouldShowReferralCTA={!dismissedReferralBanners[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} From 47fc6795159d2cddebb3db0694d32bfb9f7a6b2a Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 13 Mar 2024 14:48:38 +0100 Subject: [PATCH 13/41] update caching in all selection lists --- src/pages/NewChatPage.tsx | 6 +- src/pages/RoomInvitePage.tsx | 4 +- .../ShareLogList/BaseShareLogList.tsx | 15 ++- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 21 ++-- .../TaskShareDestinationSelectorModal.tsx | 20 +++- src/pages/workspace/WorkspaceInvitePage.tsx | 110 +++++++++--------- 6 files changed, 94 insertions(+), 82 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index ab24d641fe90..9523c31298e3 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -47,8 +47,11 @@ type NewChatPageProps = NewChatPageWithOnyxProps & { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { + const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); - const {options, areOptionsInitialized} = useOptionsList(); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: isScreenTransitionEnd, + }); const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); const [filteredRecentReports, setFilteredRecentReports] = useState([]); @@ -238,6 +241,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF includePaddingTop={false} shouldEnableMaxHeight testID={NewChatPage.displayName} + onEntryTransitionEnd={() => setIsScreenTransitionEnd(true)} > {({safeAreaPaddingBottomStyle, insets}) => ( (null); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const navigation: StackNavigationProp = useNavigation(); - const {options} = useOptionsList(); + const {options, areOptionsInitialized} = useOptionsList(); // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers = useMemo( @@ -239,7 +239,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa onConfirm={inviteUsers} showScrollIndicator shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(personalDetails)} + showLoadingPlaceholder={!didScreenTransitionEnd && !areOptionsInitialized} /> { const { @@ -47,12 +44,14 @@ function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { }); }, [betas, options, searchValue]); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - useEffect(() => { + if (!areOptionsInitialized) { + return; + } + updateOptions(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [areOptionsInitialized]); useEffect(() => { if (!isMounted.current) { @@ -134,7 +133,7 @@ function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { value={searchValue} headerMessage={headerMessage} showTitleTooltip - shouldShowOptions={isOptionsDataReady} + shouldShowOptions={areOptionsInitialized} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index ef097ffcfd71..d367a06cb60e 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -41,10 +41,12 @@ type TaskAssigneeSelectorModalOnyxProps = { type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps; function useOptions() { + const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const betas = useBetas(); - const [isLoading, setIsLoading] = useState(true); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const {options: optionsList} = useOptionsList(); + const {options: optionsList, areOptionsInitialized} = useOptionsList({ + shouldInitialize: isScreenTransitionEnd, + }); const options = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( @@ -71,10 +73,6 @@ function useOptions() { debouncedSearchValue, ); - if (isLoading) { - setIsLoading(false); - } - return { userToInvite, recentReports, @@ -82,9 +80,9 @@ function useOptions() { currentUserOption, headerMessage, }; - }, [optionsList.reports, optionsList.personalDetails, betas, debouncedSearchValue, isLoading]); + }, [optionsList.reports, optionsList.personalDetails, betas, debouncedSearchValue]); - return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; + return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, setIsScreenTransitionEnd}; } function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) { @@ -93,7 +91,7 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro const {translate} = useLocalize(); const session = useSession(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions(); + const {userToInvite, recentReports, personalDetails, currentUserOption, searchValue, setSearchValue, headerMessage, areOptionsInitialized, setIsScreenTransitionEnd} = useOptions(); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); @@ -208,6 +206,7 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro setIsScreenTransitionEnd(true)} > {({didScreenTransitionEnd}) => ( @@ -217,14 +216,14 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro /> diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index bf640fe5982a..0c31058fe329 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -48,15 +48,24 @@ const reportFilter = (reportOptions: Array }, []); function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDestinationSelectorModalProps) { + const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {options: optionList} = useOptionsList(); + const {options: optionList, areOptionsInitialized} = useOptionsList({ + shouldInitialize: isScreenTransitionEnd, + }); const textInputHint = useMemo(() => (isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''), [isOffline, translate]); const options = useMemo(() => { + if (!areOptionsInitialized) { + return { + sections: [], + headerMessage: '', + }; + } const filteredReports = reportFilter(optionList.reports); const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, optionList.personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); @@ -82,7 +91,7 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes : []; return {sections, headerMessage}; - }, [optionList.reports, optionList.personalDetails, debouncedSearchValue]); + }, [areOptionsInitialized, optionList.reports, optionList.personalDetails, debouncedSearchValue]); useEffect(() => { ReportActions.searchInServer(debouncedSearchValue); @@ -92,6 +101,7 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes setIsScreenTransitionEnd(true)} > {({didScreenTransitionEnd}) => ( <> @@ -102,13 +112,13 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 7984a47bee35..50a5301160a5 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -67,13 +67,15 @@ function WorkspaceInvitePage({ const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const navigation = useNavigation>(); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; - const {options} = useOptionsList(); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: isScreenTransitionEnd, + }); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -90,7 +92,7 @@ function WorkspaceInvitePage({ useEffect(() => { const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { - setDidScreenTransitionEnd(true); + setIsScreenTransitionEnd(true); }); return () => { @@ -110,7 +112,6 @@ function WorkspaceInvitePage({ const newSelectedOptionsDict: Record = {}; const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers, true); - // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { @@ -161,16 +162,12 @@ function WorkspaceInvitePage({ setSelectedOptions(Object.values(newSelectedOptionsDict)); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); + }, [options.personalDetails, policyMembers, betas, searchTerm, excludedUsers]); const sections: MembersSection[] = useMemo(() => { const sectionsArr: MembersSection[] = []; let indexOffset = 0; - if (!didScreenTransitionEnd) { - return []; - } - // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; if (searchTerm !== '') { @@ -220,7 +217,7 @@ function WorkspaceInvitePage({ }); return sectionsArr; - }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); + }, [selectedOptions, searchTerm, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { Policy.clearErrors(route.params.policyID); @@ -290,53 +287,56 @@ function WorkspaceInvitePage({ shouldEnableMaxHeight shouldUseCachedViewportHeight testID={WorkspaceInvitePage.displayName} + onEntryTransitionEnd={() => setIsScreenTransitionEnd(true)} > - - { - Policy.clearErrors(route.params.policyID); - Navigation.goBack(); - }} - /> - { - SearchInputManager.searchInput = value; - setSearchTerm(value); - }} - headerMessage={headerMessage} - onSelectRow={toggleOption} - onConfirm={inviteUser} - showScrollIndicator - showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(personalDetailsProp)} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - /> - - ( + + { + Policy.clearErrors(route.params.policyID); + Navigation.goBack(); + }} + /> + { + SearchInputManager.searchInput = value; + setSearchTerm(value); + }} + headerMessage={headerMessage} + onSelectRow={toggleOption} + onConfirm={inviteUser} + showScrollIndicator + showLoadingPlaceholder={areOptionsInitialized && searchTerm.trim() === '' ? sections.length === 0 : !didScreenTransitionEnd} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} /> - - + + + + + )} ); } From 623810f5bc08b2e6b2a24552ee30f84ee5f1809d Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 13 Mar 2024 15:41:04 +0100 Subject: [PATCH 14/41] update options in the background --- src/App.tsx | 2 +- src/components/OptionListContextProvider.tsx | 45 +++++++++++++++++--- src/libs/OptionsListUtils.ts | 19 +++++++++ src/libs/ReportUtils.ts | 18 ++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e53ba387d4d6..23ee2f879136 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,7 @@ import ErrorBoundary from './components/ErrorBoundary'; import HTMLEngineProvider from './components/HTMLEngineProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; -import {OptionsListContextProvider} from './components/OptionListContextProvider'; +import OptionsListContextProvider from './components/OptionListContextProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index dbaafa7f6215..ffc72d684f64 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -1,7 +1,12 @@ import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import type {OptionList} from '@libs/OptionsListUtils'; -import {usePersonalDetails, useReports} from './OnyxProvider'; +import * as ReportUtils from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; +import {usePersonalDetails} from './OnyxProvider'; type OptionsListContextProps = { options: OptionList; @@ -9,7 +14,11 @@ type OptionsListContextProps = { areOptionsInitialized: boolean; }; -type OptionsListProviderProps = { +type OptionsListProviderOnyxProps = { + reports: OnyxCollection; +}; + +type OptionsListProviderProps = OptionsListProviderOnyxProps & { /** Actual content wrapped by this component */ children: React.ReactNode; }; @@ -23,14 +32,34 @@ const OptionsListContext = createContext({ areOptionsInitialized: false, }); -function OptionsListContextProvider({children}: OptionsListProviderProps) { +function OptionsListContextProvider({reports, children}: OptionsListProviderProps) { const areOptionsInitialized = useRef(false); const [options, setOptions] = useState({ reports: [], personalDetails: [], }); const personalDetails = usePersonalDetails(); - const reports = useReports(); + + useEffect(() => { + // there is no need to update the options if the options are not initialized + if (!areOptionsInitialized.current) { + return; + } + + const lastUpdatedReport = ReportUtils.getLastUpdatedReport(); + const newOption = OptionsListUtils.createOptionFromReport(lastUpdatedReport, personalDetails); + const replaceIndex = options.reports.findIndex((option) => option.reportID === lastUpdatedReport.reportID); + + if (replaceIndex === undefined) { + return; + } + + setOptions((prevOptions) => { + const newOptions = {...prevOptions}; + newOptions.reports[replaceIndex] = newOption; + return newOptions; + }); + }, [options.reports, personalDetails, reports]); const loadOptions = useCallback(() => { const optionLists = OptionsListUtils.createOptionList(reports, personalDetails); @@ -77,4 +106,10 @@ const useOptionsList = (options?: {shouldInitialize: boolean}) => { }; }; -export {OptionsListContextProvider, useOptionsListContext, useOptionsList}; +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, +})(OptionsListContextProvider); + +export {useOptionsListContext, useOptionsList}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index c4e9c1c28816..2288cff7a254 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1422,6 +1422,24 @@ function createOptionList(reports: OnyxCollection, personalDetails: Onyx }; } +function createOptionFromReport(report: Report, personalDetails: OnyxEntry) { + const accountIDs = report.participantAccountIDs ?? []; + + return { + item: report, + ...createOption( + accountIDs, + personalDetails, + report, + {}, + { + showChatPreviewLine: true, + forcePolicyNamePreview: true, + }, + ), + }; +} + /** * filter options based on specific conditions */ @@ -2139,6 +2157,7 @@ export { transformedTaxRates, getShareLogOptions, createOptionList, + createOptionFromReport, }; export type {MemberForList, CategorySection, GetOptions, OptionList, SearchOption, PayeePersonalDetails}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8dc1c9967f13..1362974df4f4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -521,6 +521,23 @@ Onyx.connect({ }, }); +let lastUpdatedReport: Report; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + callback: (value) => { + if (!value) { + return; + } + + lastUpdatedReport = value; + }, +}); + +function getLastUpdatedReport() { + return lastUpdatedReport; +} + function getCurrentUserAvatarOrDefault(): UserUtils.AvatarSource { return currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID); } @@ -5497,6 +5514,7 @@ export { isJoinRequestInAdminRoom, canAddOrDeleteTransactions, shouldCreateNewMoneyRequestReport, + getLastUpdatedReport, }; export type { From d4621e9757e53247d067ee8d2b0a3c92689354ec Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 14 Mar 2024 09:33:03 +0100 Subject: [PATCH 15/41] update changed personal details --- src/components/OptionListContextProvider.tsx | 21 ++++- src/libs/OptionsListUtils.ts | 85 ++++++++++---------- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index ffc72d684f64..5ee76a23c3ef 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -59,10 +59,27 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp newOptions.reports[replaceIndex] = newOption; return newOptions; }); - }, [options.reports, personalDetails, reports]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reports]); + + useEffect(() => { + // there is no need to update the options if the options are not initialized + if (!areOptionsInitialized.current) { + return; + } + + // since personal details are not a collection, we need to recreate the whole list from scratch + const newPersonalDetailsOptions = OptionsListUtils.createOptionList(personalDetails).personalDetails; + + setOptions((prevOptions) => { + const newOptions = {...prevOptions}; + newOptions.personalDetails = newPersonalDetailsOptions; + return newOptions; + }); + }, [personalDetails]); const loadOptions = useCallback(() => { - const optionLists = OptionsListUtils.createOptionList(reports, personalDetails); + const optionLists = OptionsListUtils.createOptionList(personalDetails, reports); setOptions({ reports: optionLists.reports, personalDetails: optionLists.personalDetails, diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 2288cff7a254..1958015097f4 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1349,55 +1349,58 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } -function createOptionList(reports: OnyxCollection, personalDetails: OnyxEntry) { +function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection) { const reportMapForAccountIDs: Record = {}; - // Sorting the reports works like this: - // - Order everything by the last message timestamp (descending) - // - All archived reports should remain at the bottom - const orderedReports = lodashSortBy(reports, (report) => { - if (ReportUtils.isArchivedRoom(report)) { - return CONST.DATE.UNIX_EPOCH; - } - - return report?.lastVisibleActionCreated; - }); - orderedReports.reverse(); const allReportOptions: Array> = []; - orderedReports.forEach((report) => { - if (!report) { - return; - } + if (reports) { + // Sorting the reports works like this: + // - Order everything by the last message timestamp (descending) + // - All archived reports should remain at the bottom + const orderedReports = lodashSortBy(reports, (report) => { + if (ReportUtils.isArchivedRoom(report)) { + return CONST.DATE.UNIX_EPOCH; + } - const isSelfDM = ReportUtils.isSelfDM(report); - // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. - const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; + return report?.lastVisibleActionCreated; + }); + orderedReports.reverse(); - if (!accountIDs || accountIDs.length === 0) { - return; - } + orderedReports.forEach((report) => { + if (!report) { + return; + } - // 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) { - reportMapForAccountIDs[accountIDs[0]] = report; - } + const isSelfDM = ReportUtils.isSelfDM(report); + // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. + const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; - allReportOptions.push({ - item: report, - ...createOption( - accountIDs, - personalDetails, - report, - {}, - { - showChatPreviewLine: true, - forcePolicyNamePreview: true, - }, - ), + if (!accountIDs || accountIDs.length === 0) { + return; + } + + // 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) { + reportMapForAccountIDs[accountIDs[0]] = report; + } + + allReportOptions.push({ + item: report, + ...createOption( + accountIDs, + personalDetails, + report, + {}, + { + showChatPreviewLine: true, + forcePolicyNamePreview: true, + }, + ), + }); }); - }); + } const havingLoginPersonalDetails = Object.fromEntries( Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail), From 653c9cb99263ff38ab53474fcd85b50dd8f4cee3 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 14 Mar 2024 09:54:51 +0100 Subject: [PATCH 16/41] fix loader on new chat page --- src/pages/NewChatPage.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 9523c31298e3..36d51533b917 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -52,6 +52,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: isScreenTransitionEnd, }); + const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); const [filteredRecentReports, setFilteredRecentReports] = useState([]); @@ -60,7 +61,6 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF const [selectedOptions, setSelectedOptions] = useState([]); const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -212,7 +212,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF useEffect(() => { const interactionTask = doInteractionTask(() => { - setDidScreenTransitionEnd(true); + setIsScreenTransitionEnd(true); }); return () => { @@ -225,11 +225,11 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF }, []); useEffect(() => { - if (!didScreenTransitionEnd) { + if (!isScreenTransitionEnd) { return; } updateOptions(); - }, [didScreenTransitionEnd, updateOptions]); + }, [isScreenTransitionEnd, updateOptions]); const {inputCallbackRef} = useAutoFocusInput(); @@ -241,7 +241,6 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF includePaddingTop={false} shouldEnableMaxHeight testID={NewChatPage.displayName} - onEntryTransitionEnd={() => setIsScreenTransitionEnd(true)} > {({safeAreaPaddingBottomStyle, insets}) => ( Date: Thu, 14 Mar 2024 12:25:40 +0100 Subject: [PATCH 17/41] filter personal details with login --- src/libs/OptionsListUtils.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 1958015097f4..6002b8c6ab4a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1402,10 +1402,7 @@ function createOptionList(personalDetails: OnyxEntry, repor }); } - const havingLoginPersonalDetails = Object.fromEntries( - Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail), - ); - const allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => ({ + const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ item: personalDetail, ...createOption( [personalDetail?.accountID ?? -1], @@ -1616,7 +1613,9 @@ function getOptions( return option; }); - let allPersonalDetailsOptions = options.personalDetails; + const havingLoginPersonalDetails = options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail); + + let allPersonalDetailsOptions = havingLoginPersonalDetails; if (sortPersonalDetailsByAlphaAsc) { // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 From 7ad28722bd623417db85048fda9d7481e87bfacf Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 14 Mar 2024 15:54:58 +0100 Subject: [PATCH 18/41] generate alternate text on the fly if needed --- src/libs/OptionsListUtils.ts | 54 ++++++++++++++++++++++------------ src/pages/SearchPage/index.tsx | 1 + 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 6002b8c6ab4a..1f976055086e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -534,6 +534,25 @@ function getLastActorDisplayName(lastActorDetails: Partial | nu : ''; } +/** + * Update alternate text option when applicable + */ +function getAlternateTextOption( + option: ReportUtils.OptionData, + {showChatPreviewLine = false, forcePolicyNamePreview = false, lastMessageTextFromReport = ''}: PreviewConfig & {lastMessageTextFromReport?: string}, +) { + if (!!option.isThread || !!option.isMoneyRequestReport) { + return lastMessageTextFromReport.length > 0 ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + if (!!option.isChatRoom || !!option.isPolicyExpenseChat) { + return showChatPreviewLine && !forcePolicyNamePreview && option.lastMessageText ? option.lastMessageText : option.subtitle; + } + if (option.isTaskReport) { + return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : LocalePhoneNumber.formatPhoneNumber((option.participantsList ?? [])[0].login ?? ''); +} + /** * Get the last message text from the report directly or from other sources for special cases. */ @@ -646,6 +665,7 @@ function createOption( isExpenseReport: false, policyID: undefined, isOptimisticPersonalDetail: false, + lastMessageText: '', }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -699,14 +719,11 @@ function createOption( lastMessageText = `${lastActorDisplayName}: ${lastMessageTextFromReport}`; } - if (result.isThread || result.isMoneyRequestReport) { - result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else if (result.isChatRoom || result.isPolicyExpenseChat) { - result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; - } else if (result.isTaskReport) { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); + result.lastMessageText = lastMessageText; + + // If displaying chat preview line is needed, let's overwrite the default alternate text + if (showChatPreviewLine || forcePolicyNamePreview) { + result.alternateText = getAlternateTextOption(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); } reportName = ReportUtils.getReportName(report); } else { @@ -1394,8 +1411,8 @@ function createOptionList(personalDetails: OnyxEntry, repor report, {}, { - showChatPreviewLine: true, - forcePolicyNamePreview: true, + showChatPreviewLine: false, + forcePolicyNamePreview: false, }, ), }); @@ -1410,8 +1427,8 @@ function createOptionList(personalDetails: OnyxEntry, repor reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, { - showChatPreviewLine: true, - forcePolicyNamePreview: true, + showChatPreviewLine: false, + forcePolicyNamePreview: false, }, ), })); @@ -1433,8 +1450,8 @@ function createOptionFromReport(report: Report, personalDetails: OnyxEntry 0 && recentReportOptions.length === maxRecentReportsToShow) { diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index 6e06024521fd..d08d68c37cdc 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -59,6 +59,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: isScreenTransitionEnd, }); + const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); From 67a0ae29aeb32bd5a9efc3a6c05c80b34a2371c0 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 14 Mar 2024 15:59:48 +0100 Subject: [PATCH 19/41] freeze the option list when search is open --- src/components/OptionListContextProvider.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 5ee76a23c3ef..00cdaa6dc1ac 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -107,19 +107,23 @@ const useOptionsListContext = () => useContext(OptionsListContext); // Hook to use the OptionsListContext with an initializer to load the options const useOptionsList = (options?: {shouldInitialize: boolean}) => { const {shouldInitialize = true} = options ?? {}; - const {initializeOptions, ...optionsListContext} = useOptionsListContext(); + const {initializeOptions, options: optionList, areOptionsInitialized} = useOptionsListContext(); + + // freeze the options, so they won't update when the context updates and the screen is open + const optionsRef = useRef(optionList); useEffect(() => { - if (!shouldInitialize || optionsListContext.areOptionsInitialized) { + if (!shouldInitialize || areOptionsInitialized) { return; } initializeOptions(); - }, [shouldInitialize, initializeOptions, optionsListContext.areOptionsInitialized]); + }, [shouldInitialize, initializeOptions, areOptionsInitialized]); return { initializeOptions, - ...optionsListContext, + options: optionsRef.current, + areOptionsInitialized, }; }; From 47c9e6bbd151dd4d6971b93e50d5a75fe2eff155 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Mar 2024 11:08:04 +0100 Subject: [PATCH 20/41] remove unnecessary code --- src/components/OnyxProvider.tsx | 5 - src/components/OptionListContextProvider.tsx | 13 ++- src/pages/NewChatPage.tsx | 16 +-- .../ShareLogList/BaseShareLogList.tsx | 6 +- src/pages/tasks/NewTaskPage.tsx | 2 +- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 17 +-- .../TaskShareDestinationSelectorModal.tsx | 8 +- src/pages/workspace/WorkspaceInvitePage.tsx | 109 +++++++++--------- 8 files changed, 88 insertions(+), 88 deletions(-) diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx index c57c38c3eba1..0bc9130ea4a8 100644 --- a/src/components/OnyxProvider.tsx +++ b/src/components/OnyxProvider.tsx @@ -15,7 +15,6 @@ const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME); const [withFrequentlyUsedEmojis, FrequentlyUsedEmojisProvider, , useFrequentlyUsedEmojis] = createOnyxContext(ONYXKEYS.FREQUENTLY_USED_EMOJIS); const [withPreferredEmojiSkinTone, PreferredEmojiSkinToneProvider, PreferredEmojiSkinToneContext] = createOnyxContext(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE); -const [withReports, ReportsProvider, ReportsContext, useReports] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT); const [, SessionProvider, , useSession] = createOnyxContext(ONYXKEYS.SESSION); type OnyxProviderProps = { @@ -38,7 +37,6 @@ function OnyxProvider(props: OnyxProviderProps) { FrequentlyUsedEmojisProvider, PreferredEmojiSkinToneProvider, SessionProvider, - ReportsProvider, ]} > {props.children} @@ -71,7 +69,4 @@ export { useBlockedFromConcierge, useReportActionsDrafts, useSession, - withReports, - ReportsContext, - useReports, }; diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 00cdaa6dc1ac..11906c6274c9 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -107,10 +107,10 @@ const useOptionsListContext = () => useContext(OptionsListContext); // Hook to use the OptionsListContext with an initializer to load the options const useOptionsList = (options?: {shouldInitialize: boolean}) => { const {shouldInitialize = true} = options ?? {}; - const {initializeOptions, options: optionList, areOptionsInitialized} = useOptionsListContext(); + const {initializeOptions, options: optionsList, areOptionsInitialized} = useOptionsListContext(); // freeze the options, so they won't update when the context updates and the screen is open - const optionsRef = useRef(optionList); + const optionsRef = useRef(optionsList); useEffect(() => { if (!shouldInitialize || areOptionsInitialized) { @@ -120,6 +120,15 @@ const useOptionsList = (options?: {shouldInitialize: boolean}) => { initializeOptions(); }, [shouldInitialize, initializeOptions, areOptionsInitialized]); + useEffect(() => { + if (!areOptionsInitialized) { + return; + } + + optionsRef.current = optionsList; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [areOptionsInitialized]); + return { initializeOptions, options: optionsRef.current, diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 36d51533b917..2b072d2a0480 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -47,11 +47,7 @@ type NewChatPageProps = NewChatPageWithOnyxProps & { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { - const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: isScreenTransitionEnd, - }); const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); @@ -61,6 +57,10 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF const [selectedOptions, setSelectedOptions] = useState([]); const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -212,7 +212,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF useEffect(() => { const interactionTask = doInteractionTask(() => { - setIsScreenTransitionEnd(true); + setDidScreenTransitionEnd(true); }); return () => { @@ -225,11 +225,11 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF }, []); useEffect(() => { - if (!isScreenTransitionEnd) { + if (!didScreenTransitionEnd) { return; } updateOptions(); - }, [isScreenTransitionEnd, updateOptions]); + }, [didScreenTransitionEnd, updateOptions]); const {inputCallbackRef} = useAutoFocusInput(); @@ -265,7 +265,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF headerMessage={headerMessage} boldStyle shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - shouldShowOptions={areOptionsInitialized && isScreenTransitionEnd} + shouldShowOptions={areOptionsInitialized && didScreenTransitionEnd} shouldShowConfirmButton shouldShowReferralCTA={!dismissedReferralBanners[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 8c84ba12b153..cc9f1564c25e 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -23,13 +23,13 @@ function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { personalDetails: [], userToInvite: null, }); - + console.log({searchOptions}); const {isOffline} = useNetwork(); const {translate} = useLocalize(); const styles = useThemeStyles(); const isMounted = useRef(false); const {options, areOptionsInitialized} = useOptionsList(); - + console.log({options}); const updateOptions = useCallback(() => { const { recentReports: localRecentReports, @@ -51,7 +51,7 @@ function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { updateOptions(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [areOptionsInitialized]); + }, [options, areOptionsInitialized]); useEffect(() => { if (!isMounted.current) { diff --git a/src/pages/tasks/NewTaskPage.tsx b/src/pages/tasks/NewTaskPage.tsx index 64c46e75c91d..87541d82e094 100644 --- a/src/pages/tasks/NewTaskPage.tsx +++ b/src/pages/tasks/NewTaskPage.tsx @@ -178,7 +178,7 @@ function NewTaskPage({task, reports, personalDetails}: NewTaskPageProps) { description={shareDestination?.displayName ? shareDestination.subtitle : translate('newTaskPage.shareSomewhere')} icon={shareDestination?.icons} onPress={() => Navigation.navigate(ROUTES.NEW_TASK_SHARE_DESTINATION)} - interactive={!task?.parentReportID} + interactive shouldShowRightIcon={!task?.parentReportID} titleWithTooltips={shareDestination?.shouldUseFullTitleToDisplay ? shareDestination?.displayNamesWithTooltips : []} /> diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index d367a06cb60e..4185621398dd 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -41,12 +41,10 @@ type TaskAssigneeSelectorModalOnyxProps = { type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps; function useOptions() { - const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const betas = useBetas(); + const [isLoading, setIsLoading] = useState(true); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const {options: optionsList, areOptionsInitialized} = useOptionsList({ - shouldInitialize: isScreenTransitionEnd, - }); + const {options: optionsList, areOptionsInitialized} = useOptionsList(); const options = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( @@ -73,6 +71,10 @@ function useOptions() { debouncedSearchValue, ); + if (isLoading) { + setIsLoading(false); + } + return { userToInvite, recentReports, @@ -80,9 +82,9 @@ function useOptions() { currentUserOption, headerMessage, }; - }, [optionsList.reports, optionsList.personalDetails, betas, debouncedSearchValue]); + }, [optionsList.reports, optionsList.personalDetails, betas, debouncedSearchValue, isLoading]); - return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, setIsScreenTransitionEnd}; + return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; } function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) { @@ -91,7 +93,7 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro const {translate} = useLocalize(); const session = useSession(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, searchValue, setSearchValue, headerMessage, areOptionsInitialized, setIsScreenTransitionEnd} = useOptions(); + const {userToInvite, recentReports, personalDetails, currentUserOption, searchValue, setSearchValue, headerMessage, areOptionsInitialized} = useOptions(); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); @@ -206,7 +208,6 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro setIsScreenTransitionEnd(true)} > {({didScreenTransitionEnd}) => ( diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 0c31058fe329..9a1dac648887 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -48,14 +48,11 @@ const reportFilter = (reportOptions: Array }, []); function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDestinationSelectorModalProps) { - const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {options: optionList, areOptionsInitialized} = useOptionsList({ - shouldInitialize: isScreenTransitionEnd, - }); + const {options: optionList, areOptionsInitialized} = useOptionsList(); const textInputHint = useMemo(() => (isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''), [isOffline, translate]); @@ -101,7 +98,6 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes setIsScreenTransitionEnd(true)} > {({didScreenTransitionEnd}) => ( <> diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index d01a73fdd9ff..da6c00472306 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -67,15 +67,13 @@ function WorkspaceInvitePage({ const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); - const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const navigation = useNavigation>(); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: isScreenTransitionEnd, - }); + const {options, areOptionsInitialized} = useOptionsList(); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -92,7 +90,7 @@ function WorkspaceInvitePage({ useEffect(() => { const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { - setIsScreenTransitionEnd(true); + setDidScreenTransitionEnd(true); }); return () => { @@ -168,6 +166,10 @@ function WorkspaceInvitePage({ const sectionsArr: MembersSection[] = []; let indexOffset = 0; + if (!didScreenTransitionEnd) { + return []; + } + // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; if (searchTerm !== '') { @@ -217,7 +219,7 @@ function WorkspaceInvitePage({ }); return sectionsArr; - }, [selectedOptions, searchTerm, personalDetails, translate, usersToInvite]); + }, [didScreenTransitionEnd, selectedOptions, searchTerm, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { Policy.clearErrors(route.params.policyID); @@ -287,57 +289,54 @@ function WorkspaceInvitePage({ shouldEnableMaxHeight shouldUseCachedViewportHeight testID={WorkspaceInvitePage.displayName} - onEntryTransitionEnd={() => setIsScreenTransitionEnd(true)} > - {({didScreenTransitionEnd}) => ( - - { - Policy.clearErrors(route.params.policyID); - Navigation.goBack(); - }} - /> - { - SearchInputManager.searchInput = value; - setSearchTerm(value); - }} - headerMessage={headerMessage} - onSelectRow={toggleOption} - onConfirm={inviteUser} - showScrollIndicator - showLoadingPlaceholder={areOptionsInitialized && searchTerm.trim() === '' ? sections.length === 0 : !didScreenTransitionEnd} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - checkmarkPosition={CONST.DIRECTION.RIGHT} + + { + Policy.clearErrors(route.params.policyID); + Navigation.goBack(); + }} + /> + { + SearchInputManager.searchInput = value; + setSearchTerm(value); + }} + headerMessage={headerMessage} + onSelectRow={toggleOption} + onConfirm={inviteUser} + showScrollIndicator + showLoadingPlaceholder={areOptionsInitialized && searchTerm.trim() === '' ? sections.length === 0 : !didScreenTransitionEnd} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + checkmarkPosition={CONST.DIRECTION.RIGHT} + /> + + - - - - - )} + + ); } From 0377ba6965974a52181360ad0776646cfe6f410d Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Mar 2024 11:31:26 +0100 Subject: [PATCH 21/41] remove console logs --- src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index cc9f1564c25e..75e0879398ac 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -23,13 +23,11 @@ function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { personalDetails: [], userToInvite: null, }); - console.log({searchOptions}); const {isOffline} = useNetwork(); const {translate} = useLocalize(); const styles = useThemeStyles(); const isMounted = useRef(false); const {options, areOptionsInitialized} = useOptionsList(); - console.log({options}); const updateOptions = useCallback(() => { const { recentReports: localRecentReports, From c6e95419d8c5e4b0570dfa84aaadf7d4dd7ddb0c Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Mar 2024 13:57:28 +0100 Subject: [PATCH 22/41] fix for generating alternate text --- src/components/OptionListContextProvider.tsx | 14 +----- src/libs/OptionsListUtils.ts | 49 ++++---------------- 2 files changed, 9 insertions(+), 54 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 11906c6274c9..feabc0464913 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -109,9 +109,6 @@ const useOptionsList = (options?: {shouldInitialize: boolean}) => { const {shouldInitialize = true} = options ?? {}; const {initializeOptions, options: optionsList, areOptionsInitialized} = useOptionsListContext(); - // freeze the options, so they won't update when the context updates and the screen is open - const optionsRef = useRef(optionsList); - useEffect(() => { if (!shouldInitialize || areOptionsInitialized) { return; @@ -120,18 +117,9 @@ const useOptionsList = (options?: {shouldInitialize: boolean}) => { initializeOptions(); }, [shouldInitialize, initializeOptions, areOptionsInitialized]); - useEffect(() => { - if (!areOptionsInitialized) { - return; - } - - optionsRef.current = optionsList; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [areOptionsInitialized]); - return { initializeOptions, - options: optionsRef.current, + options: optionsList, areOptionsInitialized, }; }; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 1f976055086e..bb20dfeb645f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -631,8 +631,10 @@ function createOption( personalDetails: OnyxEntry, report: OnyxEntry, reportActions: ReportActions, - {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig, + config?: PreviewConfig, ): ReportUtils.OptionData { + const {showChatPreviewLine = false, forcePolicyNamePreview = false} = config ?? {}; + const result: ReportUtils.OptionData = { text: undefined, alternateText: null, @@ -725,6 +727,7 @@ function createOption( if (showChatPreviewLine || forcePolicyNamePreview) { result.alternateText = getAlternateTextOption(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); } + reportName = ReportUtils.getReportName(report); } else { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -765,16 +768,7 @@ function createOption( function getPolicyExpenseReportOption(report: Report): ReportUtils.OptionData { const expenseReport = policyExpenseReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; - const option = createOption( - expenseReport?.visibleChatMemberAccountIDs ?? [], - allPersonalDetails ?? {}, - expenseReport ?? null, - {}, - { - showChatPreviewLine: false, - forcePolicyNamePreview: false, - }, - ); + const option = createOption(expenseReport?.visibleChatMemberAccountIDs ?? [], allPersonalDetails ?? {}, expenseReport ?? null, {}); // Update text & alternateText because createOption returns workspace name only if report is owned by the user option.text = ReportUtils.getPolicyName(expenseReport); @@ -1405,32 +1399,14 @@ function createOptionList(personalDetails: OnyxEntry, repor allReportOptions.push({ item: report, - ...createOption( - accountIDs, - personalDetails, - report, - {}, - { - showChatPreviewLine: false, - forcePolicyNamePreview: false, - }, - ), + ...createOption(accountIDs, personalDetails, report, {}), }); }); } const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ item: personalDetail, - ...createOption( - [personalDetail?.accountID ?? -1], - personalDetails, - reportMapForAccountIDs[personalDetail?.accountID ?? -1], - {}, - { - showChatPreviewLine: false, - forcePolicyNamePreview: false, - }, - ), + ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}), })); return { @@ -1444,16 +1420,7 @@ function createOptionFromReport(report: Report, personalDetails: OnyxEntry Date: Fri, 15 Mar 2024 18:26:03 +0100 Subject: [PATCH 23/41] fix jest tests --- src/libs/OptionsListUtils.ts | 14 +- tests/unit/OptionsListUtilsTest.js | 209 +++++++++++++++++------------ 2 files changed, 123 insertions(+), 100 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index bb20dfeb645f..a5906d45b1cc 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1365,19 +1365,7 @@ function createOptionList(personalDetails: OnyxEntry, repor const allReportOptions: Array> = []; if (reports) { - // Sorting the reports works like this: - // - Order everything by the last message timestamp (descending) - // - All archived reports should remain at the bottom - const orderedReports = lodashSortBy(reports, (report) => { - if (ReportUtils.isArchivedRoom(report)) { - return CONST.DATE.UNIX_EPOCH; - } - - return report?.lastVisibleActionCreated; - }); - orderedReports.reverse(); - - orderedReports.forEach((report) => { + Object.values(reports).forEach((report) => { if (!report) { return; } diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 7244b7830a29..ea06c1d99db1 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -311,25 +311,38 @@ describe('OptionsListUtils', () => { return waitForBatchedUpdates().then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, PERSONAL_DETAILS)); }); + let OPTIONS = {}; + let OPTIONS_WITH_CONCIERGE = {}; + let OPTIONS_WITH_CHRONOS = {}; + let OPTIONS_WITH_RECEIPTS = {}; + let OPTIONS_WITH_WORKSPACES = {}; + + beforeEach(() => { + OPTIONS = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS); + OPTIONS_WITH_CONCIERGE = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_CONCIERGE, REPORTS_WITH_CONCIERGE); + OPTIONS_WITH_CHRONOS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_CHRONOS, REPORTS_WITH_CHRONOS); + OPTIONS_WITH_RECEIPTS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_RECEIPTS, REPORTS_WITH_RECEIPTS); + OPTIONS_WITH_WORKSPACES = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_WORKSPACE_ROOMS); + }); + it('getSearchOptions()', () => { // When we filter in the Search view without providing a searchValue - let results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); - + let results = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); // Then the 2 personalDetails that don't have reports should be returned expect(results.personalDetails.length).toBe(2); // Then all of the reports should be shown including the archived rooms. - expect(results.recentReports.length).toBe(_.size(REPORTS)); + expect(results.recentReports.length).toBe(_.size(OPTIONS.reports)); // When we filter again but provide a searchValue - results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, 'spider'); + results = OptionsListUtils.getSearchOptions(OPTIONS, 'spider'); // Then only one option should be returned and it's the one matching the search value expect(results.recentReports.length).toBe(1); expect(results.recentReports[0].login).toBe('peterparker@expensify.com'); // When we filter again but provide a searchValue that should match multiple times - results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, 'fantastic'); + results = OptionsListUtils.getSearchOptions(OPTIONS, 'fantastic'); // Value with latest lastVisibleActionCreated should be at the top. expect(results.recentReports.length).toBe(2); @@ -339,9 +352,9 @@ describe('OptionsListUtils', () => { return waitForBatchedUpdates() .then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, PERSONAL_DETAILS_WITH_PERIODS)) .then(() => { + const OPTIONS_WITH_PERIODS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS); // When we filter again but provide a searchValue that should match with periods - results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS_WITH_PERIODS, 'barry.allen@expensify.com'); - + results = OptionsListUtils.getSearchOptions(OPTIONS_WITH_PERIODS, 'barry.allen@expensify.com'); // Then we expect to have the personal detail with period filtered expect(results.recentReports.length).toBe(1); expect(results.recentReports[0].text).toBe('The Flash'); @@ -353,14 +366,14 @@ describe('OptionsListUtils', () => { const MAX_RECENT_REPORTS = 5; // When we call getFilteredOptions() with no search value - let results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], ''); + let results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], ''); // We should expect maximimum of 5 recent reports to be returned expect(results.recentReports.length).toBe(MAX_RECENT_REPORTS); // We should expect all personalDetails to be returned, // minus the currently logged in user and recent reports count - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 1 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS.personalDetails) - 1 - MAX_RECENT_REPORTS); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Widow'); @@ -373,7 +386,7 @@ describe('OptionsListUtils', () => { expect(personalDetailWithExistingReport.reportID).toBe(2); // When we only pass personal details - results = OptionsListUtils.getFilteredOptions([], PERSONAL_DETAILS, [], ''); + results = OptionsListUtils.getFilteredOptions([], OPTIONS.personalDetails, [], ''); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Panther'); @@ -382,13 +395,13 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[3].text).toBe('Invisible Woman'); // When we provide a search value that does not match any personal details - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'magneto'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'magneto'); // Then no options will be returned expect(results.personalDetails.length).toBe(0); // When we provide a search value that matches an email - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'peterparker@expensify.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'peterparker@expensify.com'); // Then one recentReports will be returned and it will be the correct option // personalDetails should be empty array @@ -397,7 +410,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails.length).toBe(0); // When we provide a search value that matches a partial display name or email - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '.com'); // Then several options will be returned and they will be each have the search string in their email or name // even though the currently logged in user matches they should not show. @@ -410,45 +423,46 @@ describe('OptionsListUtils', () => { expect(results.recentReports[2].text).toBe('Black Panther'); // Test for Concierge's existence in chat options - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE); + + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CONCIERGE.reports, OPTIONS_WITH_CONCIERGE.personalDetails); // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 1 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CONCIERGE.personalDetails) - 1 - MAX_RECENT_REPORTS); expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CONCIERGE.reports, OPTIONS_WITH_CONCIERGE.personalDetails, [], '', [], [CONST.EMAIL.CONCIERGE]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CONCIERGE.personalDetails) - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CHRONOS.reports, OPTIONS_WITH_CHRONOS.personalDetails, [], '', [], [CONST.EMAIL.CHRONOS]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CHRONOS.personalDetails) - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_RECEIPTS.reports, OPTIONS_WITH_RECEIPTS.personalDetails, [], '', [], [CONST.EMAIL.RECEIPTS]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_RECEIPTS.personalDetails) - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); it('getFilteredOptions() for group Chat', () => { // When we call getFilteredOptions() with no search value - let results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], ''); + let results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], ''); // Then we should expect only a maxmimum of 5 recent reports to be returned expect(results.recentReports.length).toBe(5); // And we should expect all the personalDetails to show (minus the 5 that are already // showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 6); + expect(results.personalDetails.length).toBe(_.size(OPTIONS.personalDetails) - 6); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Widow'); @@ -462,7 +476,7 @@ describe('OptionsListUtils', () => { expect(personalDetailsOverlapWithReports).toBe(false); // When we search for an option that is only in a personalDetail with no existing report - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'hulk'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'hulk'); // Then reports should return no results expect(results.recentReports.length).toBe(0); @@ -472,7 +486,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[0].login).toBe('brucebanner@expensify.com'); // When we search for an option that matches things in both personalDetails and reports - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '.com'); // Then all single participant reports that match will show up in the recentReports array, Recently used contact should be at the top expect(results.recentReports.length).toBe(5); @@ -483,7 +497,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[0].login).toBe('natasharomanoff@expensify.com'); // When we provide no selected options to getFilteredOptions() - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', []); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '', []); // Then one of our older report options (not in our five most recent) should appear in the personalDetails // but not in recentReports @@ -491,7 +505,7 @@ describe('OptionsListUtils', () => { expect(_.every(results.personalDetails, (option) => option.login !== 'peterparker@expensify.com')).toBe(false); // When we provide a "selected" option to getFilteredOptions() - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', [{login: 'peterparker@expensify.com'}]); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '', [{login: 'peterparker@expensify.com'}]); // Then the option should not appear anywhere in either list expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); @@ -499,7 +513,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is not a potential email or phone - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'marc@expensify'); // Then we should have no options or personal details at all and also that there is no userToInvite expect(results.recentReports.length).toBe(0); @@ -508,7 +522,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential email - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'marc@expensify.com'); // Then we should have no options or personal details at all but there should be a userToInvite expect(results.recentReports.length).toBe(0); @@ -516,7 +530,7 @@ describe('OptionsListUtils', () => { expect(results.userToInvite).not.toBe(null); // When we add a search term with a period, with options for it that don't contain the period - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'peter.parker@expensify.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'peter.parker@expensify.com'); // Then we should have no options at all but there should be a userToInvite expect(results.recentReports.length).toBe(0); @@ -524,7 +538,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential phone number without country code added - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '5005550006'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '5005550006'); // Then we should have no options or personal details at all but there should be a userToInvite and the login // should have the country code included @@ -535,7 +549,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with country code added - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '+15005550006'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '+15005550006'); // Then we should have no options or personal details at all but there should be a userToInvite and the login // should have the country code included @@ -546,7 +560,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with special characters added - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '+1 (800)324-3233'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '+1 (800)324-3233'); // Then we should have no options or personal details at all but there should be a userToInvite and the login // should have the country code included @@ -556,7 +570,7 @@ describe('OptionsListUtils', () => { expect(results.userToInvite.login).toBe('+18003243233'); // When we use a search term for contact number that contains alphabet characters - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '998243aaaa'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '998243aaaa'); // Then we shouldn't have any results or user to invite expect(results.recentReports.length).toBe(0); @@ -564,93 +578,100 @@ describe('OptionsListUtils', () => { expect(results.userToInvite).toBe(null); // Test Concierge's existence in new group options - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CONCIERGE.reports, OPTIONS_WITH_CONCIERGE.personalDetails); // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 6); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CONCIERGE.personalDetails) - 6); expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CONCIERGE.reports, OPTIONS_WITH_CONCIERGE.personalDetails, [], '', [], [CONST.EMAIL.CONCIERGE]); // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 7); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CONCIERGE.personalDetails) - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CHRONOS.reports, OPTIONS_WITH_CHRONOS.personalDetails, [], '', [], [CONST.EMAIL.CHRONOS]); // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 7); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CHRONOS.personalDetails) - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_RECEIPTS.reports, OPTIONS_WITH_RECEIPTS.personalDetails, [], '', [], [CONST.EMAIL.RECEIPTS]); // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 7); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_RECEIPTS.personalDetails) - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); it('getShareDestinationsOptions()', () => { // Filter current REPORTS as we do in the component, before getting share destination options - const filteredReports = {}; - _.keys(REPORTS).forEach((reportKey) => { - if (!ReportUtils.canUserPerformWriteAction(REPORTS[reportKey]) || ReportUtils.isExpensifyOnlyParticipantInReport(REPORTS[reportKey])) { - return; - } - filteredReports[reportKey] = REPORTS[reportKey]; - }); + const filteredReports = _.reduce( + OPTIONS.reports, + (filtered, option) => { + const report = option.item; + if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { + filtered.push(option); + } + return filtered; + }, + [], + ); // When we pass an empty search value - let results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], ''); + let results = OptionsListUtils.getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, [], ''); // Then we should expect all the recent reports to show but exclude the archived rooms - expect(results.recentReports.length).toBe(_.size(REPORTS) - 1); + expect(results.recentReports.length).toBe(_.size(OPTIONS.reports) - 1); // When we pass a search value that doesn't match the group chat name - results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], 'mutants'); + results = OptionsListUtils.getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, [], 'mutants'); // Then we should expect no recent reports to show expect(results.recentReports.length).toBe(0); // When we pass a search value that matches the group chat name - results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], 'Iron Man, Fantastic'); + results = OptionsListUtils.getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, [], 'Iron Man, Fantastic'); // Then we should expect the group chat to show along with the contacts matching the search expect(results.recentReports.length).toBe(1); // Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options - const filteredReportsWithWorkspaceRooms = {}; - _.keys(REPORTS_WITH_WORKSPACE_ROOMS).forEach((reportKey) => { - if (!ReportUtils.canUserPerformWriteAction(REPORTS_WITH_WORKSPACE_ROOMS[reportKey]) || ReportUtils.isExpensifyOnlyParticipantInReport(REPORTS_WITH_WORKSPACE_ROOMS[reportKey])) { - return; - } - filteredReportsWithWorkspaceRooms[reportKey] = REPORTS_WITH_WORKSPACE_ROOMS[reportKey]; - }); + const filteredReportsWithWorkspaceRooms = _.reduce( + OPTIONS_WITH_WORKSPACES.reports, + (filtered, option) => { + const report = option.item; + if (ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { + filtered.push(option); + } + return filtered; + }, + [], + ); // When we also have a policy to return rooms in the results - results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], ''); - + results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], ''); // Then we should expect the DMS, the group chats and the workspace room to show // We should expect all the recent reports to show, excluding the archived rooms - expect(results.recentReports.length).toBe(_.size(REPORTS_WITH_WORKSPACE_ROOMS) - 1); + expect(results.recentReports.length).toBe(_.size(OPTIONS_WITH_WORKSPACES.reports) - 1); // When we search for a workspace room - results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], 'Avengers Room'); + results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], 'Avengers Room'); // Then we should expect only the workspace room to show expect(results.recentReports.length).toBe(1); // When we search for a workspace room that doesn't exist - results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], 'Mutants Lair'); + results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], 'Mutants Lair'); // Then we should expect no results to show expect(results.recentReports.length).toBe(0); @@ -658,7 +679,7 @@ describe('OptionsListUtils', () => { it('getMemberInviteOptions()', () => { // When we only pass personal details - let results = OptionsListUtils.getMemberInviteOptions(PERSONAL_DETAILS, [], ''); + let results = OptionsListUtils.getMemberInviteOptions(OPTIONS.personalDetails, [], ''); // We should expect personal details to be sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Panther'); @@ -667,13 +688,13 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[3].text).toBe('Invisible Woman'); // When we provide a search value that does not match any personal details - results = OptionsListUtils.getMemberInviteOptions(PERSONAL_DETAILS, [], 'magneto'); + results = OptionsListUtils.getMemberInviteOptions(OPTIONS.personalDetails, [], 'magneto'); // Then no options will be returned expect(results.personalDetails.length).toBe(0); // When we provide a search value that matches an email - results = OptionsListUtils.getMemberInviteOptions(PERSONAL_DETAILS, [], 'peterparker@expensify.com'); + results = OptionsListUtils.getMemberInviteOptions(OPTIONS.personalDetails, [], 'peterparker@expensify.com'); // Then one personal should be in personalDetails list expect(results.personalDetails.length).toBe(1); @@ -1020,18 +1041,18 @@ describe('OptionsListUtils', () => { }, ]; - const smallResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, true, smallCategoriesList); + const smallResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], emptySearch, [], [], false, false, true, smallCategoriesList); expect(smallResult.categoryOptions).toStrictEqual(smallResultList); - const smallSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, true, smallCategoriesList); + const smallSearchResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], search, [], [], false, false, true, smallCategoriesList); expect(smallSearchResult.categoryOptions).toStrictEqual(smallSearchResultList); - const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, true, smallCategoriesList); + const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], wrongSearch, [], [], false, false, true, smallCategoriesList); expect(smallWrongSearchResult.categoryOptions).toStrictEqual(smallWrongSearchResultList); const largeResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], emptySearch, selectedOptions, @@ -1045,8 +1066,8 @@ describe('OptionsListUtils', () => { expect(largeResult.categoryOptions).toStrictEqual(largeResultList); const largeSearchResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], search, selectedOptions, @@ -1060,8 +1081,8 @@ describe('OptionsListUtils', () => { expect(largeSearchResult.categoryOptions).toStrictEqual(largeSearchResultList); const largeWrongSearchResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], wrongSearch, selectedOptions, @@ -1074,7 +1095,7 @@ describe('OptionsListUtils', () => { ); expect(largeWrongSearchResult.categoryOptions).toStrictEqual(largeWrongSearchResultList); - const emptyResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, selectedOptions, [], false, false, true, emptyCategoriesList); + const emptyResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], search, selectedOptions, [], false, false, true, emptyCategoriesList); expect(emptyResult.categoryOptions).toStrictEqual(emptySelectedResultList); }); @@ -1327,18 +1348,32 @@ describe('OptionsListUtils', () => { }, ]; - const smallResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, false, {}, [], true, smallTagsList); + const smallResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], emptySearch, [], [], false, false, false, {}, [], true, smallTagsList); expect(smallResult.tagOptions).toStrictEqual(smallResultList); - const smallSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, false, {}, [], true, smallTagsList); + const smallSearchResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], search, [], [], false, false, false, {}, [], true, smallTagsList); expect(smallSearchResult.tagOptions).toStrictEqual(smallSearchResultList); - const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, false, {}, [], true, smallTagsList); + const smallWrongSearchResult = OptionsListUtils.getFilteredOptions( + OPTIONS.reports, + OPTIONS.personalDetails, + [], + wrongSearch, + [], + [], + false, + false, + false, + {}, + [], + true, + smallTagsList, + ); expect(smallWrongSearchResult.tagOptions).toStrictEqual(smallWrongSearchResultList); const largeResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], emptySearch, selectedOptions, @@ -1355,8 +1390,8 @@ describe('OptionsListUtils', () => { expect(largeResult.tagOptions).toStrictEqual(largeResultList); const largeSearchResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], search, selectedOptions, @@ -1373,8 +1408,8 @@ describe('OptionsListUtils', () => { expect(largeSearchResult.tagOptions).toStrictEqual(largeSearchResultList); const largeWrongSearchResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], wrongSearch, selectedOptions, From 69cd8cebb4af2d84717e6351bbe85b9934c3001b Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 18 Mar 2024 09:22:57 +0100 Subject: [PATCH 24/41] update reassure tests --- tests/perf-test/SearchPage.perf-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index be6b6a5d78f9..b25eb9c0ec05 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -5,6 +5,7 @@ import {measurePerformance} from 'reassure'; import _ from 'underscore'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import SearchPage from '@pages/SearchPage'; +import OptionListContextProvider from '@components/OptionListContextProvider'; import ComposeProviders from '../../src/components/ComposeProviders'; import OnyxProvider from '../../src/components/OnyxProvider'; import CONST from '../../src/CONST'; @@ -110,7 +111,7 @@ afterEach(() => { function SearchPageWrapper(args) { return ( - + Date: Mon, 18 Mar 2024 10:02:34 +0100 Subject: [PATCH 25/41] fix prettier --- tests/perf-test/SearchPage.perf-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index b25eb9c0ec05..bc3b32dbba96 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -4,8 +4,8 @@ import Onyx from 'react-native-onyx'; import {measurePerformance} from 'reassure'; import _ from 'underscore'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; -import SearchPage from '@pages/SearchPage'; import OptionListContextProvider from '@components/OptionListContextProvider'; +import SearchPage from '@pages/SearchPage'; import ComposeProviders from '../../src/components/ComposeProviders'; import OnyxProvider from '../../src/components/OnyxProvider'; import CONST from '../../src/CONST'; From 6876ae7384f4917a4657553606460fad4ae75e14 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 19 Mar 2024 16:21:56 +0100 Subject: [PATCH 26/41] update reassure option list utils tests --- src/libs/OptionsListUtils.ts | 5 ++++- tests/perf-test/OptionsListUtils.perf-test.ts | 12 +++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 5ee58ab861bd..454ac92c201c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -550,7 +550,10 @@ function getAlternateTextOption( if (option.isTaskReport) { return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); } - return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : LocalePhoneNumber.formatPhoneNumber((option.participantsList ?? [])[0].login ?? ''); + + return showChatPreviewLine && option.lastMessageText + ? option.lastMessageText + : LocalePhoneNumber.formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList[0].login ?? '' : ''); } /** diff --git a/tests/perf-test/OptionsListUtils.perf-test.ts b/tests/perf-test/OptionsListUtils.perf-test.ts index d81be3165919..5041e919e7c1 100644 --- a/tests/perf-test/OptionsListUtils.perf-test.ts +++ b/tests/perf-test/OptionsListUtils.perf-test.ts @@ -44,35 +44,37 @@ jest.mock('@react-navigation/native', () => { } as typeof NativeNavigation; }); +const options = OptionsListUtils.createOptionList(personalDetails, reports); + /* GetOption is the private function and is never called directly, we are testing the functions which call getOption with different params */ describe('OptionsListUtils', () => { /* Testing getSearchOptions */ test('[OptionsListUtils] getSearchOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getSearchOptions(reports, personalDetails, SEARCH_VALUE, mockedBetas)); + await measureFunction(() => OptionsListUtils.getSearchOptions(options, SEARCH_VALUE, mockedBetas)); }); /* Testing getShareLogOptions */ test('[OptionsListUtils] getShareLogOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getShareLogOptions(reports, personalDetails, SEARCH_VALUE, mockedBetas)); + await measureFunction(() => OptionsListUtils.getShareLogOptions(options, SEARCH_VALUE, mockedBetas)); }); /* Testing getFilteredOptions */ test('[OptionsListUtils] getFilteredOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getFilteredOptions(reports, personalDetails, mockedBetas, SEARCH_VALUE)); + await measureFunction(() => OptionsListUtils.getFilteredOptions(options.reports, options.personalDetails, mockedBetas, SEARCH_VALUE)); }); /* Testing getShareDestinationOptions */ test('[OptionsListUtils] getShareDestinationOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getShareDestinationOptions(reports, personalDetails, mockedBetas, SEARCH_VALUE)); + await measureFunction(() => OptionsListUtils.getShareDestinationOptions(options.reports, options.personalDetails, mockedBetas, SEARCH_VALUE)); }); /* Testing getMemberInviteOptions */ test('[OptionsListUtils] getMemberInviteOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getMemberInviteOptions(personalDetails, mockedBetas, SEARCH_VALUE)); + await measureFunction(() => OptionsListUtils.getMemberInviteOptions(options.personalDetails, mockedBetas, SEARCH_VALUE)); }); }); From e9fae4e94994cec2aac8b003e56f1d78b9647771 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 20 Mar 2024 09:28:43 +0100 Subject: [PATCH 27/41] remove redundant function calls --- src/libs/OptionsListUtils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 454ac92c201c..dd0dc2ed8224 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1528,7 +1528,7 @@ function getOptions( // - All archived reports should remain at the bottom const orderedReportOptions = lodashSortBy(filteredReportOptions, (option) => { const report = option.item; - if (ReportUtils.isArchivedRoom(report)) { + if (option.isArchivedRoom) { return CONST.DATE.UNIX_EPOCH; } @@ -1543,11 +1543,11 @@ function getOptions( return; } - const isThread = ReportUtils.isChatThread(report); - const isTaskReport = ReportUtils.isTaskReport(report); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const isSelfDM = ReportUtils.isSelfDM(report); + const isThread = option.isThread; + const isTaskReport = option.isTaskReport; + const isPolicyExpenseChat = option.isPolicyExpenseChat; + const isMoneyRequestReport = option.isMoneyRequestReport; + const isSelfDM = option.isSelfDM; // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; From 46a4fccbcf01bc68e6ce51825eb81452519f0f63 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 20 Mar 2024 11:31:14 +0100 Subject: [PATCH 28/41] speed up setting options --- src/components/OptionListContextProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index feabc0464913..4bbd36c9df27 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -55,7 +55,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp } setOptions((prevOptions) => { - const newOptions = {...prevOptions}; + const newOptions = prevOptions; newOptions.reports[replaceIndex] = newOption; return newOptions; }); From b28bd1307c1d81a1b668ff329a31fc23c06bdee5 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 20 Mar 2024 16:07:24 +0100 Subject: [PATCH 29/41] update money request participants selector --- ...raryForRefactorRequestParticipantsSelector.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index cdc3e72f98f4..927ee855c6eb 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -20,9 +20,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import { useOptionsList } from '@components/OptionListContextProvider'; const propTypes = { /** Beta features list */ @@ -48,9 +48,6 @@ const propTypes = { }), ), - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - /** Padding bottom style of safe area */ safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), @@ -67,7 +64,6 @@ const propTypes = { const defaultProps = { participants: [], safeAreaPaddingBottomStyle: {}, - reports: {}, betas: [], dismissedReferralBanners: {}, didScreenTransitionEnd: false, @@ -76,7 +72,6 @@ const defaultProps = { function MoneyTemporaryForRefactorRequestParticipantsSelector({ betas, participants, - reports, onFinish, onParticipantsAdded, safeAreaPaddingBottomStyle, @@ -92,6 +87,9 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const {canUseP2PDistanceRequests} = usePermissions(); + const {options} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -111,8 +109,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ let indexOffset = 0; const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports, + options.personalDetails, betas, searchTerm, participants, @@ -180,7 +178,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, canUseP2PDistanceRequests, translate]); + }, [didScreenTransitionEnd, options.reports, options.personalDetails, betas, searchTerm, participants, iouType, canUseP2PDistanceRequests, iouRequestType, maxParticipantsReached, personalDetails, translate]); /** * Adds a single participant to the request From 5692a8d60209763e383ba0e9760f91011920187f Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 20 Mar 2024 16:57:34 +0100 Subject: [PATCH 30/41] lint code --- ...aryForRefactorRequestParticipantsSelector.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 927ee855c6eb..444cfeb2171d 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; @@ -22,7 +23,6 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import { useOptionsList } from '@components/OptionListContextProvider'; const propTypes = { /** Beta features list */ @@ -178,7 +178,20 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [didScreenTransitionEnd, options.reports, options.personalDetails, betas, searchTerm, participants, iouType, canUseP2PDistanceRequests, iouRequestType, maxParticipantsReached, personalDetails, translate]); + }, [ + didScreenTransitionEnd, + options.reports, + options.personalDetails, + betas, + searchTerm, + participants, + iouType, + canUseP2PDistanceRequests, + iouRequestType, + maxParticipantsReached, + personalDetails, + translate, + ]); /** * Adds a single participant to the request From ca2c10a24615923956fafb6a75442785cfe70520 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 20 Mar 2024 17:07:15 +0100 Subject: [PATCH 31/41] add comments for option list context --- src/components/OptionListContextProvider.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 4bbd36c9df27..4cabe9db6108 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -9,12 +9,16 @@ import type {Report} from '@src/types/onyx'; import {usePersonalDetails} from './OnyxProvider'; type OptionsListContextProps = { + /** List of options for reports and personal details */ options: OptionList; + /** Function to initialize the options */ initializeOptions: () => void; + /** Flag to check if the options are initialized */ areOptionsInitialized: boolean; }; type OptionsListProviderOnyxProps = { + /** Collection of reports */ reports: OnyxCollection; }; From ec3547e59d731e181a990056e0ba1117963126b0 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 20 Mar 2024 17:53:35 +0100 Subject: [PATCH 32/41] add test for cached options in search page --- src/components/OptionListContextProvider.tsx | 2 +- src/pages/SearchPage/index.tsx | 6 +-- tests/perf-test/SearchPage.perf-test.tsx | 45 +++++++++++++++++++- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 4cabe9db6108..bad72c52488e 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -134,4 +134,4 @@ export default withOnyx( }, })(OptionsListContextProvider); -export {useOptionsListContext, useOptionsList}; +export {useOptionsListContext, useOptionsList, OptionsListContext}; diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index d08d68c37cdc..c25dae65ab5c 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -150,7 +150,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) shouldEnableMaxHeight navigation={navigation} > - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({safeAreaPaddingBottomStyle}) => ( <> - sections={(!areOptionsInitialized && didScreenTransitionEnd) || areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} + sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} ListItem={UserListItem} textInputValue={searchValue} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} @@ -168,7 +168,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) onLayout={setPerformanceTimersEnd} autoFocus onSelectRow={selectReport} - showLoadingPlaceholder={areOptionsInitialized && debouncedSearchValue.trim() === '' ? sections.length === 0 : !didScreenTransitionEnd} + showLoadingPlaceholder={!areOptionsInitialized} footerContent={SearchPageFooterInstance} isLoadingNewOptions={isSearchingForReports ?? undefined} /> diff --git a/tests/perf-test/SearchPage.perf-test.tsx b/tests/perf-test/SearchPage.perf-test.tsx index 7bc127886076..b8588253e0c6 100644 --- a/tests/perf-test/SearchPage.perf-test.tsx +++ b/tests/perf-test/SearchPage.perf-test.tsx @@ -2,15 +2,16 @@ import type * as NativeNavigation from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import {fireEvent, screen, waitFor} from '@testing-library/react-native'; import type {TextMatch} from '@testing-library/react-native/build/matches'; -import React from 'react'; +import React, {useMemo} from 'react'; import type {ComponentType} from 'react'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {measurePerformance} from 'reassure'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; -import OptionListContextProvider from '@components/OptionListContextProvider'; +import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider'; import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; import type {RootStackParamList} from '@libs/Navigation/types'; +import {createOptionList} from '@libs/OptionsListUtils'; import SearchPage from '@pages/SearchPage'; import ComposeProviders from '@src/components/ComposeProviders'; import OnyxProvider from '@src/components/OnyxProvider'; @@ -95,6 +96,7 @@ const getMockedPersonalDetails = (length = 100) => const mockedReports = getMockedReports(600); const mockedBetas = Object.values(CONST.BETAS); const mockedPersonalDetails = getMockedPersonalDetails(100); +const mockedOptions = createOptionList(mockedPersonalDetails, mockedReports); beforeAll(() => Onyx.init({ @@ -135,6 +137,45 @@ function SearchPageWrapper(args: SearchPageProps) { ); } +function SearchPageWithCachedOptions(args: SearchPageProps) { + return ( + + ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> + + + + ); +} + +test('[Search Page] should render list with cached options', async () => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + const {addListener} = TestHelper.createAddListenerMock(); + + const scenario = async () => { + await screen.findByTestId('SearchPage'); + }; + + const navigation = {addListener}; + + return ( + waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(() => measurePerformance(, {scenario})) + ); +}); + test('[Search Page] should interact when text input changes', async () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. const {addListener} = TestHelper.createAddListenerMock(); From eef4d629e4ceb8c6bb571249b83b19c1f86bca50 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 21 Mar 2024 08:55:56 +0100 Subject: [PATCH 33/41] update displaying lists --- src/pages/RoomInvitePage.tsx | 26 ++++------------- .../MoneyRequestParticipantsSelector.js | 28 ++++--------------- 2 files changed, 11 insertions(+), 43 deletions(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index df57a4fee892..3f95d11027ba 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -1,5 +1,3 @@ -import {useNavigation} from '@react-navigation/native'; -import type {StackNavigationProp} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; @@ -19,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; -import type {RootStackParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; @@ -51,8 +48,6 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa const [selectedOptions, setSelectedOptions] = useState([]); const [invitePersonalDetails, setInvitePersonalDetails] = useState([]); const [userToInvite, setUserToInvite] = useState(null); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const navigation: StackNavigationProp = useNavigation(); const {options, areOptionsInitialized} = useOptionsList(); useEffect(() => { @@ -87,26 +82,14 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa setUserToInvite(inviteOptions.userToInvite); setInvitePersonalDetails(inviteOptions.personalDetails); setSelectedOptions(newSelectedOptions); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [personalDetails, betas, searchTerm, excludedUsers]); - - useEffect(() => { - const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { - setDidScreenTransitionEnd(true); - }); - - return () => { - unsubscribeTransitionEnd(); - }; - // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [personalDetails, betas, searchTerm, excludedUsers, options.personalDetails]); const sections = useMemo(() => { const sectionsArr: Sections = []; let indexOffset = 0; - if (!didScreenTransitionEnd) { + if (!areOptionsInitialized) { return []; } @@ -153,7 +136,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa } return sectionsArr; - }, [invitePersonalDetails, searchTerm, selectedOptions, translate, userToInvite, didScreenTransitionEnd]); + }, [areOptionsInitialized, selectedOptions, searchTerm, invitePersonalDetails, userToInvite, translate]); const toggleOption = useCallback( (option: OptionsListUtils.MemberForList) => { @@ -216,6 +199,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa } return OptionsListUtils.getHeaderMessage(invitePersonalDetails.length !== 0, Boolean(userToInvite), searchValue); }, [searchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); + return ( { const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports, + options.personalDetails, betas, searchTerm, participants, @@ -132,7 +123,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest, canUseP2PDistanceRequests]); + }, [options.reports, options.personalDetails, betas, searchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]); /** * Returns the sections needed for the OptionsSelector @@ -360,7 +351,7 @@ function MoneyRequestParticipantsSelector({ onSelectRow={addSingleParticipant} footerContent={footerContent} headerMessage={headerMessage} - showLoadingPlaceholder={isSearchingForReports} + showLoadingPlaceholder={!areOptionsInitialized} rightHandSideComponent={itemRightSideComponent} /> @@ -376,14 +367,7 @@ export default withOnyx({ key: ONYXKEYS.ACCOUNT, selector: (data) => data.dismissedReferralBanners || {}, }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, betas: { key: ONYXKEYS.BETAS, }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, })(MoneyRequestParticipantsSelector); From 8e1cc450bf4b82801526244847a831d290db4ce3 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 25 Mar 2024 12:16:28 +0100 Subject: [PATCH 34/41] code review updates --- src/components/OptionListContextProvider.tsx | 9 +++- src/libs/ReportUtils.ts | 4 +- src/pages/NewChatPage.tsx | 20 ++------ src/pages/tasks/NewTaskPage.tsx | 2 +- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 36 +++++++------ .../TaskShareDestinationSelectorModal.tsx | 50 +++++++++---------- 6 files changed, 56 insertions(+), 65 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index bad72c52488e..43c5906d4900 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -51,15 +51,20 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp } const lastUpdatedReport = ReportUtils.getLastUpdatedReport(); + + if (!lastUpdatedReport) { + return; + } + const newOption = OptionsListUtils.createOptionFromReport(lastUpdatedReport, personalDetails); const replaceIndex = options.reports.findIndex((option) => option.reportID === lastUpdatedReport.reportID); - if (replaceIndex === undefined) { + if (replaceIndex === -1) { return; } setOptions((prevOptions) => { - const newOptions = prevOptions; + const newOptions = {...prevOptions}; newOptions.reports[replaceIndex] = newOption; return newOptions; }); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7ea9cd39fc32..12c94f77d9ad 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -512,7 +512,7 @@ Onyx.connect({ }, }); -let lastUpdatedReport: Report; +let lastUpdatedReport: OnyxEntry; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, @@ -525,7 +525,7 @@ Onyx.connect({ }, }); -function getLastUpdatedReport() { +function getLastUpdatedReport(): OnyxEntry { return lastUpdatedReport; } diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 6f8e887e200c..c9d17e5f98d9 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; import {useOptionsList} from '@components/OptionListContextProvider'; @@ -24,12 +24,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; type NewChatPageWithOnyxProps = { - /** All reports shared with the user */ - reports: OnyxCollection; - - /** All of the personal details for everyone */ - personalDetails: OnyxEntry; - betas: OnyxEntry; /** An object that holds data about which referral banners have been dismissed */ @@ -45,7 +39,7 @@ type NewChatPageProps = NewChatPageWithOnyxProps & { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); -function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { +function NewChatPage({betas, isGroupChat, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -207,7 +201,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF setFilteredUserToInvite(userToInvite); // props.betas is not added as dependency since it doesn't change during the component lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reports, personalDetails, searchTerm]); + }, [options, searchTerm]); useEffect(() => { const interactionTask = doInteractionTask(() => { @@ -264,7 +258,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF headerMessage={headerMessage} boldStyle shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - shouldShowOptions={areOptionsInitialized && didScreenTransitionEnd} + shouldShowOptions={areOptionsInitialized} shouldShowConfirmButton shouldShowReferralCTA={!dismissedReferralBanners?.[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} @@ -290,12 +284,6 @@ export default withOnyx({ dismissedReferralBanners: { key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, betas: { key: ONYXKEYS.BETAS, }, diff --git a/src/pages/tasks/NewTaskPage.tsx b/src/pages/tasks/NewTaskPage.tsx index 079fd807096d..44cb485c0b06 100644 --- a/src/pages/tasks/NewTaskPage.tsx +++ b/src/pages/tasks/NewTaskPage.tsx @@ -178,7 +178,7 @@ function NewTaskPage({task, reports, personalDetails}: NewTaskPageProps) { description={shareDestination?.displayName ? shareDestination.subtitle : translate('newTaskPage.shareSomewhere')} icon={shareDestination?.icons} onPress={() => Navigation.navigate(ROUTES.NEW_TASK_SHARE_DESTINATION)} - interactive + interactive={!task?.parentReportID} shouldShowRightIcon={!task?.parentReportID} titleWithTooltips={shareDestination?.shouldUseFullTitleToDisplay ? undefined : shareDestination?.displayNamesWithTooltips} /> diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 4185621398dd..79e265e7f80d 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -209,26 +209,24 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro includeSafeAreaPaddingBottom={false} testID={TaskAssigneeSelectorModal.displayName} > - {({didScreenTransitionEnd}) => ( - - + + + - - - - - )} + + ); } diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 9a1dac648887..b4b8f9084a57 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -48,11 +48,14 @@ const reportFilter = (reportOptions: Array }, []); function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDestinationSelectorModalProps) { + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {options: optionList, areOptionsInitialized} = useOptionsList(); + const {options: optionList, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); const textInputHint = useMemo(() => (isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''), [isOffline, translate]); @@ -64,9 +67,7 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes }; } const filteredReports = reportFilter(optionList.reports); - const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, optionList.personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); - const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue); const sections = @@ -98,29 +99,28 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes setDidScreenTransitionEnd(true)} > - {({didScreenTransitionEnd}) => ( - <> - Navigation.goBack(ROUTES.NEW_TASK)} + <> + Navigation.goBack(ROUTES.NEW_TASK)} + /> + + - - - - - )} + + ); } From 3467a7317c67a4b1185554fa1985fe3d0c9d07f3 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 25 Mar 2024 14:43:20 +0100 Subject: [PATCH 35/41] fix displaying personal details in current user option --- src/libs/OptionsListUtils.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 19dfe80a15e2..af04b8e5cee2 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -176,7 +176,7 @@ type GetOptions = { taxRatesOptions: CategorySection[]; }; -type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; +type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -640,8 +640,7 @@ function createOption( reportActions: ReportActions, config?: PreviewConfig, ): ReportUtils.OptionData { - const {showChatPreviewLine = false, forcePolicyNamePreview = false} = config ?? {}; - + const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false} = config ?? {}; const result: ReportUtils.OptionData = { text: undefined, alternateText: null, @@ -683,10 +682,8 @@ function createOption( let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; let reportName; - result.participantsList = personalDetailList; result.isOptimisticPersonalDetail = personalDetail?.isOptimisticPersonalDetail; - if (report) { result.isChatRoom = ReportUtils.isChatRoom(report); result.isDefaultRoom = ReportUtils.isDefaultRoom(report); @@ -731,11 +728,11 @@ function createOption( result.lastMessageText = lastMessageText; // If displaying chat preview line is needed, let's overwrite the default alternate text - if (showChatPreviewLine || forcePolicyNamePreview) { - result.alternateText = getAlternateTextOption(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); - } + result.alternateText = showPersonalDetails ? personalDetail.login : getAlternateTextOption(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); - reportName = ReportUtils.getReportName(report); + reportName = showPersonalDetails + ? ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') + : ReportUtils.getReportName(report); } else { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); @@ -1428,7 +1425,7 @@ function createOptionList(personalDetails: OnyxEntry, repor const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ item: personalDetail, - ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}), + ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}), })); return { @@ -1620,7 +1617,6 @@ function getOptions( }); const havingLoginPersonalDetails = options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail); - let allPersonalDetailsOptions = havingLoginPersonalDetails; if (sortPersonalDetailsByAlphaAsc) { From cdd26e89107792e495455595c1f7760ec8e3d536 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 25 Mar 2024 15:12:36 +0100 Subject: [PATCH 36/41] update caching behavior in lists --- ...mporaryForRefactorRequestParticipantsSelector.js | 13 +++++-------- src/pages/workspace/WorkspaceInvitePage.tsx | 10 ++++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 142adc8b7159..d67da28a94f4 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -20,7 +20,6 @@ import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -87,7 +86,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const {canUseP2PDistanceRequests} = usePermissions(); - const {options} = useOptionsList({ + const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); @@ -103,7 +102,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ */ const [sections, newChatOptions] = useMemo(() => { const newSections = []; - if (!didScreenTransitionEnd) { + if (!areOptionsInitialized) { return [newSections, {}]; } let indexOffset = 0; @@ -179,7 +178,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ return [newSections, chatOptions]; }, [ - didScreenTransitionEnd, + areOptionsInitialized, options.reports, options.personalDetails, betas, @@ -354,13 +353,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); - const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - return ( 0 ? safeAreaPaddingBottomStyle : {}]}> diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index da6c00472306..e07cf355ba23 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -73,7 +73,9 @@ function WorkspaceInvitePage({ const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; - const {options, areOptionsInitialized} = useOptionsList(); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -166,7 +168,7 @@ function WorkspaceInvitePage({ const sectionsArr: MembersSection[] = []; let indexOffset = 0; - if (!didScreenTransitionEnd) { + if (!areOptionsInitialized) { return []; } @@ -219,7 +221,7 @@ function WorkspaceInvitePage({ }); return sectionsArr; - }, [didScreenTransitionEnd, selectedOptions, searchTerm, personalDetails, translate, usersToInvite]); + }, [areOptionsInitialized, selectedOptions, searchTerm, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { Policy.clearErrors(route.params.policyID); @@ -320,7 +322,7 @@ function WorkspaceInvitePage({ onSelectRow={toggleOption} onConfirm={inviteUser} showScrollIndicator - showLoadingPlaceholder={areOptionsInitialized && searchTerm.trim() === '' ? sections.length === 0 : !didScreenTransitionEnd} + showLoadingPlaceholder={!areOptionsInitialized} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} checkmarkPosition={CONST.DIRECTION.RIGHT} /> From 40dfb81f46af06f91cef84f1c1208bfdf9fd06cf Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 27 Mar 2024 08:38:39 +0100 Subject: [PATCH 37/41] update tagpicker types --- src/components/TagPicker/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index af8acd19e8c4..54ad016173b7 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -91,7 +91,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]); const sections = useMemo( - () => OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions, + () => OptionsListUtils.getFilteredOptions([], [], [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions, [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList], ); From c5770946ed70372a12b8f7dbd3bfc32d2da8199c Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 27 Mar 2024 14:08:27 +0100 Subject: [PATCH 38/41] fix failing reassure tests --- src/libs/OptionsListUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 6eca05188a70..0c97a2cfa537 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -722,7 +722,8 @@ function createOption( result.lastMessageText = lastMessageText; // If displaying chat preview line is needed, let's overwrite the default alternate text - result.alternateText = showPersonalDetails ? personalDetail.login : getAlternateTextOption(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); + result.alternateText = + showPersonalDetails && personalDetail?.login ? personalDetail.login : getAlternateTextOption(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); reportName = showPersonalDetails ? ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') From 304544fe545b8c958f5fe7ea7c827b2b46e323d6 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 29 Mar 2024 08:26:21 +0100 Subject: [PATCH 39/41] lint code --- ...oraryForRefactorRequestParticipantsSelector.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 5ab21c0d8380..d7be3fc87327 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -180,7 +180,20 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [areOptionsInitialized, options.reports, options.personalDetails, betas, debouncedSearchTerm, participants, iouType, canUseP2PDistanceRequests, iouRequestType, maxParticipantsReached, personalDetails, translate]); + }, [ + areOptionsInitialized, + options.reports, + options.personalDetails, + betas, + debouncedSearchTerm, + participants, + iouType, + canUseP2PDistanceRequests, + iouRequestType, + maxParticipantsReached, + personalDetails, + translate, + ]); /** * Adds a single participant to the request From 4865582b2d0abc4500f2284d2943f770526ef9d0 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 29 Mar 2024 08:28:44 +0100 Subject: [PATCH 40/41] fix typecheck --- tests/perf-test/SearchPage.perf-test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/perf-test/SearchPage.perf-test.tsx b/tests/perf-test/SearchPage.perf-test.tsx index 51f162bf2043..95f5630e3fe9 100644 --- a/tests/perf-test/SearchPage.perf-test.tsx +++ b/tests/perf-test/SearchPage.perf-test.tsx @@ -152,7 +152,6 @@ function SearchPageWithCachedOptions(args: SearchPageProps) { } test('[Search Page] should render list with cached options', async () => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. const {addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { From 07d1b7434d61bfa5c6c83468d050bd17efc197c1 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 3 Apr 2024 09:04:29 +0200 Subject: [PATCH 41/41] update function naming --- src/libs/OptionsListUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 36de89049b74..fe5aac388116 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -519,9 +519,9 @@ function getLastActorDisplayName(lastActorDetails: Partial | nu } /** - * Update alternate text option when applicable + * Update alternate text for the option when applicable */ -function getAlternateTextOption( +function getAlternateText( option: ReportUtils.OptionData, {showChatPreviewLine = false, forcePolicyNamePreview = false, lastMessageTextFromReport = ''}: PreviewConfig & {lastMessageTextFromReport?: string}, ) { @@ -710,7 +710,7 @@ function createOption( // If displaying chat preview line is needed, let's overwrite the default alternate text result.alternateText = - showPersonalDetails && personalDetail?.login ? personalDetail.login : getAlternateTextOption(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); + showPersonalDetails && personalDetail?.login ? personalDetail.login : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); reportName = showPersonalDetails ? ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') @@ -1606,7 +1606,7 @@ function getOptions( * By default, generated options does not have the chat preview line enabled. * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text. */ - reportOption.alternateText = getAlternateTextOption(reportOption, {showChatPreviewLine, forcePolicyNamePreview}); + reportOption.alternateText = getAlternateText(reportOption, {showChatPreviewLine, forcePolicyNamePreview}); // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) {