diff --git a/src/CONST.ts b/src/CONST.ts
index b9141c9c0d4b..a4f28a9c98c7 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -668,6 +668,7 @@ const CONST = {
TOOLTIP_SENSE: 1000,
TRIE_INITIALIZATION: 'trie_initialization',
COMMENT_LENGTH_DEBOUNCE_TIME: 500,
+ SEARCH_FOR_REPORTS_DEBOUNCE_TIME: 300,
},
PRIORITY_MODE: {
GSD: 'gsd',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index f7c4a11bc52f..3e4348dae3cc 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -26,6 +26,9 @@ const ONYXKEYS = {
/** Boolean flag set whenever the sidebar has loaded */
IS_SIDEBAR_LOADED: 'isSidebarLoaded',
+ /** Boolean flag set whenever we are searching for reports in the server */
+ IS_SEARCHING_FOR_REPORTS: 'isSearchingForReports',
+
/** Note: These are Persisted Requests - not all requests in the main queue as the key name might lead one to believe */
PERSISTED_REQUESTS: 'networkRequestQueue',
diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js
index 23049b65f198..edea0b8d1aba 100644
--- a/src/components/OptionsList/BaseOptionsList.js
+++ b/src/components/OptionsList/BaseOptionsList.js
@@ -66,6 +66,7 @@ function BaseOptionsList({
isDisabled,
innerRef,
isRowMultilineSupported,
+ isLoadingNewOptions,
}) {
const flattenedData = useRef();
const previousSections = usePrevious(sections);
@@ -245,7 +246,9 @@ function BaseOptionsList({
) : (
<>
- {headerMessage ? (
+ {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */}
+ {/* This is misleading because we might be in the process of loading fresh options from the server. */}
+ {!isLoadingNewOptions && headerMessage ? (
{headerMessage}
diff --git a/src/components/OptionsList/optionsListPropTypes.js b/src/components/OptionsList/optionsListPropTypes.js
index 165cec699b80..dc716453b2a8 100644
--- a/src/components/OptionsList/optionsListPropTypes.js
+++ b/src/components/OptionsList/optionsListPropTypes.js
@@ -87,6 +87,9 @@ const propTypes = {
/** Whether to wrap large text up to 2 lines */
isRowMultilineSupported: PropTypes.bool,
+
+ /** Whether we are loading new options */
+ isLoadingNewOptions: PropTypes.bool,
};
const defaultProps = {
@@ -113,6 +116,7 @@ const defaultProps = {
shouldPreventDefaultFocusOnSelectRow: false,
showScrollIndicator: false,
isRowMultilineSupported: false,
+ isLoadingNewOptions: false,
};
export {propTypes, defaultProps};
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index e72bb7ef4b8e..3c9d401cdbdb 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -17,6 +17,7 @@ import {propTypes as optionsSelectorPropTypes, defaultProps as optionsSelectorDe
import setSelection from '../../libs/setSelection';
import compose from '../../libs/compose';
import getPlatform from '../../libs/getPlatform';
+import FormHelpMessage from '../FormHelpMessage';
const propTypes = {
/** padding bottom style of safe area */
@@ -392,6 +393,7 @@ class BaseOptionsSelector extends Component {
blurOnSubmit={Boolean(this.state.allOptions.length)}
spellCheck={false}
shouldInterceptSwipe={this.props.shouldTextInputInterceptSwipe}
+ isLoading={this.props.isLoadingNewOptions}
/>
);
const optionsList = (
@@ -428,6 +430,7 @@ class BaseOptionsSelector extends Component {
isLoading={!this.props.shouldShowOptions}
showScrollIndicator={this.props.showScrollIndicator}
isRowMultilineSupported={this.props.isRowMultilineSupported}
+ isLoadingNewOptions={this.props.isLoadingNewOptions}
shouldPreventDefaultFocusOnSelectRow={this.props.shouldPreventDefaultFocusOnSelectRow}
/>
);
@@ -453,6 +456,13 @@ class BaseOptionsSelector extends Component {
{this.props.children}
{this.props.shouldShowTextInput && textInput}
+ {Boolean(this.props.textInputAlert) && (
+
+ )}
{optionsList}
>
diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js
index cfc042b4f370..76b7728458c4 100644
--- a/src/components/TextInput/BaseTextInput.js
+++ b/src/components/TextInput/BaseTextInput.js
@@ -1,6 +1,6 @@
import _ from 'underscore';
import React, {useState, useRef, useEffect, useCallback, useMemo} from 'react';
-import {Animated, View, StyleSheet} from 'react-native';
+import {Animated, View, StyleSheet, ActivityIndicator} from 'react-native';
import Str from 'expensify-common/lib/str';
import RNTextInput from '../RNTextInput';
import TextInputLabel from './TextInputLabel';
@@ -372,6 +372,13 @@ function BaseTextInput(props) {
// `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback.
dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}}
/>
+ {props.isLoading && (
+
+ )}
{Boolean(props.secureTextEntry) && (
(preferredLocale = val),
});
+let priorityMode;
+Onyx.connect({
+ key: ONYXKEYS.NVP_PRIORITY_MODE,
+ callback: (nextPriorityMode) => {
+ // When someone switches their priority mode we need to fetch all their chats because only #focus mode works with a subset of a user's chats. This is only possible via the OpenApp command.
+ if (nextPriorityMode === CONST.PRIORITY_MODE.DEFAULT && priorityMode === CONST.PRIORITY_MODE.GSD) {
+ // eslint-disable-next-line no-use-before-define
+ openApp();
+ }
+ priorityMode = nextPriorityMode;
+ },
+});
+
let resolveIsReadyPromise;
const isReadyToOpenApp = new Promise((resolve) => {
resolveIsReadyPromise = resolve;
@@ -207,7 +220,8 @@ function getOnyxDataForOpenOrReconnect(isOpenApp = false) {
*/
function openApp() {
getPolicyParamsForOpenOrReconnect().then((policyParams) => {
- API.read('OpenApp', policyParams, getOnyxDataForOpenOrReconnect(true));
+ const params = {enablePriorityModeFilter: true, ...policyParams};
+ API.read('OpenApp', params, getOnyxDataForOpenOrReconnect(true));
});
}
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index bbc5ddeadd82..00c3ab325f29 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -1,6 +1,7 @@
import {InteractionManager} from 'react-native';
import _ from 'underscore';
import lodashGet from 'lodash/get';
+import lodashDebounce from 'lodash/debounce';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import Onyx from 'react-native-onyx';
import Str from 'expensify-common/lib/str';
@@ -2221,7 +2222,63 @@ function savePrivateNotesDraft(reportID, note) {
Onyx.merge(`${ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT}${reportID}`, note);
}
+/**
+ * @private
+ * @param {string} searchInput
+ */
+function searchForReports(searchInput) {
+ // We do not try to make this request while offline because it sets a loading indicator optimistically
+ if (isNetworkOffline) {
+ Onyx.set(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, false);
+ return;
+ }
+
+ API.read(
+ 'SearchForReports',
+ {searchInput},
+ {
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
+ value: false,
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
+ value: false,
+ },
+ ],
+ },
+ );
+}
+
+/**
+ * @private
+ * @param {string} searchInput
+ */
+const debouncedSearchInServer = lodashDebounce(searchForReports, CONST.TIMING.SEARCH_FOR_REPORTS_DEBOUNCE_TIME, {leading: false});
+
+/**
+ * @param {string} searchInput
+ */
+function searchInServer(searchInput) {
+ if (isNetworkOffline) {
+ Onyx.set(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, false);
+ return;
+ }
+
+ // Why not set this in optimistic data? It won't run until the API request happens and while the API request is debounced
+ // we want to show the loading state right away. Otherwise, we will see a flashing UI where the client options are sorted and
+ // tell the user there are no options, then we start searching, and tell them there are no options again.
+ Onyx.set(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, true);
+ debouncedSearchInServer(searchInput);
+}
+
export {
+ searchInServer,
addComment,
addAttachment,
reconnect,
diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js
index 565f36d69e54..64bff8655403 100755
--- a/src/pages/NewChatPage.js
+++ b/src/pages/NewChatPage.js
@@ -1,5 +1,5 @@
import _ from 'underscore';
-import React, {useState, useEffect, useMemo} from 'react';
+import React, {useState, useEffect, useMemo, useCallback} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
@@ -20,6 +20,7 @@ import compose from '../libs/compose';
import personalDetailsPropType from './personalDetailsPropType';
import reportPropTypes from './reportPropTypes';
import variables from '../styles/variables';
+import useNetwork from '../hooks/useNetwork';
const propTypes = {
/** Beta features list */
@@ -34,22 +35,27 @@ const propTypes = {
...windowDimensionsPropTypes,
...withLocalizePropTypes,
+
+ /** Whether we are searching for reports in the server */
+ isSearchingForReports: PropTypes.bool,
};
const defaultProps = {
betas: [],
personalDetails: {},
reports: {},
+ isSearchingForReports: false,
};
const excludedGroupEmails = _.without(CONST.EXPENSIFY_EMAILS, CONST.EMAIL.CONCIERGE);
-function NewChatPage({betas, isGroupChat, personalDetails, reports, translate}) {
+function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, isSearchingForReports}) {
const [searchTerm, setSearchTerm] = useState('');
const [filteredRecentReports, setFilteredRecentReports] = useState([]);
const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]);
const [filteredUserToInvite, setFilteredUserToInvite] = useState();
const [selectedOptions, setSelectedOptions] = useState([]);
+ const {isOffline} = useNetwork();
const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
const headerMessage = OptionsListUtils.getHeaderMessage(
@@ -167,6 +173,13 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reports, personalDetails, searchTerm]);
+ // When search term updates we will fetch any reports
+ const setSearchTermAndSearchInServer = useCallback((text = '') => {
+ if (text.length) {
+ Report.searchInServer(text);
+ }
+ setSearchTerm(text);
+ }, []);
return (
createChat(option)}
- onChangeText={setSearchTerm}
+ onChangeText={setSearchTermAndSearchInServer}
headerMessage={headerMessage}
boldStyle
shouldPreventDefaultFocusOnSelectRow={!Browser.isMobile()}
shouldShowOptions={isOptionsDataReady}
shouldShowConfirmButton
confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')}
+ textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''}
onConfirmSelection={createGroup}
textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
+ isLoadingNewOptions={isSearchingForReports}
/>
@@ -230,5 +245,9 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
+ isSearchingForReports: {
+ key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
+ initWithStoredValues: false,
+ },
}),
)(NewChatPage);
diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js
index 141f4e841853..272fb30de858 100755
--- a/src/pages/SearchPage.js
+++ b/src/pages/SearchPage.js
@@ -20,6 +20,8 @@ import compose from '../libs/compose';
import personalDetailsPropType from './personalDetailsPropType';
import reportPropTypes from './reportPropTypes';
import Performance from '../libs/Performance';
+import networkPropTypes from '../components/networkPropTypes';
+import {withNetwork} from '../components/OnyxProvider';
const propTypes = {
/* Onyx Props */
@@ -37,12 +39,20 @@ const propTypes = {
...windowDimensionsPropTypes,
...withLocalizePropTypes,
+
+ /** Network info */
+ network: networkPropTypes,
+
+ /** Whether we are searching for reports in the server */
+ isSearchingForReports: PropTypes.bool,
};
const defaultProps = {
betas: [],
personalDetails: {},
reports: {},
+ network: {},
+ isSearchingForReports: false,
};
class SearchPage extends Component {
@@ -75,6 +85,10 @@ class SearchPage extends Component {
}
onChangeText(searchValue = '') {
+ if (searchValue.length) {
+ Report.searchInServer(searchValue);
+ }
+
this.setState({searchValue}, this.debouncedUpdateOptions);
}
@@ -187,9 +201,13 @@ class SearchPage extends Component {
showTitleTooltip
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')}
+ textInputAlert={
+ this.props.network.isOffline ? `${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('search.resultsAreLimited')}` : ''
+ }
onLayout={this.searchRendered}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
autoFocus
+ isLoadingNewOptions={this.props.isSearchingForReports}
/>
>
@@ -205,6 +223,7 @@ SearchPage.defaultProps = defaultProps;
export default compose(
withLocalize,
withWindowDimensions,
+ withNetwork(),
withOnyx({
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
@@ -215,5 +234,9 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
+ isSearchingForReports: {
+ key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
+ initWithStoredValues: false,
+ },
}),
)(SearchPage);