Skip to content

Commit

Permalink
Merge pull request #27819 from Expensify/marcaaron-minimalSearch
Browse files Browse the repository at this point in the history
Minimal server search / enable improved focus mode
  • Loading branch information
Beamanator authored Oct 13, 2023
2 parents be54929 + 6df2efa commit 1201077
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down
5 changes: 4 additions & 1 deletion src/components/OptionsList/BaseOptionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ function BaseOptionsList({
isDisabled,
innerRef,
isRowMultilineSupported,
isLoadingNewOptions,
}) {
const flattenedData = useRef();
const previousSections = usePrevious(sections);
Expand Down Expand Up @@ -245,7 +246,9 @@ function BaseOptionsList({
<OptionsListSkeletonView shouldAnimate />
) : (
<>
{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 ? (
<View style={[styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted]}>{headerMessage}</Text>
</View>
Expand Down
4 changes: 4 additions & 0 deletions src/components/OptionsList/optionsListPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -113,6 +116,7 @@ const defaultProps = {
shouldPreventDefaultFocusOnSelectRow: false,
showScrollIndicator: false,
isRowMultilineSupported: false,
isLoadingNewOptions: false,
};

export {propTypes, defaultProps};
10 changes: 10 additions & 0 deletions src/components/OptionsSelector/BaseOptionsSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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}
/>
);
Expand All @@ -453,6 +456,13 @@ class BaseOptionsSelector extends Component {
<View style={this.props.shouldUseStyleForChildren ? [styles.ph5, styles.pb3] : []}>
{this.props.children}
{this.props.shouldShowTextInput && textInput}
{Boolean(this.props.textInputAlert) && (
<FormHelpMessage
message={this.props.textInputAlert}
style={[styles.mb3]}
isError={false}
/>
)}
</View>
{optionsList}
</>
Expand Down
9 changes: 8 additions & 1 deletion src/components/TextInput/BaseTextInput.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 && (
<ActivityIndicator
size="small"
color={themeColors.iconSuccessFill}
style={[styles.mt4, styles.ml1]}
/>
)}
{Boolean(props.secureTextEntry) && (
<Checkbox
style={[styles.flex1, styles.textInputIconContainer]}
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,9 @@ export default {
screenShare: 'Screen share',
screenShareRequest: 'Expensify is inviting you to a screen share',
},
search: {
resultsAreLimited: 'Search results are limited.',
},
genericErrorPage: {
title: 'Uh-oh, something went wrong!',
body: {
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1638,6 +1638,9 @@ export default {
screenShare: 'Compartir pantalla',
screenShareRequest: 'Expensify te está invitando a compartir la pantalla',
},
search: {
resultsAreLimited: 'Los resultados de búsqueda están limitados.',
},
genericErrorPage: {
title: '¡Uh-oh, algo salió mal!',
body: {
Expand Down
16 changes: 15 additions & 1 deletion src/libs/actions/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ Onyx.connect({
callback: (val) => (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;
Expand Down Expand Up @@ -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));
});
}

Expand Down
57 changes: 57 additions & 0 deletions src/libs/actions/Report.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 22 additions & 3 deletions src/pages/NewChatPage.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 */
Expand All @@ -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(
Expand Down Expand Up @@ -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 (
<ScreenWrapper
shouldEnableKeyboardAvoidingView={false}
Expand Down Expand Up @@ -195,16 +208,18 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate})
selectedOptions={selectedOptions}
value={searchTerm}
onSelectRow={(option) => 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}
/>
</View>
</KeyboardAvoidingView>
Expand All @@ -230,5 +245,9 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
isSearchingForReports: {
key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
initWithStoredValues: false,
},
}),
)(NewChatPage);
23 changes: 23 additions & 0 deletions src/pages/SearchPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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 {
Expand Down Expand Up @@ -75,6 +85,10 @@ class SearchPage extends Component {
}

onChangeText(searchValue = '') {
if (searchValue.length) {
Report.searchInServer(searchValue);
}

this.setState({searchValue}, this.debouncedUpdateOptions);
}

Expand Down Expand Up @@ -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}
/>
</View>
</>
Expand All @@ -205,6 +223,7 @@ SearchPage.defaultProps = defaultProps;
export default compose(
withLocalize,
withWindowDimensions,
withNetwork(),
withOnyx({
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
Expand All @@ -215,5 +234,9 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
isSearchingForReports: {
key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
initWithStoredValues: false,
},
}),
)(SearchPage);

0 comments on commit 1201077

Please sign in to comment.