diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx index a7516163c40b..09bfbbebc552 100644 --- a/src/pages/ChatFinderPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -184,7 +184,7 @@ function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPa onSelectRow={selectReport} showLoadingPlaceholder={!areOptionsInitialized || !isScreenTransitionEnd} footerContent={!isDismissed && ChatFinderPageFooterInstance} - isLoadingNewOptions={isSearchingForReports ?? undefined} + isLoadingNewOptions={!!isSearchingForReports} /> ); diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 703a87c9510a..139406249a5e 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -3,6 +3,7 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -13,8 +14,10 @@ import type {Section} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ReportActions from '@libs/actions/Report'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -26,6 +29,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import type {RoomInviteNavigatorParamList} from '@navigation/types'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Policy} from '@src/types/onyx'; @@ -37,7 +41,6 @@ import SearchInputManager from './workspace/SearchInputManager'; type RoomInvitePageProps = WithReportOrNotFoundProps & WithNavigationTransitionEndProps & StackScreenProps; type Sections = Array>>; - function RoomInvitePage({ betas, report, @@ -48,14 +51,16 @@ function RoomInvitePage({ }: RoomInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [invitePersonalDetails, setInvitePersonalDetails] = useState([]); const [userToInvite, setUserToInvite] = useState(null); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const {options, areOptionsInitialized} = useOptionsList(); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Any existing participants and Expensify emails should not be eligible for invitation @@ -68,7 +73,7 @@ function RoomInvitePage({ ); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], debouncedSearchTerm, excludedUsers); // Update selectedOptions with the latest personalDetails information const detailsMap: Record = {}; @@ -87,7 +92,7 @@ function RoomInvitePage({ setInvitePersonalDetails(inviteOptions.personalDetails); setSelectedOptions(newSelectedOptions); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [betas, searchTerm, excludedUsers, options.personalDetails]); + }, [betas, debouncedSearchTerm, excludedUsers, options.personalDetails]); const sections = useMemo(() => { const sectionsArr: Sections = []; @@ -98,12 +103,12 @@ function RoomInvitePage({ // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; - if (searchTerm !== '') { + if (debouncedSearchTerm !== '') { filterSelectedOptions = selectedOptions.filter((option) => { const accountID = option?.accountID; const isOptionInPersonalDetails = invitePersonalDetails.some((personalDetail) => accountID && personalDetail?.accountID === accountID); - const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchTerm))); - const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number ? parsedPhoneNumber.number.e164 : searchTerm.toLowerCase(); + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(debouncedSearchTerm))); + const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number ? parsedPhoneNumber.number.e164 : debouncedSearchTerm.toLowerCase(); const isPartOfSearchTerm = (option.text?.toLowerCase() ?? '').includes(searchValue) || (option.login?.toLowerCase() ?? '').includes(searchValue); return isPartOfSearchTerm || isOptionInPersonalDetails; }); @@ -134,7 +139,7 @@ function RoomInvitePage({ } return sectionsArr; - }, [areOptionsInitialized, selectedOptions, searchTerm, invitePersonalDetails, userToInvite, translate]); + }, [areOptionsInitialized, selectedOptions, debouncedSearchTerm, invitePersonalDetails, userToInvite, translate]); const toggleOption = useCallback( (option: OptionsListUtils.MemberForList) => { @@ -193,7 +198,7 @@ function RoomInvitePage({ }, [role, reportID, backRoute]); const headerMessage = useMemo(() => { - const searchValue = searchTerm.trim().toLowerCase(); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); const expensifyEmails = CONST.EXPENSIFY_EMAILS as string[]; if (!userToInvite && expensifyEmails.includes(searchValue)) { return translate('messages.errorMessageInvalidEmail'); @@ -209,7 +214,11 @@ function RoomInvitePage({ return translate('messages.userIsAlreadyMember', {login: searchValue, name: reportName}); } return OptionsListUtils.getHeaderMessage(invitePersonalDetails.length !== 0, Boolean(userToInvite), searchValue); - }, [searchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); + }, [debouncedSearchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); + + useEffect(() => { + ReportActions.searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); return ( ); } diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 578efbe5317b..58c3be7df6b1 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -1,4 +1,5 @@ -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; +import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useBetas} from '@components/OnyxProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; @@ -8,10 +9,12 @@ import UserListItem from '@components/SelectionList/UserListItem'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import * as ReportActions from '@libs/actions/Report'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import type {BaseShareLogListProps} from './types'; @@ -21,6 +24,7 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { const {isOffline} = useNetwork(); const {translate} = useLocalize(); const betas = useBetas(); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const {options, areOptionsInitialized} = useOptionsList(); const searchOptions = useMemo(() => { @@ -82,6 +86,10 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { onAttachLogToReport(option.reportID, filename); }; + useEffect(() => { + ReportActions.searchInServer(debouncedSearchValue); + }, [debouncedSearchValue]); + return ( )} diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 4ebabbce350e..9a3993a6b649 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -1,9 +1,9 @@ /* eslint-disable es/no-optional-chaining */ import type {RouteProp} from '@react-navigation/native'; import {useRoute} from '@react-navigation/native'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -21,6 +21,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ReportActions from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -94,12 +95,9 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro const route = useRoute>(); const {translate} = useLocalize(); const session = useSession(); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, searchValue, setSearchValue, headerMessage, areOptionsInitialized} = useOptions(); - - const onChangeText = (newSearchTerm = '') => { - setSearchValue(newSearchTerm); - }; + const {userToInvite, recentReports, personalDetails, currentUserOption, searchValue, debouncedSearchValue, setSearchValue, headerMessage, areOptionsInitialized} = useOptions(); const report: OnyxEntry = useMemo(() => { if (!route.params?.reportID) { @@ -200,6 +198,10 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro const canModifyTask = TaskActions.canModifyTask(report, currentUserPersonalDetails.accountID); const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); + useEffect(() => { + ReportActions.searchInServer(debouncedSearchValue); + }, [debouncedSearchValue]); + return ( diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index b4b8f9084a57..859dfa36a33f 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,7 +1,6 @@ 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'; +import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -21,12 +20,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; -type TaskShareDestinationSelectorModalOnyxProps = { - isSearchingForReports: OnyxEntry; -}; - -type TaskShareDestinationSelectorModalProps = TaskShareDestinationSelectorModalOnyxProps; - const selectReportHandler = (option: unknown) => { const optionItem = option as ReportUtils.OptionData; @@ -47,12 +40,13 @@ const reportFilter = (reportOptions: Array return filtered; }, []); -function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDestinationSelectorModalProps) { +function TaskShareDestinationSelectorModal() { const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const {options: optionList, areOptionsInitialized} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); @@ -116,7 +110,7 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes headerMessage={options.headerMessage} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} showLoadingPlaceholder={areOptionsInitialized && debouncedSearchValue.trim() === '' ? options.sections.length === 0 : !didScreenTransitionEnd} - isLoadingNewOptions={isSearchingForReports ?? undefined} + isLoadingNewOptions={!!isSearchingForReports} textInputHint={textInputHint} /> @@ -127,9 +121,4 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal'; -export default withOnyx({ - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, -})(TaskShareDestinationSelectorModal); +export default TaskShareDestinationSelectorModal; diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index abb7b0a15944..2ba3cd88f137 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,7 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {SectionListData} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -12,9 +12,11 @@ import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem import type {Section} from '@components/SelectionList/types'; import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ReportActions from '@libs/actions/Report'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -55,11 +57,12 @@ type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, policy}: WorkspaceInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const firstRenderRef = useRef(true); const openWorkspaceInvitePage = () => { @@ -75,6 +78,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli return () => { Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {}); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [route.params.policyID]); useEffect(() => { @@ -92,7 +96,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli const newPersonalDetailsDict: Record = {}; const newSelectedOptionsDict: Record = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers, true); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], debouncedSearchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyEmployeeList information const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { @@ -147,7 +151,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli setSelectedOptions(Object.values(newSelectedOptionsDict)); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [options.personalDetails, policy?.employeeList, betas, searchTerm, excludedUsers]); + }, [options.personalDetails, policy?.employeeList, betas, debouncedSearchTerm, excludedUsers]); const sections: MembersSection[] = useMemo(() => { const sectionsArr: MembersSection[] = []; @@ -158,12 +162,12 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; - if (searchTerm !== '') { + if (debouncedSearchTerm !== '') { filterSelectedOptions = selectedOptions.filter((option) => { const accountID = option.accountID; const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); - const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(debouncedSearchTerm); const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); return isPartOfSearchTerm || isOptionInPersonalDetails; @@ -200,7 +204,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli }); return sectionsArr; - }, [areOptionsInitialized, selectedOptions, searchTerm, personalDetails, translate, usersToInvite]); + }, [areOptionsInitialized, selectedOptions, debouncedSearchTerm, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { Policy.clearErrors(route.params.policyID); @@ -246,7 +250,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli const [policyName, shouldShowAlertPrompt] = useMemo(() => [policy?.name ?? '', !isEmptyObject(policy?.errors) || !!policy?.alertMessage], [policy]); const headerMessage = useMemo(() => { - const searchValue = searchTerm.trim().toLowerCase(); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.some((email) => email === searchValue)) { return translate('messages.errorMessageInvalidEmail'); } @@ -261,7 +265,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli return translate('messages.userIsAlreadyMember', {login: searchValue, name: policyName}); } return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, usersToInvite.length > 0, searchValue); - }, [excludedUsers, translate, searchTerm, policyName, usersToInvite, personalDetails.length]); + }, [excludedUsers, translate, debouncedSearchTerm, policyName, usersToInvite, personalDetails.length]); const footerContent = useMemo( () => ( @@ -279,6 +283,10 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli [inviteUser, policy?.alertMessage, selectedOptions.length, shouldShowAlertPrompt, styles, translate], ); + useEffect(() => { + ReportActions.searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); + return (