diff --git a/package.json b/package.json index 510f2253647e..2329d629d317 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=668 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=378 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "check-lazy-loading": "ts-node scripts/checkLazyLoading.ts", "lint-watch": "npx eslint-watch --watch --changed", diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index bc5ca52af418..178a8cddaf87 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useRef} from 'react'; import type {RefObject} from 'react'; // eslint-disable-next-line no-restricted-imports import type {ScrollView as RNScrollView, StyleProp, ViewStyle} from 'react-native'; @@ -103,9 +103,9 @@ function FormWrapper({ const [formState] = useOnyx(`${formID}`, {canBeMissing: true}); - const errorMessage = useMemo(() => (formState ? getLatestErrorMessage(formState) : undefined), [formState]); + const errorMessage = formState ? getLatestErrorMessage(formState) : undefined; - const onFixTheErrorsLinkPressed = useCallback(() => { + const onFixTheErrorsLinkPressed = () => { const errorFields = !isEmptyObject(errors) ? errors : (formState?.errorFields ?? {}); const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => key in errorFields); @@ -134,7 +134,7 @@ function FormWrapper({ // Focus the input after scrolling, as on the Web it gives a slightly better visual result focusInput?.focus?.(); - }, [errors, formState?.errorFields, inputRefs]); + }; // If either of `addBottomSafeAreaPadding` or `shouldSubmitButtonStickToBottom` is explicitly set, // we expect that the user wants to use the new edge-to-edge mode. @@ -159,88 +159,53 @@ function FormWrapper({ style: submitButtonStyles, }); - const SubmitButton = useMemo( - () => - isSubmitButtonVisible && ( - - ), - [ - disablePressOnEnter, - enterKeyEventListenerPriority, - enabledWhenOffline, - errorMessage, - errors, - footerContent, - formState?.errorFields, - formState?.isLoading, - isLoading, - isSubmitActionDangerous, - isSubmitButtonVisible, - isSubmitDisabled, - onFixTheErrorsLinkPressed, - onSubmit, - shouldHideFixErrorsAlert, - shouldSubmitButtonBlendOpacity, - shouldSubmitButtonStickToBottom, - style, - styles.flex1, - styles.mh0, - styles.mt5, - styles.stickToBottom, - submitButtonStylesWithBottomSafeAreaPadding, - submitButtonText, - submitFlexEnabled, - shouldRenderFooterAboveSubmit, - shouldPreventDefaultFocusOnPressSubmit, - ], + const SubmitButton = isSubmitButtonVisible && ( + ); - const scrollViewContent = useCallback( - () => ( - { - if (!shouldScrollToEnd) { - return; - } - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - requestAnimationFrame(() => { - formRef.current?.scrollToEnd({animated: true}); - }); + const scrollViewContent = () => ( + { + if (!shouldScrollToEnd) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { + formRef.current?.scrollToEnd({animated: true}); }); - }} - > - {children} - {!shouldSubmitButtonStickToBottom && SubmitButton} - - ), - [formID, style, styles.pb5, children, shouldSubmitButtonStickToBottom, SubmitButton, shouldScrollToEnd], + }); + }} + > + {children} + {!shouldSubmitButtonStickToBottom && SubmitButton} + ); if (!shouldUseScrollView) { diff --git a/src/components/InvertedFlatList/index.e2e.tsx b/src/components/InvertedFlatList/index.e2e.tsx index b0cb84050188..caa08ea9b023 100644 --- a/src/components/InvertedFlatList/index.e2e.tsx +++ b/src/components/InvertedFlatList/index.e2e.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import type {ScrollViewProps, ViewToken} from 'react-native'; import {DeviceEventEmitter, FlatList} from 'react-native'; import type {ReportAction} from '@src/types/onyx'; @@ -9,33 +9,25 @@ const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function InvertedFlatListE2E({ref, ...props}: InvertedFlatListProps) { const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; - const handleViewableItemsChanged = useMemo( - () => - ({viewableItems}: {viewableItems: ViewToken[]}) => { - DeviceEventEmitter.emit('onViewableItemsChanged', viewableItems); - }, - [], - ); - - const maintainVisibleContentPosition = useMemo(() => { - const config: ScrollViewProps['maintainVisibleContentPosition'] = { - // This needs to be 1 to avoid using loading views as anchors. - minIndexForVisible: rest.data?.length ? Math.min(1, rest.data.length - 1) : 0, - }; + const handleViewableItemsChanged = ({viewableItems}: {viewableItems: ViewToken[]}) => { + DeviceEventEmitter.emit('onViewableItemsChanged', viewableItems); + }; - if (shouldEnableAutoScrollToTopThreshold) { - config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; - } + const config: ScrollViewProps['maintainVisibleContentPosition'] = { + // This needs to be 1 to avoid using loading views as anchors. + minIndexForVisible: rest.data?.length ? Math.min(1, rest.data.length - 1) : 0, + }; - return config; - }, [shouldEnableAutoScrollToTopThreshold, rest.data?.length]); + if (shouldEnableAutoScrollToTopThreshold) { + config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; + } return ( // eslint-disable-next-line react/jsx-props-no-spreading {...rest} ref={ref} - maintainVisibleContentPosition={maintainVisibleContentPosition} + maintainVisibleContentPosition={config} inverted onViewableItemsChanged={handleViewableItemsChanged} /> diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx index f7e3bcde883b..21570acb8c5c 100644 --- a/src/components/Navigation/SearchSidebar.tsx +++ b/src/components/Navigation/SearchSidebar.tsx @@ -1,5 +1,5 @@ import type {ParamListBase} from '@react-navigation/native'; -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; import {useSearchContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; @@ -29,23 +29,20 @@ function SearchSidebar({state}: SearchSidebarProps) { const params = route?.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT] | undefined; const {lastSearchType, setLastSearchType, currentSearchResults} = useSearchContext(); - const queryJSON = useMemo(() => { - if (!params?.q) { - return undefined; - } + const queryJSON = params?.q ? buildSearchQueryJSON(params.q, params.rawQuery) : undefined; - return buildSearchQueryJSON(params.q, params.rawQuery); - }, [params?.q, params?.rawQuery]); + const searchType = currentSearchResults?.search?.type; + const isSearchLoading = currentSearchResults?.search?.isLoading; useEffect(() => { - if (!currentSearchResults?.search?.type) { + if (!searchType) { return; } - setLastSearchType(currentSearchResults.search.type); - }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults?.search?.type]); + setLastSearchType(searchType); + }, [lastSearchType, queryJSON, setLastSearchType, searchType]); - const shouldShowLoadingState = route?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT ? false : !isOffline && !!currentSearchResults?.search?.isLoading; + const shouldShowLoadingState = route?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT ? false : !isOffline && !!isSearchLoading; if (shouldUseNarrowLayout) { return null; diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 707944fb0f77..332d6cf56b6a 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -1,5 +1,5 @@ import {createPersonalDetailsSelector} from '@selectors/PersonalDetails'; -import React, {useMemo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import useEnvironment from '@hooks/useEnvironment'; @@ -90,37 +90,20 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK) || moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT); - const reportDetailsLink = useMemo(() => { - if (!report?.reportID) { - return ''; - } - - return `${environmentURL}/${ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, Navigation.getReportRHPActiveRoute())}`; - }, [environmentURL, report?.reportID]); - - const welcomeHeroText = useMemo(() => { - if (isInvoiceRoom) { - return translate('reportActionsView.sayHello'); - } - - if (isChatRoom) { - return translate('reportActionsView.welcomeToRoom', {roomName: reportName}); - } - - if (isSelfDM) { - return translate('reportActionsView.yourSpace'); - } - - if (isSystemChat) { - return reportName; - } - - if (isPolicyExpenseChat) { - return translate('reportActionsView.welcomeToRoom', {roomName: policyName}); - } - - return translate('reportActionsView.sayHello'); - }, [isChatRoom, isInvoiceRoom, isPolicyExpenseChat, isSelfDM, isSystemChat, translate, policyName, reportName]); + const reportDetailsLink = report?.reportID ? `${environmentURL}/${ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, Navigation.getReportRHPActiveRoute())}` : ''; + + let welcomeHeroText = translate('reportActionsView.sayHello'); + if (isInvoiceRoom) { + welcomeHeroText = translate('reportActionsView.sayHello'); + } else if (isChatRoom) { + welcomeHeroText = translate('reportActionsView.welcomeToRoom', {roomName: reportName}); + } else if (isSelfDM) { + welcomeHeroText = translate('reportActionsView.yourSpace'); + } else if (isSystemChat) { + welcomeHeroText = reportName; + } else if (isPolicyExpenseChat) { + welcomeHeroText = translate('reportActionsView.welcomeToRoom', {roomName: policyName}); + } // If we are the only participant (e.g. solo group chat) then keep the current user personal details so the welcome message does not show up empty. const shouldExcludeCurrentUser = participantAccountIDs.length > 0; diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx index afbe31305f33..874021d576e2 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useContext, useMemo} from 'react'; +import React, {useContext} from 'react'; import {View} from 'react-native'; import ActivityIndicator from '@components/ActivityIndicator'; import Button from '@components/Button'; @@ -56,33 +56,22 @@ function TransactionGroupListExpanded({ const [isMobileSelectionModeEnabled] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true}); const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true, selector: columnsSelector}); - const transactionsSnapshotMetadata = useMemo(() => { - return transactionsSnapshot?.search; - }, [transactionsSnapshot?.search]); + const transactionsSnapshotMetadata = transactionsSnapshot?.search; - const visibleTransactions = useMemo(() => { - if (isExpenseReportType) { - return transactions.slice(0, transactionsVisibleLimit); - } - return transactions; - }, [transactions, transactionsVisibleLimit, isExpenseReportType]); + const visibleTransactions = isExpenseReportType ? transactions.slice(0, transactionsVisibleLimit) : transactions; - const isLastTransaction = useCallback( - (index: number) => { - return index === visibleTransactions.length - 1; - }, - [visibleTransactions.length], - ); + const isLastTransaction = (index: number) => { + return index === visibleTransactions.length - 1; + }; - const currentColumns = useMemo(() => { - if (isExpenseReportType) { - return columns ?? []; - } + let currentColumns = columns ?? []; + if (!isExpenseReportType) { if (!transactionsSnapshot?.data) { - return []; + currentColumns = []; + } else { + currentColumns = getColumnsToShow(accountID, transactionsSnapshot?.data, visibleColumns, false, transactionsSnapshot?.search.type); } - return getColumnsToShow(accountID, transactionsSnapshot?.data, visibleColumns, false, transactionsSnapshot?.search.type); - }, [accountID, columns, isExpenseReportType, transactionsSnapshot?.data, transactionsSnapshot?.search.type, visibleColumns]); + } // Currently only the transaction report groups have transactions where the empty view makes sense const shouldDisplayShowMoreButton = isExpenseReportType ? transactions.length > transactionsVisibleLimit : !!transactionsSnapshotMetadata?.hasMoreResults && !isOffline; @@ -91,16 +80,12 @@ function TransactionGroupListExpanded({ const shouldDisplayLoadingIndicator = !isExpenseReportType && !!transactionsSnapshotMetadata?.isLoading && shouldShowLoadingOnSearch; const {isLargeScreenWidth} = useResponsiveLayout(); - const {amountColumnSize, dateColumnSize, taxAmountColumnSize} = useMemo(() => { - const isAmountColumnWide = transactions.some((transaction) => transaction.isAmountColumnWide); - const isTaxAmountColumnWide = transactions.some((transaction) => transaction.isTaxAmountColumnWide); - const shouldShowYearForSomeTransaction = transactions.some((transaction) => transaction.shouldShowYear); - return { - amountColumnSize: isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - taxAmountColumnSize: isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - dateColumnSize: shouldShowYearForSomeTransaction ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - }; - }, [transactions]); + const isAmountColumnWide = transactions.some((transaction) => transaction.isAmountColumnWide); + const isTaxAmountColumnWide = transactions.some((transaction) => transaction.isTaxAmountColumnWide); + const shouldShowYearForSomeTransaction = transactions.some((transaction) => transaction.shouldShowYear); + const amountColumnSize = isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const taxAmountColumnSize = isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const dateColumnSize = shouldShowYearForSomeTransaction ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; const {markReportIDAsExpense} = useContext(WideRHPContext); const openReportInRHP = (transactionItem: TransactionListItemType) => { @@ -134,13 +119,13 @@ function TransactionGroupListExpanded({ }); }; - const onShowMoreButtonPress = useCallback(() => { + const onShowMoreButtonPress = () => { if (isExpenseReportType) { setTransactionsVisibleLimit((currentPageSize) => currentPageSize + CONST.TRANSACTION.RESULTS_PAGE_SIZE); } else if (!isOffline && transactionsQueryJSON) { searchTransactions(CONST.SEARCH.RESULTS_PAGE_SIZE); } - }, [isExpenseReportType, isOffline, transactionsQueryJSON, setTransactionsVisibleLimit, searchTransactions]); + }; if (shouldDisplayEmptyView) { return ( diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx index 7568e399d04e..21331dd7d5cf 100644 --- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useContext, useMemo, useRef} from 'react'; +import React, {useContext, useRef} from 'react'; import type {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; // Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot @@ -56,15 +56,14 @@ function TransactionListItem({ const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {currentSearchHash, currentSearchKey, currentSearchResults} = useSearchContext(); - const snapshotReport = useMemo(() => { - return (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as Report; - }, [currentSearchResults, transactionItem.reportID]); + const snapshotReport = (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as Report; const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector}); // Use active policy (user's current workspace) as fallback for self DM tracking expenses // This matches MoneyRequestView's approach via usePolicyForMovingExpenses() const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + // Use report's policyID as fallback when transaction doesn't have policyID directly // Use active policy as final fallback for SelfDM (tracking expenses) // NOTE: Using || instead of ?? to treat empty string "" as falsy @@ -74,14 +73,10 @@ function TransactionListItem({ canBeMissing: true, selector: (policy) => policy?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`], }); - const snapshotPolicy = useMemo(() => { - return (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy; - }, [currentSearchResults, transactionItem.policyID]); + const snapshotPolicy = (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy; - const exportedReportActions = useMemo(() => { - const actionsData = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionItem.reportID}`]; - return actionsData ? Object.values(actionsData) : []; - }, [currentSearchResults, transactionItem.reportID]); + const actionsData = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionItem.reportID}`]; + const exportedReportActions = actionsData ? Object.values(actionsData) : []; // Fetch policy categories directly from Onyx since they are not included in the search snapshot const [policyCategories] = originalUseOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(policyID)}`, {canBeMissing: true}); @@ -91,20 +86,19 @@ function TransactionListItem({ const [parentReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transactionItem.reportID)}`, {canBeMissing: true}); const [transactionThreadReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionItem?.reportAction?.childReportID}`, {canBeMissing: true}); const [transaction] = originalUseOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionItem.transactionID)}`, {canBeMissing: true}); - const parentReportActionSelector = useCallback( - (reportActions: OnyxEntry): OnyxEntry => reportActions?.[`${transactionItem?.reportAction?.reportActionID}`], - [transactionItem?.reportAction?.reportActionID], - ); + const parentReportActionSelector = (reportActions: OnyxEntry): OnyxEntry => reportActions?.[`${transactionItem?.reportAction?.reportActionID}`]; const [parentReportAction] = originalUseOnyx( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(transactionItem.reportID)}`, {selector: parentReportActionSelector, canBeMissing: true}, [transactionItem], ); const currentUserDetails = useCurrentUserPersonalDetails(); - const transactionPreviewData: TransactionPreviewData = useMemo( - () => ({hasParentReport: !!parentReport, hasTransaction: !!transaction, hasParentReportAction: !!parentReportAction, hasTransactionThreadReport: !!transactionThreadReport}), - [parentReport, transaction, parentReportAction, transactionThreadReport], - ); + const transactionPreviewData: TransactionPreviewData = { + hasParentReport: !!parentReport, + hasTransaction: !!transaction, + hasParentReportAction: !!parentReportAction, + hasTransactionThreadReport: !!transactionThreadReport, + }; const pressableStyle = [ styles.transactionListItemStyle, @@ -120,25 +114,13 @@ function TransactionListItem({ backgroundColor: theme.highlightBG, }); - const {amountColumnSize, dateColumnSize, taxAmountColumnSize, submittedColumnSize, approvedColumnSize, postedColumnSize, exportedColumnSize} = useMemo(() => { - return { - amountColumnSize: transactionItem.isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - taxAmountColumnSize: transactionItem.isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - dateColumnSize: transactionItem.shouldShowYear ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - submittedColumnSize: transactionItem.shouldShowYearSubmitted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - approvedColumnSize: transactionItem.shouldShowYearApproved ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - postedColumnSize: transactionItem.shouldShowYearPosted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - exportedColumnSize: transactionItem.shouldShowYearExported ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - }; - }, [ - transactionItem.isAmountColumnWide, - transactionItem.isTaxAmountColumnWide, - transactionItem.shouldShowYear, - transactionItem.shouldShowYearSubmitted, - transactionItem.shouldShowYearApproved, - transactionItem.shouldShowYearPosted, - transactionItem.shouldShowYearExported, - ]); + const amountColumnSize = transactionItem.isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const taxAmountColumnSize = transactionItem.isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const dateColumnSize = transactionItem.shouldShowYear ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const submittedColumnSize = transactionItem.shouldShowYearSubmitted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const approvedColumnSize = transactionItem.shouldShowYearApproved ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const postedColumnSize = transactionItem.shouldShowYearPosted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const exportedColumnSize = transactionItem.shouldShowYearExported ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; // Prefer live Onyx policy data over snapshot to ensure fresh policy settings // like isAttendeeTrackingEnabled is not missing @@ -147,31 +129,29 @@ function TransactionListItem({ const policyForViolations = parentPolicy ?? snapshotPolicy; const reportForViolations = parentReport ?? snapshotReport; - const transactionViolations = useMemo(() => { - const onyxViolations = (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter( - (violation: TransactionViolation) => - !isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, reportForViolations, policyForViolations) && - shouldShowViolation(reportForViolations, policyForViolations, violation.name, currentUserDetails.email ?? '', false, transactionItem), - ); + const onyxViolations = (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter( + (violation: TransactionViolation) => + !isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, reportForViolations, policyForViolations) && + shouldShowViolation(reportForViolations, policyForViolations, violation.name, currentUserDetails.email ?? '', false, transactionItem), + ); - // Sync missingAttendees violation with current policy category settings (can be removed later when BE handles this) - // Use live transaction data (attendees, category) to ensure we check against current state, not stale snapshot - const attendeeOnyxViolations = syncMissingAttendeesViolation( - onyxViolations, - policyCategories, - transaction?.category ?? transactionItem.category ?? '', - transaction?.comment?.attendees ?? transactionItem.attendees, - currentUserDetails, - policyForViolations?.isAttendeeTrackingEnabled ?? false, - policyForViolations?.type === CONST.POLICY.TYPE.CORPORATE, - ); + // Sync missingAttendees violation with current policy category settings (can be removed later when BE handles this) + // Use live transaction data (attendees, category) to ensure we check against current state, not stale snapshot + const attendeeOnyxViolations = syncMissingAttendeesViolation( + onyxViolations, + policyCategories, + transaction?.category ?? transactionItem.category ?? '', + transaction?.comment?.attendees ?? transactionItem.attendees, + currentUserDetails, + policyForViolations?.isAttendeeTrackingEnabled ?? false, + policyForViolations?.type === CONST.POLICY.TYPE.CORPORATE, + ); - return mergeProhibitedViolations(attendeeOnyxViolations); - }, [policyForViolations, reportForViolations, policyCategories, transactionItem, currentUserDetails, violations]); + const transactionViolations = mergeProhibitedViolations(attendeeOnyxViolations); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); - const handleActionButtonPress = useCallback(() => { + const handleActionButtonPress = () => { handleActionButtonPressUtil({ hash: currentSearchHash, item: transactionItem, @@ -186,34 +166,7 @@ function TransactionListItem({ onDelegateAccessRestricted: showDelegateNoAccessModal, personalPolicyID, }); - }, [ - currentSearchHash, - transactionItem, - transactionPreviewData, - snapshotReport, - snapshotPolicy, - lastPaymentMethod, - personalPolicyID, - currentSearchKey, - onSelectRow, - item, - onDEWModalOpen, - isDEWBetaEnabled, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - ]); - - const handleCheckboxPress = useCallback(() => { - onCheckboxPress?.(item); - }, [item, onCheckboxPress]); - - const onPress = useCallback(() => { - onSelectRow(item, transactionPreviewData); - }, [item, onSelectRow, transactionPreviewData]); - - const onLongPress = useCallback(() => { - onLongPressRow?.(item); - }, [item, onLongPressRow]); + }; const StyleUtils = useStyleUtils(); const pressableRef = useRef(null); @@ -224,8 +177,8 @@ function TransactionListItem({ onLongPressRow?.(item)} + onPress={() => onSelectRow(item, transactionPreviewData)} disabled={isDisabled && !item.isSelected} accessibilityLabel={item.text ?? ''} role={getButtonRole(true)} @@ -256,7 +209,7 @@ function TransactionListItem({ report={transactionItem.report} shouldShowTooltip={showTooltip} onButtonPress={handleActionButtonPress} - onCheckboxPress={handleCheckboxPress} + onCheckboxPress={() => onCheckboxPress?.(item)} shouldUseNarrowLayout={!isLargeScreenWidth} columns={columns} isActionLoading={isLoading ?? isActionLoading} @@ -271,7 +224,7 @@ function TransactionListItem({ shouldShowCheckbox={!!canSelectMultiple} style={[styles.p3, styles.pv2, shouldUseNarrowLayout ? styles.pt2 : {}]} violations={transactionViolations} - onArrowRightPress={onPress} + onArrowRightPress={() => onSelectRow(item, transactionPreviewData)} isHover={hovered} customCardNames={customCardNames} reportActions={exportedReportActions} diff --git a/src/components/WorkspaceMembersSelectionList.tsx b/src/components/WorkspaceMembersSelectionList.tsx index 3bf855ae4d1f..ec60837ad5a9 100644 --- a/src/components/WorkspaceMembersSelectionList.tsx +++ b/src/components/WorkspaceMembersSelectionList.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import useDebouncedState from '@hooks/useDebouncedState'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -43,67 +43,53 @@ function WorkspaceMembersSelectionList({policyID, selectedApprover, setApprover} const policy = usePolicy(policyID); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); - const orderedApprovers = useMemo(() => { - const approvers: SelectionListApprover[] = []; - - if (policy?.employeeList) { - const availableApprovers = Object.values(policy.employeeList) - .map((employee): SelectionListApprover | null => { - const email = employee.email; - - if (!email || employee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - return null; - } - - const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList); - const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); - const {avatar, displayName = email, login} = personalDetails?.[accountID] ?? {}; - - return { - text: displayName, - alternateText: email, - keyForList: email, - isSelected: selectedApprover === email, - login: email, - icons: [{source: avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}], - rightElement: ( - - ), - }; - }) - .filter((approver): approver is SelectionListApprover => !!approver); - - approvers.push(...availableApprovers); + const approvers: SelectionListApprover[] = []; + + if (policy?.employeeList) { + for (const employee of Object.values(policy.employeeList)) { + const email = employee.email; + + if (!email || employee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + continue; + } + + const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList); + const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); + const {avatar, displayName = email, login} = personalDetails?.[accountID] ?? {}; + + approvers.push({ + text: displayName, + alternateText: email, + keyForList: email, + isSelected: selectedApprover === email, + login: email, + icons: [{source: avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}], + rightElement: ( + + ), + }); } + } - const filteredApprovers = tokenizedSearch(approvers, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (approver) => [approver.text ?? '', approver.login ?? '']); - - return sortAlphabetically(filteredApprovers, 'text', localeCompare); - }, [policy?.employeeList, policy?.owner, debouncedSearchTerm, countryCode, localeCompare, personalDetails, selectedApprover, icons.FallbackAvatar]); + const filteredApprovers = tokenizedSearch(approvers, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (approver) => [approver.text ?? '', approver.login ?? '']); + const orderedApprovers = sortAlphabetically(filteredApprovers, 'text', localeCompare); - const handleOnSelectRow = (approver: SelectionListApprover) => { - setApprover(approver.login); + const textInputOptions = { + label: translate('selectionList.nameEmailOrPhoneNumber'), + value: searchTerm, + headerMessage: searchTerm && !orderedApprovers.length ? translate('common.noResultsFound') : '', + onChangeText: setSearchTerm, }; - const textInputOptions = useMemo( - () => ({ - label: translate('selectionList.nameEmailOrPhoneNumber'), - value: searchTerm, - headerMessage: searchTerm && !orderedApprovers.length ? translate('common.noResultsFound') : '', - onChangeText: setSearchTerm, - }), - [searchTerm, orderedApprovers.length, setSearchTerm, translate], - ); - return ( setApprover(approver.login)} textInputOptions={textInputOptions} showLoadingPlaceholder={!didScreenTransitionEnd} shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} diff --git a/src/hooks/useBulkPayOptions.ts b/src/hooks/useBulkPayOptions.ts index 8beeb9099f2f..29b70bea45be 100644 --- a/src/hooks/useBulkPayOptions.ts +++ b/src/hooks/useBulkPayOptions.ts @@ -1,5 +1,4 @@ import truncate from 'lodash/truncate'; -import {useCallback, useMemo} from 'react'; import type {TupleToUnion} from 'type-fest'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import type {BankAccountMenuItem} from '@components/Search/types'; @@ -99,72 +98,65 @@ function useBulkPayOptions({ }); } - const getPaymentSubitems = useCallback( - (payAsBusiness: boolean) => { - const requiredAccountType = payAsBusiness ? CONST.BANK_ACCOUNT.TYPE.BUSINESS : CONST.BANK_ACCOUNT.TYPE.PERSONAL; - - return formattedPaymentMethods - .filter((method) => { - const accountData = method?.accountData as AccountData; - return accountData?.type === requiredAccountType; - }) - .map((formattedPaymentMethod) => ({ - text: formattedPaymentMethod?.title ?? '', - description: formattedPaymentMethod?.description ?? '', - icon: formattedPaymentMethod?.icon, - shouldUpdateSelectedIndex: true, - iconStyles: formattedPaymentMethod?.iconStyles, - iconHeight: formattedPaymentMethod?.iconSize, - iconWidth: formattedPaymentMethod?.iconSize, - key: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, - additionalData: { - payAsBusiness, - methodID: formattedPaymentMethod.methodID, - paymentMethod: formattedPaymentMethod.accountType, - }, - })); - }, - [formattedPaymentMethods], - ); + const getPaymentSubitems = (payAsBusiness: boolean) => { + const requiredAccountType = payAsBusiness ? CONST.BANK_ACCOUNT.TYPE.BUSINESS : CONST.BANK_ACCOUNT.TYPE.PERSONAL; + return formattedPaymentMethods + .filter((method) => { + const accountData = method?.accountData as AccountData; + return accountData?.type === requiredAccountType; + }) + .map((formattedPaymentMethod) => ({ + text: formattedPaymentMethod?.title ?? '', + description: formattedPaymentMethod?.description ?? '', + icon: formattedPaymentMethod?.icon, + shouldUpdateSelectedIndex: true, + iconStyles: formattedPaymentMethod?.iconStyles, + iconHeight: formattedPaymentMethod?.iconSize, + iconWidth: formattedPaymentMethod?.iconSize, + key: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + additionalData: { + payAsBusiness, + methodID: formattedPaymentMethod.methodID, + paymentMethod: formattedPaymentMethod.accountType, + }, + })); + }; const latestBankItems = getLatestBankAccountItem(); const personalBankAccountList = formattedPaymentMethods.filter((ba) => (ba.accountData as AccountData)?.type === CONST.BANK_ACCOUNT.TYPE.PERSONAL); - const bulkPayButtonOptions = useMemo(() => { - const buttonOptions = []; - - if (!selectedReportID || !selectedPolicyID) { - return undefined; - } - - if (onlyShowPayElsewhere) { - return [paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]]; - } + let bulkPayButtonOptions; + if (!selectedReportID || !selectedPolicyID) { + bulkPayButtonOptions = undefined; + } else if (onlyShowPayElsewhere) { + bulkPayButtonOptions = [paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]]; + } else { + bulkPayButtonOptions = []; if (shouldShowBusinessBankAccountOptions) { - buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]); + bulkPayButtonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]); } if (canUseWallet) { if (personalBankAccountList.length && canUsePersonalBankAccount) { - buttonOptions.push({ + bulkPayButtonOptions.push({ text: translate('iou.settleWallet', {formattedAmount: ''}), key: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, icon: icons.Wallet, }); } else if (canUsePersonalBankAccount) { - buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]); + bulkPayButtonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]); } if (activeAdminPolicies.length === 0 && !isPersonalOnlyOption) { - buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]); + bulkPayButtonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]); } } if ((hasMultiplePolicies || hasSinglePolicy) && canUseWallet && !isPersonalOnlyOption) { for (const activePolicy of activeAdminPolicies) { const policyName = activePolicy.name; - buttonOptions.push({ + bulkPayButtonOptions.push({ text: translate('iou.payWithPolicy', {policyName: truncate(policyName, {length: CONST.ADDITIONAL_ALLOWED_CHARACTERS}), formattedAmount: ''}), icon: icons.Building, key: activePolicy.id, @@ -174,7 +166,7 @@ function useBulkPayOptions({ } if (shouldShowPayElsewhereOption) { - buttonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]); + bulkPayButtonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]); } if (isInvoiceReport) { @@ -203,13 +195,13 @@ function useBulkPayOptions({ }; if (isIndividualInvoiceRoomUtil(chatReport)) { - buttonOptions.push({ + bulkPayButtonOptions.push({ text: translate('iou.settlePersonal', {formattedAmount}), icon: icons.User, backButtonText: translate('iou.individual'), subMenuItems: getInvoicesOptions(false), }); - buttonOptions.push({ + bulkPayButtonOptions.push({ text: translate('iou.settleBusiness', {formattedAmount}), icon: icons.Building, backButtonText: translate('iou.business'), @@ -217,34 +209,10 @@ function useBulkPayOptions({ }); } else { // If there is pay as business option, we should show the submenu items instead. - buttonOptions.push(...getInvoicesOptions(true)); + bulkPayButtonOptions.push(...getInvoicesOptions(true)); } } - - return buttonOptions; - }, [ - translate, - icons.Building, - icons.User, - selectedReportID, - selectedPolicyID, - shouldShowBusinessBankAccountOptions, - canUseWallet, - hasMultiplePolicies, - hasSinglePolicy, - isPersonalOnlyOption, - shouldShowPayElsewhereOption, - isInvoiceReport, - paymentMethods, - personalBankAccountList.length, - canUsePersonalBankAccount, - activeAdminPolicies, - currency, - chatReport, - getPaymentSubitems, - formattedAmount, - onlyShowPayElsewhere, - ]); + } return { bulkPayButtonOptions, diff --git a/src/hooks/useCardFeedsForDisplay.ts b/src/hooks/useCardFeedsForDisplay.ts index ec83214640f2..0248d41d9f6f 100644 --- a/src/hooks/useCardFeedsForDisplay.ts +++ b/src/hooks/useCardFeedsForDisplay.ts @@ -1,4 +1,3 @@ -import {useMemo} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import {getCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy} from '@libs/CardFeedUtils'; import {filterPersonalCards, isCustomFeed, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; @@ -27,40 +26,42 @@ const useCardFeedsForDisplay = () => { canBeMissing: true, }); - const cardFeedsByPolicy = useMemo(() => getCardFeedsForDisplayPerPolicy(allFeeds), [allFeeds]); - - const defaultCardFeed = useMemo(() => { - if (!eligiblePoliciesIDs) { - return undefined; - } + const cardFeedsByPolicy = getCardFeedsForDisplayPerPolicy(allFeeds); + let defaultCardFeed; + if (eligiblePoliciesIDs) { // Prioritize the active policy if eligible if (activePolicyID && eligiblePoliciesIDs.has(activePolicyID)) { const policyCardFeeds = cardFeedsByPolicy[activePolicyID]; if (policyCardFeeds?.length) { - return policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); + defaultCardFeed = policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); } } - // If the active policy doesn't have card feeds, use the first eligible policy that does - for (const eligiblePolicyID of eligiblePoliciesIDs) { - const policyCardFeeds = cardFeedsByPolicy[eligiblePolicyID]; - if (policyCardFeeds?.length) { - return policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); + if (!defaultCardFeed) { + // If the active policy doesn't have card feeds, use the first eligible policy that does + for (const eligiblePolicyID of eligiblePoliciesIDs) { + const policyCardFeeds = cardFeedsByPolicy[eligiblePolicyID]; + if (policyCardFeeds?.length) { + defaultCardFeed = policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); + break; + } } } - // Commercial feeds don't have preferred policies, so we need to include these in the list - const commercialFeeds = Object.values(cardFeedsByPolicy) - .flat() - .filter((feed) => !isCustomFeed(feed.name as CompanyCardFeed)); + if (!defaultCardFeed) { + // Commercial feeds don't have preferred policies, so we need to include these in the list + const commercialFeeds = Object.values(cardFeedsByPolicy) + .flat() + .filter((feed) => !isCustomFeed(feed.name as CompanyCardFeed)); - return commercialFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); - }, [eligiblePoliciesIDs, activePolicyID, cardFeedsByPolicy, localeCompare]); + defaultCardFeed = commercialFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); + } + } const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); + const allCards = mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList); const expensifyCards = getCardFeedsForDisplay({}, allCards); const defaultExpensifyCard = Object.values(expensifyCards)?.at(0); diff --git a/src/hooks/useIsPaidPolicyAdmin.ts b/src/hooks/useIsPaidPolicyAdmin.ts index fcdfdc8e11dc..dcbef1526eaf 100644 --- a/src/hooks/useIsPaidPolicyAdmin.ts +++ b/src/hooks/useIsPaidPolicyAdmin.ts @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -12,12 +11,9 @@ import useOnyx from './useOnyx'; function useIsPaidPolicyAdmin() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const isUserPaidPolicyAdminSelector = useCallback( - (policies: OnyxCollection) => { - return Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserPersonalDetails.login)); - }, - [currentUserPersonalDetails?.login], - ); + const isUserPaidPolicyAdminSelector = (policies: OnyxCollection) => { + return Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserPersonalDetails.login)); + }; const [isCurrentUserPolicyAdmin = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { canBeMissing: true, diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index bb2826105dc4..87d08877018a 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -1,5 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; -import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SearchQueryJSON} from '@components/Search/types'; @@ -51,36 +51,22 @@ function useSearchHighlightAndScroll({ const initializedRef = useRef(false); const hasPendingSearchRef = useRef(false); const isChat = queryJSON.type === CONST.SEARCH.DATA_TYPES.CHAT; + const searchResultsData = searchResults?.data; - const existingSearchResultIDs = useMemo(() => { - if (!searchResults?.data) { - return []; - } - return isChat ? extractReportActionIDsFromSearchResults(searchResults.data) : extractTransactionIDsFromSearchResults(searchResults.data); - }, [searchResults?.data, isChat]); - - const newTransactions = useMemo(() => { - const previousTransactionsIDs = Object.keys(previousTransactions ?? {}); - - if (previousTransactionsIDs.length === 0) { - return []; - } - - const previousIDs = new Set(previousTransactionsIDs); - const result: Transaction[] = []; - + const prevTransactionsIDs = Object.keys(previousTransactions ?? {}); + const newTransactions: Transaction[] = []; + if (prevTransactionsIDs.length > 0) { + const previousIDs = new Set(prevTransactionsIDs); for (const [id, transaction] of Object.entries(transactions ?? {})) { if (!previousIDs.has(id) && transaction) { - result.push(transaction); + newTransactions.push(transaction); } } - - return result; - }, [previousTransactions, transactions]); + } // Trigger search when a new report action is added while on chat or when a new transaction is added for the other search types. useEffect(() => { - const previousTransactionsIDs = Object.keys(previousTransactions ?? {}); + const previousTransactionIDsLocal = Object.keys(previousTransactions ?? {}); const transactionsIDs = Object.keys(transactions ?? {}); const reportActionsIDs = Object.values(reportActions ?? {}) @@ -92,13 +78,13 @@ function useSearchHighlightAndScroll({ // Only proceed if we have previous data to compare against // This prevents triggering on initial data load - if ((previousTransactionsIDs.length === 0 && previousReportActionsIDs.length === 0) || searchTriggeredRef.current) { + if ((previousTransactionIDsLocal.length === 0 && previousReportActionsIDs.length === 0) || searchTriggeredRef.current) { return; } - const previousTransactionsIDsSet = new Set(previousTransactionsIDs); + const previousTransactionsIDsSet = new Set(previousTransactionIDsLocal); const previousReportActionsIDsSet = new Set(previousReportActionsIDs); - const hasTransactionsIDsChange = transactionsIDs.length !== previousTransactionsIDs.length || transactionsIDs.some((id) => !previousTransactionsIDsSet.has(id)); + const hasTransactionsIDsChange = transactionsIDs.length !== previousTransactionIDsLocal.length || transactionsIDs.some((id) => !previousTransactionsIDsSet.has(id)); const hasReportActionsIDsChange = reportActionsIDs.some((id) => !previousReportActionsIDsSet.has(id)); // Check if there is a change in the transactions or report actions list @@ -111,14 +97,18 @@ function useSearchHighlightAndScroll({ hasPendingSearchRef.current = false; const newIDs = isChat ? reportActionsIDs : transactionsIDs; - const existingSearchResultIDsSet = new Set(existingSearchResultIDs); + let currentSearchResultIDs: string[] = []; + if (searchResultsData) { + currentSearchResultIDs = isChat ? extractReportActionIDsFromSearchResults(searchResultsData) : extractTransactionIDsFromSearchResults(searchResultsData); + } + const existingSearchResultIDsSet = new Set(currentSearchResultIDs); const hasAGenuinelyNewID = newIDs.some((id) => !existingSearchResultIDsSet.has(id)); // Only skip search if there are no new items AND search results aren't empty // This ensures deletions that result in empty data still trigger search - if (!hasAGenuinelyNewID && existingSearchResultIDs.length > 0) { + if (!hasAGenuinelyNewID && currentSearchResultIDs.length > 0) { const newIDsSet = new Set(newIDs); - const hasDeletedID = existingSearchResultIDs.some((id) => !newIDsSet.has(id)); + const hasDeletedID = currentSearchResultIDs.some((id) => !newIDsSet.has(id)); if (!hasDeletedID) { return; } @@ -126,7 +116,7 @@ function useSearchHighlightAndScroll({ // We only want to highlight new items if the addition of transactions or report actions triggered the search. // This is because, on deletion of items, the backend sometimes returns old items in place of the deleted ones. // We don't want to highlight these old items, even if they appear new in the current search results. - hasNewItemsRef.current = isChat ? reportActionsIDs.length > previousReportActionsIDs.length : transactionsIDs.length > previousTransactionsIDs.length; + hasNewItemsRef.current = isChat ? reportActionsIDs.length > previousReportActionsIDs.length : transactionsIDs.length > previousTransactionIDsLocal.length; // Set the flag indicating the search is triggered by the hook triggeredByHookRef.current = true; @@ -151,8 +141,7 @@ function useSearchHighlightAndScroll({ reportActions, previousReportActions, isChat, - searchResults?.data, - existingSearchResultIDs, + searchResultsData, isOffline, searchResults?.search?.isLoading, ]); @@ -167,13 +156,14 @@ function useSearchHighlightAndScroll({ // Initialize the set with existing IDs only once useEffect(() => { - if (initializedRef.current || !searchResults?.data) { + if (initializedRef.current || !searchResultsData) { return; } - highlightedIDs.current = new Set(existingSearchResultIDs); + const initialIDs = isChat ? extractReportActionIDsFromSearchResults(searchResultsData) : extractTransactionIDsFromSearchResults(searchResultsData); + highlightedIDs.current = new Set(initialIDs); initializedRef.current = true; - }, [searchResults?.data, isChat, existingSearchResultIDs]); + }, [searchResultsData, isChat]); // Detect new items (transactions or report actions) useEffect(() => { @@ -235,51 +225,48 @@ function useSearchHighlightAndScroll({ /** * Callback to handle scrolling to the new search result. */ - const handleSelectionListScroll = useCallback( - (data: SearchListItem[], ref: SelectionListHandle | null) => { - // Early return if there's no ref, new transaction wasn't brought in by this hook - // or there's no new search result key - const newSearchResultKey = newSearchResultKeys?.values().next().value; - if (!ref || !triggeredByHookRef.current || !newSearchResultKey) { - return; - } + const handleSelectionListScroll = (data: SearchListItem[], ref: SelectionListHandle | null) => { + // Early return if there's no ref, new transaction wasn't brought in by this hook + // or there's no new search result key + const newSearchResultKey = newSearchResultKeys?.values().next().value; + if (!ref || !triggeredByHookRef.current || !newSearchResultKey) { + return; + } - // Extract the transaction/report action ID from the newSearchResultKey - const newID = newSearchResultKey.replace(isChat ? ONYXKEYS.COLLECTION.REPORT_ACTIONS : ONYXKEYS.COLLECTION.TRANSACTION, ''); - - // Find the index of the new transaction/report action in the data array - const indexOfNewItem = data.findIndex((item) => { - if (isChat) { - if ('reportActionID' in item && item.reportActionID === newID) { - return true; - } - } else { - // Handle TransactionListItemType - if ('transactionID' in item && item.transactionID === newID) { - return true; - } - - // Handle TransactionGroupListItemType with transactions array - if ('transactions' in item && Array.isArray(item.transactions)) { - return item.transactions.some((transaction) => transaction?.transactionID === newID); - } - } + // Extract the transaction/report action ID from the newSearchResultKey + const newID = newSearchResultKey.replace(isChat ? ONYXKEYS.COLLECTION.REPORT_ACTIONS : ONYXKEYS.COLLECTION.TRANSACTION, ''); - return false; - }); + // Find the index of the new transaction/report action in the data array + const indexOfNewItem = data.findIndex((item) => { + if (isChat) { + if ('reportActionID' in item && item.reportActionID === newID) { + return true; + } + } else { + // Handle TransactionListItemType + if ('transactionID' in item && item.transactionID === newID) { + return true; + } - // Early return if the new item is not found in the data array - if (indexOfNewItem <= 0) { - return; + // Handle TransactionGroupListItemType with transactions array + if ('transactions' in item && Array.isArray(item.transactions)) { + return item.transactions.some((transaction) => transaction?.transactionID === newID); + } } - // Perform the scrolling action - ref.scrollToIndex(indexOfNewItem); - // Reset the trigger flag to prevent unintended future scrolls and highlights - triggeredByHookRef.current = false; - }, - [newSearchResultKeys, isChat], - ); + return false; + }); + + // Early return if the new item is not found in the data array + if (indexOfNewItem <= 0) { + return; + } + + // Perform the scrolling action + ref.scrollToIndex(indexOfNewItem); + // Reset the trigger flag to prevent unintended future scrolls and highlights + triggeredByHookRef.current = false; + }; return {newSearchResultKeys, handleSelectionListScroll, newTransactions}; } diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 6a640914a309..cb7d120b0436 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -1,4 +1,5 @@ -import {useCallback, useMemo, useState} from 'react'; +import {useState} from 'react'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import {useSearchContext} from '@components/Search/SearchContext'; import {initSplitExpense} from '@libs/actions/IOU'; @@ -80,59 +81,44 @@ function useSelectedTransactionsActions({ const isReportArchived = useReportIsArchived(report?.reportID); const {deleteTransactions} = useDeleteTransactions({report, reportActions, policy}); const {login} = useCurrentUserPersonalDetails(); - const selectedTransactionsList = useMemo( - () => - selectedTransactionIDs.reduce((acc, transactionID) => { - const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (transaction) { - acc.push(transaction); - } - return acc; - }, [] as Transaction[]), - [allTransactions, selectedTransactionIDs], - ); - const hasTransactionsFromMultipleOwners = useMemo(() => { - const knownOwnerIDs = new Set(); - let hasUnknownOwner = false; - - for (const selectedTransactionInfo of Object.values(selectedTransactionsMeta ?? {})) { - const ownerAccountID = selectedTransactionInfo?.ownerAccountID; - if (typeof ownerAccountID === 'number') { - knownOwnerIDs.add(ownerAccountID); - if (knownOwnerIDs.size > 1) { - return true; - } - } else { - hasUnknownOwner = true; - } + const selectedTransactionsList = selectedTransactionIDs.reduce((acc, transactionID) => { + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + if (transaction) { + acc.push(transaction); } + return acc; + }, [] as Transaction[]); + + const knownOwnerIDs = new Set(); + let hasUnknownOwner = false; + + for (const selectedTransactionInfo of Object.values(selectedTransactionsMeta ?? {})) { + const ownerAccountID = selectedTransactionInfo?.ownerAccountID; + if (typeof ownerAccountID === 'number') { + knownOwnerIDs.add(ownerAccountID); + } else { + hasUnknownOwner = true; + } + } - for (const selectedTransaction of selectedTransactionsList) { - const reportID = selectedTransaction?.reportID; - if (!reportID || reportID === CONST.REPORT.UNREPORTED_REPORT_ID) { - hasUnknownOwner = true; - continue; - } - - const parentReport = getReportOrDraftReport(reportID); - const ownerAccountID = parentReport?.ownerAccountID; - - if (typeof ownerAccountID === 'number') { - knownOwnerIDs.add(ownerAccountID); - if (knownOwnerIDs.size > 1) { - return true; - } - } else { - hasUnknownOwner = true; - } + for (const selectedTransaction of selectedTransactionsList) { + const reportID = selectedTransaction?.reportID; + if (!reportID || reportID === CONST.REPORT.UNREPORTED_REPORT_ID) { + hasUnknownOwner = true; + continue; } - if (hasUnknownOwner) { - return knownOwnerIDs.size > 0 || selectedTransactionIDs.length > 1; + const parentReport = getReportOrDraftReport(reportID); + const ownerAccountID = parentReport?.ownerAccountID; + + if (typeof ownerAccountID === 'number') { + knownOwnerIDs.add(ownerAccountID); + } else { + hasUnknownOwner = true; } + } - return false; - }, [selectedTransactionsList, selectedTransactionsMeta, selectedTransactionIDs.length]); + const hasTransactionsFromMultipleOwners = hasUnknownOwner ? knownOwnerIDs.size > 0 || selectedTransactionIDs.length > 1 : knownOwnerIDs.size > 1; const {translate, localeCompare} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -150,25 +136,23 @@ function useSelectedTransactionsActions({ iouType = CONST.IOU.TYPE.INVOICE; } - const handleDeleteTransactions = useCallback(() => { + const handleDeleteTransactions = () => { const deletedThreadReportIDs = deleteTransactions(selectedTransactionIDs, duplicateTransactions, duplicateTransactionViolations, currentSearchHash, false); clearSelectedTransactions(true); setIsDeleteModalVisible(false); Navigation.removeReportScreen(new Set(deletedThreadReportIDs)); - }, [deleteTransactions, selectedTransactionIDs, duplicateTransactions, duplicateTransactionViolations, currentSearchHash, clearSelectedTransactions]); + }; - const showDeleteModal = useCallback(() => { + const showDeleteModal = () => { setIsDeleteModalVisible(true); - }, []); + }; - const hideDeleteModal = useCallback(() => { + const hideDeleteModal = () => { setIsDeleteModalVisible(false); - }, []); + }; - const computedOptions = useMemo(() => { - if (!selectedTransactionIDs.length) { - return []; - } + let computedOptions: Array> = []; + if (selectedTransactionIDs.length) { const options = []; const isMoneyRequestReport = isMoneyRequestReportUtils(report); const isReportReimbursed = report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; @@ -365,46 +349,8 @@ function useSelectedTransactionsActions({ onSelected: showDeleteModal, }); } - return options; - }, [ - session?.email, - selectedTransactionIDs, - report, - selectedTransactionsList, - translate, - isReportArchived, - hasTransactionsFromMultipleOwners, - policy, - reportActions, - clearSelectedTransactions, - allTransactionsLength, - integrationsExportTemplates, - csvExportLayouts, - isOffline, - onExportOffline, - onExportFailed, - beginExportWithTemplate, - outstandingReportsByPolicyID, - iouType, - lastVisitedPath, - allTransactions, - allReports, - session?.accountID, - showDeleteModal, - allTransactionViolations, - expensifyIcons.Stopwatch, - expensifyIcons.ThumbsDown, - expensifyIcons.Table, - expensifyIcons.Export, - expensifyIcons.ArrowRight, - expensifyIcons.ArrowSplit, - expensifyIcons.DocumentMerge, - expensifyIcons.ArrowCollapse, - expensifyIcons.Trashcan, - localeCompare, - isOnSearch, - login, - ]); + computedOptions = options; + } return { options: computedOptions, diff --git a/src/hooks/useTransactionsAndViolationsForReport.ts b/src/hooks/useTransactionsAndViolationsForReport.ts index d92ec801bdd4..512030a1f3ed 100644 --- a/src/hooks/useTransactionsAndViolationsForReport.ts +++ b/src/hooks/useTransactionsAndViolationsForReport.ts @@ -1,4 +1,3 @@ -import {useMemo} from 'react'; import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; import {getTransactionViolations} from '@libs/TransactionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -17,25 +16,14 @@ function useTransactionsAndViolationsForReport(reportID?: string) { const {transactions, violations} = reportID ? (allReportsTransactionsAndViolations?.[reportID] ?? DEFAULT_RETURN_VALUE) : DEFAULT_RETURN_VALUE; - const transactionsAndViolations = useMemo(() => { - const filteredViolations = Object.keys(violations).reduce( - (filteredTransactionViolations, transactionViolationKey) => { - const transactionID = transactionViolationKey.split('_').at(1) ?? ''; - const transaction = transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const filteredViolations: Record = {}; + for (const transactionViolationKey of Object.keys(violations)) { + const transactionID = transactionViolationKey.split('_').at(1) ?? ''; + const transaction = transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + filteredViolations[transactionViolationKey] = getTransactionViolations(transaction, violations, currentUserDetails.email ?? '', currentUserDetails.accountID, report, policy) ?? []; + } - // This is our accumulator, it's okay to reassign - // eslint-disable-next-line no-param-reassign - filteredTransactionViolations[transactionViolationKey] = - getTransactionViolations(transaction, violations, currentUserDetails.email ?? '', currentUserDetails.accountID, report, policy) ?? []; - return filteredTransactionViolations; - }, - {} as Record, - ); - - return {transactions, violations: filteredViolations}; - }, [transactions, violations, currentUserDetails?.email, currentUserDetails?.accountID, report, policy]); - - return transactionsAndViolations; + return {transactions, violations: filteredViolations}; } export default useTransactionsAndViolationsForReport; diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts index a760ca84f50c..6201df99d63f 100644 --- a/src/libs/LoginUtils.ts +++ b/src/libs/LoginUtils.ts @@ -115,6 +115,18 @@ function formatE164PhoneNumber(phoneNumber: string, countryCode: number) { return parsedPhoneNumber.number?.e164; } +/** + * Format a login string by removing SMS domain if applicable + * @param login - The login string to format + * @returns The formatted login string, or empty string if no login provided + */ +function normalizeLogin(login: string | undefined): string { + if (!login) { + return ''; + } + return Str.isSMSLogin(login) ? Str.removeSMSDomain(login) : login; +} + function sanitizePhoneOrEmail(phoneOrEmail: string) { return phoneOrEmail.replaceAll(CONST.REGEX.WHITESPACE, '').toLowerCase(); } @@ -131,5 +143,6 @@ export { formatE164PhoneNumber, getEmailDomain, isDomainPublic, + normalizeLogin, sanitizePhoneOrEmail, }; diff --git a/src/pages/GroupChatNameEditPage.tsx b/src/pages/GroupChatNameEditPage.tsx index 1cd289c4f84a..a282eb1dc215 100644 --- a/src/pages/GroupChatNameEditPage.tsx +++ b/src/pages/GroupChatNameEditPage.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo} from 'react'; +import React from 'react'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; @@ -39,43 +39,34 @@ function GroupChatNameEditPage({report}: GroupChatNameEditPageProps) { const {translate, formatPhoneNumber} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); - const existingReportName = useMemo( - () => (report ? getGroupChatName(formatPhoneNumber, undefined, false, report) : getGroupChatName(formatPhoneNumber, groupChatDraft?.participants)), - [formatPhoneNumber, groupChatDraft?.participants, report], - ); + const existingReportName = report ? getGroupChatName(formatPhoneNumber, undefined, false, report) : getGroupChatName(formatPhoneNumber, groupChatDraft?.participants); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const currentChatName = reportID ? existingReportName : groupChatDraft?.reportName || existingReportName; - const validate = useCallback( - (values: FormOnyxValues): Errors => { - const errors: Errors = {}; - const name = values[INPUT_IDS.NEW_CHAT_NAME] ?? ''; - const nameLength = StringUtils.getUTF8ByteLength(name.trim()); - if (nameLength > CONST.REPORT_NAME_LIMIT) { - errors.newChatName = translate('common.error.characterLimitExceedCounter', nameLength, CONST.REPORT_NAME_LIMIT); - } + const validate = (values: FormOnyxValues): Errors => { + const errors: Errors = {}; + const name = values[INPUT_IDS.NEW_CHAT_NAME] ?? ''; + const nameLength = StringUtils.getUTF8ByteLength(name.trim()); + if (nameLength > CONST.REPORT_NAME_LIMIT) { + errors.newChatName = translate('common.error.characterLimitExceedCounter', nameLength, CONST.REPORT_NAME_LIMIT); + } - return errors; - }, - [translate], - ); + return errors; + }; - const editName = useCallback( - (values: FormOnyxValues) => { - if (isUpdatingExistingReport) { - if (values[INPUT_IDS.NEW_CHAT_NAME] !== currentChatName) { - updateChatName(reportID, report.reportName, values[INPUT_IDS.NEW_CHAT_NAME] ?? '', CONST.REPORT.CHAT_TYPE.GROUP); - } - Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID))); - return; - } + const editName = (values: FormOnyxValues) => { + if (isUpdatingExistingReport) { if (values[INPUT_IDS.NEW_CHAT_NAME] !== currentChatName) { - setGroupDraft({reportName: values[INPUT_IDS.NEW_CHAT_NAME]}); + updateChatName(reportID, report.reportName, values[INPUT_IDS.NEW_CHAT_NAME] ?? '', CONST.REPORT.CHAT_TYPE.GROUP); } - Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.NEW_CHAT_CONFIRM)); - }, - [isUpdatingExistingReport, currentChatName, reportID, report?.reportName], - ); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID))); + return; + } + if (values[INPUT_IDS.NEW_CHAT_NAME] !== currentChatName) { + setGroupDraft({reportName: values[INPUT_IDS.NEW_CHAT_NAME]}); + } + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.NEW_CHAT_CONFIRM)); + }; return ( getPersonalDetailSearchTerms(participant).join(' ').toLowerCase?.().includes(cleanSearchTerm)), ); - useFocusEffect( - useCallback(() => { - focusTimeoutRef.current = setTimeout(() => { - setDidScreenTransitionEnd(true); - }, CONST.ANIMATED_TRANSITION); + useFocusEffect(() => { + focusTimeoutRef.current = setTimeout(() => { + setDidScreenTransitionEnd(true); + }, CONST.ANIMATED_TRANSITION); - return () => focusTimeoutRef.current && clearTimeout(focusTimeoutRef.current); - }, []), - ); + return () => focusTimeoutRef.current && clearTimeout(focusTimeoutRef.current); + }); useEffect(() => { if (!debouncedSearchTerm.length) { @@ -253,186 +251,173 @@ function NewChatPage({ref}: NewChatPageProps) { areOptionsInitialized, } = useOptions(); - const [sections, firstKeyForList] = useMemo(() => { - const sectionsList: Section[] = []; - let firstKey = ''; - - const formatResults = formatSectionsFromSearchTerm( - debouncedSearchTerm, - selectedOptions as OptionData[], - recentReports, - personalDetails, - undefined, - undefined, - undefined, - reportAttributesDerived, - ); - sectionsList.push(formatResults.section); + const sections: Section[] = []; + let firstKeyForList = ''; - if (!firstKey) { - firstKey = getFirstKeyForList(formatResults.section.data); - } + const formatResults = formatSectionsFromSearchTerm( + debouncedSearchTerm, + selectedOptions as OptionData[], + recentReports, + personalDetails, + undefined, + undefined, + undefined, + reportAttributesDerived, + ); + sections.push(formatResults.section); - sectionsList.push({ - title: translate('common.recents'), - data: selectedOptions.length ? recentReports.filter((option) => !option.isSelfDM) : recentReports, - shouldShow: !isEmpty(recentReports), - }); - if (!firstKey) { - firstKey = getFirstKeyForList(recentReports); - } + if (!firstKeyForList) { + firstKeyForList = getFirstKeyForList(formatResults.section.data); + } - sectionsList.push({ - title: translate('common.contacts'), - data: personalDetails, - shouldShow: !isEmpty(personalDetails), + sections.push({ + title: translate('common.recents'), + data: selectedOptions.length ? recentReports.filter((option) => !option.isSelfDM) : recentReports, + shouldShow: !isEmpty(recentReports), + }); + if (!firstKeyForList) { + firstKeyForList = getFirstKeyForList(recentReports); + } + + sections.push({ + title: translate('common.contacts'), + data: personalDetails, + shouldShow: !isEmpty(personalDetails), + }); + if (!firstKeyForList) { + firstKeyForList = getFirstKeyForList(personalDetails); + } + + if (userToInvite) { + sections.push({ + title: undefined, + data: [userToInvite], + shouldShow: true, }); - if (!firstKey) { - firstKey = getFirstKeyForList(personalDetails); - } - - if (userToInvite) { - sectionsList.push({ - title: undefined, - data: [userToInvite], - shouldShow: true, - }); - if (!firstKey) { - firstKey = getFirstKeyForList([userToInvite]); - } + if (!firstKeyForList) { + firstKeyForList = getFirstKeyForList([userToInvite]); } - - return [sectionsList, firstKey]; - }, [debouncedSearchTerm, selectedOptions, recentReports, personalDetails, reportAttributesDerived, translate, userToInvite]); + } /** * Removes a selected option from list if already selected. If not already selected add this option to the list. */ - const toggleOption = useCallback( - (option: ListItem & Partial) => { - const isOptionInList = !!option.isSelected; + const toggleOption = (option: ListItem & Partial) => { + const isOptionInList = !!option.isSelected; - let newSelectedOptions: SelectedOption[]; + let newSelectedOptions: SelectedOption[]; - if (isOptionInList) { - newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); - } else { - newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID}]; - selectionListRef?.current?.scrollToIndex(0, true); - } + if (isOptionInList) { + newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); + } else { + newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID}]; + selectionListRef?.current?.scrollToIndex(0, true); + } - selectionListRef?.current?.clearInputAfterSelect?.(); - if (!canUseTouchScreen()) { - selectionListRef.current?.focusTextInput(); - } - setSelectedOptions(newSelectedOptions); - - if (personalData?.login && personalData?.accountID) { - const participants: SelectedParticipant[] = [ - ...newSelectedOptions.map((selectedOption) => ({ - login: selectedOption.login, - accountID: selectedOption.accountID ?? CONST.DEFAULT_NUMBER_ID, - })), - { - login: personalData.login, - accountID: personalData.accountID, - }, - ]; - setGroupDraft({participants}); - } - }, - [selectedOptions, setSelectedOptions, personalData?.accountID, personalData?.login], - ); + selectionListRef?.current?.clearInputAfterSelect?.(); + if (!canUseTouchScreen()) { + selectionListRef.current?.focusTextInput(); + } + setSelectedOptions(newSelectedOptions); + + if (personalData?.login && personalData?.accountID) { + const participants: SelectedParticipant[] = [ + ...newSelectedOptions.map((selectedOption) => ({ + login: selectedOption.login, + accountID: selectedOption.accountID ?? CONST.DEFAULT_NUMBER_ID, + })), + { + login: personalData.login, + accountID: personalData.accountID, + }, + ]; + setGroupDraft({participants}); + } + }; /** * If there are selected options already then it will toggle the option otherwise * creates a new 1:1 chat with the option and the current user, * or navigates to the existing chat if one with those participants already exists. */ - const selectOption = useCallback( - (option?: Option) => { - if (option?.isSelfDM) { - if (!option.reportID) { - Navigation.dismissModal(); - return; - } - Navigation.dismissModalWithReport({reportID: option.reportID}); + const selectOption = (option?: Option) => { + if (option?.isSelfDM) { + if (!option.reportID) { + Navigation.dismissModal(); return; } + Navigation.dismissModalWithReport({reportID: option.reportID}); + return; + } - if (selectedOptions.length && option) { - // Prevent excluded emails from being added to groups - if (option?.login && excludedGroupEmails.has(option.login)) { - return; - } - toggleOption(option); + if (selectedOptions.length && option) { + // Prevent excluded emails from being added to groups + if (option?.login && excludedGroupEmails.has(option.login)) { return; } + toggleOption(option); + return; + } - if (option?.reportID) { - Navigation.dismissModal({ - callback: () => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option?.reportID)); - }, - }); - return; - } + if (option?.reportID) { + Navigation.dismissModal({ + callback: () => { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option?.reportID)); + }, + }); + return; + } - let login = ''; + let login = ''; - if (option?.login) { - login = option.login; - } else if (selectedOptions.length === 1) { - login = selectedOptions.at(0)?.login ?? ''; - } - if (!login) { - Log.warn('Tried to create chat with empty login'); - return; - } - KeyboardUtils.dismiss().then(() => { - singleExecution(() => navigateToAndOpenReport([login]))(); - }); - }, - [selectedOptions, toggleOption, singleExecution], - ); + if (option?.login) { + login = option.login; + } else if (selectedOptions.length === 1) { + login = selectedOptions.at(0)?.login ?? ''; + } + if (!login) { + Log.warn('Tried to create chat with empty login'); + return; + } + KeyboardUtils.dismiss().then(() => { + singleExecution(() => navigateToAndOpenReport([login]))(); + }); + }; - const itemRightSideComponent = useCallback( - (item: ListItem & Option, isFocused?: boolean) => { - if (!!item.isSelfDM || (item.login && excludedGroupEmails.has(item.login)) || !item.login) { - return null; - } + const itemRightSideComponent = (item: ListItem & Option, isFocused?: boolean) => { + if (!!item.isSelfDM || (item.login && excludedGroupEmails.has(item.login)) || !item.login) { + return null; + } - if (item.isSelected) { - return ( - toggleOption(item)} - disabled={item.isDisabled} - role={CONST.ROLE.BUTTON} - accessibilityLabel={CONST.ROLE.BUTTON} - style={[styles.flexRow, styles.alignItemsCenter, styles.ml5, styles.optionSelectCircle]} - > - - - ); - } - const buttonInnerStyles = isFocused ? styles.buttonDefaultHovered : {}; + if (item.isSelected) { return ( -