diff --git a/android/app/build.gradle b/android/app/build.gradle
index 1e54ef3d39be..2030a56bc45c 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -106,8 +106,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001034400
- versionName "1.3.44-0"
+ versionCode 1001034401
+ versionName "1.3.44-1"
}
splits {
diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js
index 0303d9d57365..af302a0e663e 100644
--- a/config/webpack/webpack.dev.js
+++ b/config/webpack/webpack.dev.js
@@ -5,7 +5,7 @@ const {merge} = require('webpack-merge');
const {TimeAnalyticsPlugin} = require('time-analytics-webpack-plugin');
const getCommonConfig = require('./webpack.common');
-const BASE_PORT = 8080;
+const BASE_PORT = 8082;
/**
* Configuration for the local dev server
diff --git a/desktop/start.js b/desktop/start.js
index 570f8fe13f07..d9ec59b71c83 100644
--- a/desktop/start.js
+++ b/desktop/start.js
@@ -3,7 +3,7 @@ const portfinder = require('portfinder');
const concurrently = require('concurrently');
require('dotenv').config();
-const basePort = 8080;
+const basePort = 8082;
portfinder
.getPortPromise({
diff --git a/docs/_includes/CONST.html b/docs/_includes/CONST.html
index 5ec5d296a336..4b87f87931d5 100644
--- a/docs/_includes/CONST.html
+++ b/docs/_includes/CONST.html
@@ -1,7 +1,7 @@
{% if jekyll.environment == "production" %}
{% assign MAIN_SITE_URL = "https://new.expensify.com" %}
{% else %}
- {% assign MAIN_SITE_URL = "http://localhost:8080" %}
+ {% assign MAIN_SITE_URL = "http://localhost:8082" %}
{% endif %}
{% capture CONCIERGE_CHAT_URL %}{{MAIN_SITE_URL}}/concierge{% endcapture %}
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index d982d2fd3134..43e02c8e150e 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -32,7 +32,7 @@
CFBundleVersion
- 1.3.44.0
+ 1.3.44.1
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index ebbf274a9fd4..4123a6cfa96e 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.44.0
+ 1.3.44.1
diff --git a/package-lock.json b/package-lock.json
index 39cc4882517f..ee5a03f93412 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.44-0",
+ "version": "1.3.44-1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.44-0",
+ "version": "1.3.44-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 2528838930e6..2337d609f7d6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.44-0",
+ "version": "1.3.44-1",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -9,11 +9,11 @@
"scripts": {
"postinstall": "scripts/postInstall.sh",
"clean": "npx react-native clean-project-auto",
- "android": "scripts/set-pusher-suffix.sh && npx react-native run-android --port=8083",
- "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --port=8082",
+ "android": "scripts/set-pusher-suffix.sh && npx react-native run-android",
+ "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios",
"pod-install": "cd ios && bundle exec pod install",
- "ipad": "concurrently \"npx react-native run-ios --port=8082 --simulator=\"iPad Pro (12.9-inch) (4th generation)\"\"",
- "ipad-sm": "concurrently \"npx react-native run-ios --port=8082 --simulator=\"iPad Pro (9.7-inch)\"\"",
+ "ipad": "concurrently \"npx react-native run-ios --simulator=\"iPad Pro (12.9-inch) (4th generation)\"\"",
+ "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\"iPad Pro (9.7-inch)\"\"",
"start": "npx react-native start",
"web": "scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server",
"web-proxy": "node web/proxy.js",
diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js
index 23aa7a2902de..3d189fd197c7 100755
--- a/src/components/Composer/index.js
+++ b/src/components/Composer/index.js
@@ -10,6 +10,7 @@ import themeColors from '../../styles/themes/default';
import updateIsFullComposerAvailable from '../../libs/ComposerUtils/updateIsFullComposerAvailable';
import * as ComposerUtils from '../../libs/ComposerUtils';
import * as Browser from '../../libs/Browser';
+import * as StyleUtils from '../../styles/StyleUtils';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
import compose from '../../libs/compose';
import styles from '../../styles/styles';
@@ -482,6 +483,7 @@ class Composer extends React.Component {
// We are hiding the scrollbar to prevent it from reducing the text input width,
// so we can get the correct scroll height while calculating the number of lines.
this.state.numberOfLines < this.props.maxLines ? styles.overflowHidden : {},
+ StyleUtils.getComposeTextAreaPadding(this.props.numberOfLines),
]}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...propsWithoutStyles}
diff --git a/src/components/KeyboardSpacer/BaseKeyboardSpacer.js b/src/components/KeyboardSpacer/BaseKeyboardSpacer.js
index 341d0ddf6f4c..2066f3492373 100644
--- a/src/components/KeyboardSpacer/BaseKeyboardSpacer.js
+++ b/src/components/KeyboardSpacer/BaseKeyboardSpacer.js
@@ -1,66 +1,53 @@
-import React, {PureComponent} from 'react';
+import React, {useState, useEffect, useCallback} from 'react';
import {Dimensions, Keyboard, View} from 'react-native';
import * as StyleUtils from '../../styles/StyleUtils';
import {propTypes, defaultProps} from './BaseKeyboardSpacerPropTypes';
-class BaseKeyboardSpacer extends PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- keyboardSpace: 0,
- };
- this.keyboardListeners = null;
- this.updateKeyboardSpace = this.updateKeyboardSpace.bind(this);
- this.resetKeyboardSpace = this.resetKeyboardSpace.bind(this);
- }
-
- componentDidMount() {
- const updateListener = this.props.keyboardShowMethod;
- const resetListener = this.props.keyboardHideMethod;
- this.keyboardListeners = [Keyboard.addListener(updateListener, this.updateKeyboardSpace), Keyboard.addListener(resetListener, this.resetKeyboardSpace)];
- }
-
- componentWillUnmount() {
- this.keyboardListeners.forEach((listener) => listener.remove());
- }
+function BaseKeyboardSpacer(props) {
+ const [keyboardSpace, setKeyboardSpace] = useState(0);
/**
* Update the height of Keyboard View.
*
* @param {Object} [event] - A Keyboard Event.
*/
- updateKeyboardSpace(event) {
- if (!event.endCoordinates) {
- return;
- }
-
- const screenHeight = Dimensions.get('window').height;
- const keyboardSpace = screenHeight - event.endCoordinates.screenY + this.props.topSpacing;
- this.setState(
- {
- keyboardSpace,
- },
- this.props.onToggle(true, keyboardSpace),
- );
- }
+ const updateKeyboardSpace = useCallback(
+ (event) => {
+ if (!event.endCoordinates) {
+ return;
+ }
+
+ const screenHeight = Dimensions.get('window').height;
+ const space = screenHeight - event.endCoordinates.screenY + props.topSpacing;
+ setKeyboardSpace(space);
+ props.onToggle(true, space);
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
/**
* Reset the height of Keyboard View.
*
* @param {Object} [event] - A Keyboard Event.
*/
- resetKeyboardSpace() {
- this.setState(
- {
- keyboardSpace: 0,
- },
- this.props.onToggle(false, 0),
- );
- }
+ const resetKeyboardSpace = useCallback(() => {
+ setKeyboardSpace(0);
+ props.onToggle(false, 0);
+ }, [setKeyboardSpace, props]);
+
+ useEffect(() => {
+ const updateListener = props.keyboardShowMethod;
+ const resetListener = props.keyboardHideMethod;
+ const keyboardListeners = [Keyboard.addListener(updateListener, updateKeyboardSpace), Keyboard.addListener(resetListener, resetKeyboardSpace)];
+
+ return () => {
+ keyboardListeners.forEach((listener) => listener.remove());
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
- render() {
- return ;
- }
+ return ;
}
BaseKeyboardSpacer.defaultProps = defaultProps;
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index 5941f6d17ac0..0d05e8401cce 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -2,7 +2,7 @@ import _ from 'underscore';
import lodashGet from 'lodash/get';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
-import {View} from 'react-native';
+import {View, InteractionManager} from 'react-native';
import Button from '../Button';
import FixedFooter from '../FixedFooter';
import OptionsList from '../OptionsList';
@@ -125,6 +125,14 @@ class BaseOptionsSelector extends Component {
}
componentDidUpdate(prevProps) {
+ if (this.textInput && this.props.autoFocus && !prevProps.isFocused && this.props.isFocused) {
+ InteractionManager.runAfterInteractions(() => {
+ // If we automatically focus on a text input when mounting a component,
+ // let's automatically focus on it when the component updates as well (eg, when navigating back from a page)
+ this.textInput.focus();
+ });
+ }
+
if (_.isEqual(this.props.sections, prevProps.sections)) {
return;
}
diff --git a/src/components/ReportActionItem/TaskView.js b/src/components/ReportActionItem/TaskView.js
index 7eb77c47980a..99916534f986 100644
--- a/src/components/ReportActionItem/TaskView.js
+++ b/src/components/ReportActionItem/TaskView.js
@@ -109,7 +109,7 @@ function TaskView(props) {
onPress={() => Navigation.navigate(ROUTES.getTaskReportDescriptionRoute(props.report.reportID))}
shouldShowRightIcon={isOpen}
disabled={disableState}
- wrapperStyle={[styles.pv2]}
+ wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
shouldGreyOutWhenDisabled={false}
numberOfLinesTitle={0}
/>
diff --git a/src/components/ReportWelcomeText.js b/src/components/ReportWelcomeText.js
index 86c220400267..71392f46037e 100644
--- a/src/components/ReportWelcomeText.js
+++ b/src/components/ReportWelcomeText.js
@@ -114,9 +114,7 @@ function ReportWelcomeText(props) {
))}
)}
- {(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)) && (
- {props.translate('reportActionsView.usePlusButton')}
- )}
+ {moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST) && {props.translate('reportActionsView.usePlusButton')}}
>
);
diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js
index 2e96aeb7b2b9..68c09e3a7f82 100644
--- a/src/components/TextInput/BaseTextInput.js
+++ b/src/components/TextInput/BaseTextInput.js
@@ -395,7 +395,8 @@ function BaseTextInput(props) {
setTextInputHeight(e.nativeEvent.layout.height);
}}
>
- {props.value || props.placeholder}
+ {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */}
+ {props.value ? `${props.value}${props.value.endsWith('\n') ? '\u200B' : ''}` : props.placeholder}
)}
>
diff --git a/src/components/withWindowDimensions.js b/src/components/withWindowDimensions.js
index 674a153f7e10..9ec9c5d4acbd 100644
--- a/src/components/withWindowDimensions.js
+++ b/src/components/withWindowDimensions.js
@@ -1,4 +1,4 @@
-import React, {forwardRef, createContext} from 'react';
+import React, {forwardRef, createContext, useState, useEffect} from 'react';
import PropTypes from 'prop-types';
import {Dimensions} from 'react-native';
import {SafeAreaInsetsContext} from 'react-native-safe-area-context';
@@ -32,77 +32,63 @@ const windowDimensionsProviderPropTypes = {
children: PropTypes.node.isRequired,
};
-class WindowDimensionsProvider extends React.Component {
- constructor(props) {
- super(props);
+function WindowDimensionsProvider(props) {
+ const [windowDimension, setWindowDimension] = useState(() => {
+ const initialDimensions = Dimensions.get('window');
+ return {
+ windowHeight: initialDimensions.height,
+ windowWidth: initialDimensions.width,
+ };
+ });
- this.onDimensionChange = this.onDimensionChange.bind(this);
+ useEffect(() => {
+ const onDimensionChange = (newDimensions) => {
+ const {window} = newDimensions;
- const initialDimensions = Dimensions.get('window');
+ setWindowDimension({
+ windowHeight: window.height,
+ windowWidth: window.width,
+ });
+ };
- this.dimensionsEventListener = null;
+ const dimensionsEventListener = Dimensions.addEventListener('change', onDimensionChange);
- this.state = {
- windowHeight: initialDimensions.height,
- windowWidth: initialDimensions.width,
+ return () => {
+ if (!dimensionsEventListener) {
+ return;
+ }
+ dimensionsEventListener.remove();
};
- }
-
- componentDidMount() {
- this.dimensionsEventListener = Dimensions.addEventListener('change', this.onDimensionChange);
- }
-
- componentWillUnmount() {
- if (!this.dimensionsEventListener) {
- return;
- }
- this.dimensionsEventListener.remove();
- }
-
- /**
- * Stores the application window's width and height in a component state variable.
- * Called each time the application's window dimensions or screen dimensions change.
- * @link https://reactnative.dev/docs/dimensions
- * @param {Object} newDimensions Dimension object containing updated window and screen dimensions
- */
- onDimensionChange(newDimensions) {
- const {window} = newDimensions;
-
- this.setState({
- windowHeight: window.height,
- windowWidth: window.width,
- });
- }
-
- render() {
- return (
-
- {(insets) => {
- const isExtraSmallScreenWidth = this.state.windowWidth <= variables.extraSmallMobileResponsiveWidthBreakpoint;
- const isSmallScreenWidth = this.state.windowWidth <= variables.mobileResponsiveWidthBreakpoint;
- const isMediumScreenWidth = !isSmallScreenWidth && this.state.windowWidth <= variables.tabletResponsiveWidthBreakpoint;
- const isLargeScreenWidth = !isSmallScreenWidth && !isMediumScreenWidth;
- return (
-
- {this.props.children}
-
- );
- }}
-
- );
- }
+ }, []);
+
+ return (
+
+ {(insets) => {
+ const isExtraSmallScreenWidth = windowDimension.windowWidth <= variables.extraSmallMobileResponsiveWidthBreakpoint;
+ const isSmallScreenWidth = windowDimension.windowWidth <= variables.mobileResponsiveWidthBreakpoint;
+ const isMediumScreenWidth = !isSmallScreenWidth && windowDimension.windowWidth <= variables.tabletResponsiveWidthBreakpoint;
+ const isLargeScreenWidth = !isSmallScreenWidth && !isMediumScreenWidth;
+ return (
+
+ {props.children}
+
+ );
+ }}
+
+ );
}
WindowDimensionsProvider.propTypes = windowDimensionsProviderPropTypes;
+WindowDimensionsProvider.displayName = 'WindowDimensionsProvider';
/**
* @param {React.Component} WrappedComponent
diff --git a/src/languages/en.js b/src/languages/en.js
index 47cc8d209735..33fc3f3e74c7 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -298,7 +298,7 @@ export default {
beginningOfChatHistoryPolicyExpenseChatPartThree: ' starts here! 🎉 This is the place to chat, request money and settle up.',
chatWithAccountManager: 'Chat with your account manager here',
sayHello: 'Say hello!',
- usePlusButton: '\n\nYou can also use the + button below to send or request money!',
+ usePlusButton: '\n\nYou can also use the + button below to request money or assign a task!',
},
reportAction: {
asCopilot: 'as copilot for',
diff --git a/src/languages/es.js b/src/languages/es.js
index 5eea74099e4c..b481f89b4940 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -297,7 +297,7 @@ export default {
beginningOfChatHistoryPolicyExpenseChatPartThree: ' empieza aquÃ! 🎉 Este es el lugar donde chatear, pedir dinero y pagar.',
chatWithAccountManager: 'Chatea con tu gestor de cuenta aquÃ',
sayHello: '¡Saluda!',
- usePlusButton: '\n\n¡También puedes usar el botón + de abajo para enviar o pedir dinero!',
+ usePlusButton: '\n\n¡También puedes usar el botón + de abajo para pedir dinero o asignar una tarea!',
},
reportAction: {
asCopilot: 'como copiloto de',
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index 2acb1f51cbe7..56bbd326795d 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -801,7 +801,8 @@ function getOptions(
(noOptions || noOptionsMatchExactly) &&
!isCurrentUser({login: searchValue}) &&
_.every(selectedOptions, (option) => option.login !== searchValue) &&
- ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || parsedPhoneNumber.possible) &&
+ ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) ||
+ (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number.input)))) &&
!_.find(loginOptionsToExclude, (loginOptionToExclude) => loginOptionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) &&
(searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas))
) {
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index c0ada6d34868..8dfd51f515e1 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -577,6 +577,16 @@ function isDM(report) {
return !getChatType(report);
}
+/**
+ * Returns true if report has a single participant.
+ *
+ * @param {Object} report
+ * @returns {Boolean}
+ */
+function hasSingleParticipant(report) {
+ return report.participants && report.participants.length === 1;
+}
+
/**
* If the report is a thread and has a chat type set, it is a workspace chat.
*
@@ -2406,6 +2416,7 @@ function getMoneyRequestOptions(report, reportParticipants, betas) {
const participants = _.filter(reportParticipants, (accountID) => currentUserPersonalDetails.accountID !== accountID);
const hasExcludedIOUAccountIDs = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0;
+ const hasSingleParticipantInReport = participants.length === 1;
const hasMultipleParticipants = participants.length > 1;
if (hasExcludedIOUAccountIDs || (participants.length === 0 && !report.isOwnPolicyExpenseChat)) {
@@ -2431,7 +2442,7 @@ function getMoneyRequestOptions(report, reportParticipants, betas) {
...(canRequestMoney(report) ? [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST] : []),
// Send money option should be visible only in DMs
- ...(Permissions.canUseIOUSend(betas) && isChatReport(report) && !isPolicyExpenseChat(report) && participants.length === 1 ? [CONST.IOU.MONEY_REQUEST_TYPE.SEND] : []),
+ ...(Permissions.canUseIOUSend(betas) && isChatReport(report) && !isPolicyExpenseChat(report) && hasSingleParticipantInReport ? [CONST.IOU.MONEY_REQUEST_TYPE.SEND] : []),
];
}
@@ -2727,7 +2738,9 @@ export {
getOriginalReportID,
canAccessReport,
getReportOfflinePendingActionAndErrors,
+ isDM,
getPolicy,
shouldDisableSettings,
shouldDisableRename,
+ hasSingleParticipant,
};
diff --git a/src/libs/actions/PersistedRequests.js b/src/libs/actions/PersistedRequests.js
index d893ee255287..c99ed2ace7fe 100644
--- a/src/libs/actions/PersistedRequests.js
+++ b/src/libs/actions/PersistedRequests.js
@@ -25,7 +25,15 @@ function save(requestsToPersist) {
* @param {Object} requestToRemove
*/
function remove(requestToRemove) {
- persistedRequests = _.reject(persistedRequests, (persistedRequest) => _.isEqual(persistedRequest, requestToRemove));
+ /**
+ * We only remove the first matching request because the order of requests matters.
+ * If we were to remove all matching requests, we can end up with a final state that is different than what the user intended.
+ */
+ const index = _.findIndex(persistedRequests, (persistedRequest) => _.isEqual(persistedRequest, requestToRemove));
+ if (index !== -1) {
+ persistedRequests.splice(index, 1);
+ }
+
Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests);
}
diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js
index afc6391f3003..9cf64e44038b 100644
--- a/src/libs/actions/PersonalDetails.js
+++ b/src/libs/actions/PersonalDetails.js
@@ -318,7 +318,7 @@ function updateSelectedTimezone(selectedTimezone) {
],
},
);
- Navigation.navigate(ROUTES.SETTINGS_TIMEZONE);
+ Navigation.goBack(ROUTES.SETTINGS_TIMEZONE);
}
/**
diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js
index b3dd76a38d78..32ffa759fa16 100644
--- a/src/libs/actions/Task.js
+++ b/src/libs/actions/Task.js
@@ -13,6 +13,7 @@ import * as UserUtils from '../UserUtils';
import * as ErrorUtils from '../ErrorUtils';
import * as ReportActionsUtils from '../ReportActionsUtils';
import * as Expensicons from '../../components/Icon/Expensicons';
+import * as LocalePhoneNumber from '../LocalePhoneNumber';
let currentUserEmail;
let currentUserAccountID;
@@ -597,10 +598,16 @@ function getAssignee(details) {
* */
function getShareDestination(reportID, reports, personalDetails) {
const report = lodashGet(reports, `report_${reportID}`, {});
+ let subtitle = '';
+ if (ReportUtils.isChatReport(report) && ReportUtils.isDM(report) && ReportUtils.hasSingleParticipant(report)) {
+ subtitle = LocalePhoneNumber.formatPhoneNumber(report.participants[0]);
+ } else {
+ subtitle = ReportUtils.getChatRoomSubtitle(report);
+ }
return {
icons: ReportUtils.getIcons(report, personalDetails, Expensicons.FallbackAvatar, ReportUtils.isIOUReport(report)),
displayName: ReportUtils.getReportName(report),
- subtitle: ReportUtils.getChatRoomSubtitle(report),
+ subtitle,
};
}
diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js
index b8daf12c2ca7..af54965b25ec 100644
--- a/src/pages/ReportDetailsPage.js
+++ b/src/pages/ReportDetailsPage.js
@@ -61,7 +61,7 @@ function ReportDetailsPage(props) {
const policy = useMemo(() => props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`], [props.policies, props.report.policyID]);
const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy), [policy]);
const shouldDisableSettings = useMemo(() => ReportUtils.shouldDisableSettings(props.report), [props.report]);
- const shouldUseFullTitle = shouldDisableSettings;
+ const shouldUseFullTitle = !shouldDisableSettings;
const isThread = useMemo(() => ReportUtils.isChatThread(props.report), [props.report]);
const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(props.report), [props.report]);
const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(props.report), [props.report]);
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index bc8cf0fed584..7622a0e73f18 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -91,8 +91,8 @@ function HeaderView(props) {
const title = ReportUtils.getReportName(reportHeaderData);
const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData);
const parentNavigationSubtitle = ReportUtils.getParentNavigationSubtitle(reportHeaderData);
- const isConcierge = participants.length === 1 && _.contains(participants, CONST.ACCOUNT_ID.CONCIERGE);
- const isAutomatedExpensifyAccount = participants.length === 1 && ReportUtils.hasAutomatedExpensifyAccountIDs(participants);
+ const isConcierge = ReportUtils.hasSingleParticipant(props.report) && _.contains(participants, CONST.ACCOUNT_ID.CONCIERGE);
+ const isAutomatedExpensifyAccount = ReportUtils.hasSingleParticipant(props.report) && ReportUtils.hasAutomatedExpensifyAccountIDs(participants);
const guideCalendarLink = lodashGet(props.account, 'guideCalendarLink');
// We hide the button when we are chatting with an automated Expensify account since it's not possible to contact
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index 8155f09b2aac..8fc3eb1bb30d 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -49,6 +49,7 @@ import OfflineWithFeedback from '../../../components/OfflineWithFeedback';
import * as ComposerUtils from '../../../libs/ComposerUtils';
import * as Welcome from '../../../libs/actions/Welcome';
import Permissions from '../../../libs/Permissions';
+import containerComposeStyles from '../../../styles/containerComposeStyles';
import * as Task from '../../../libs/actions/Task';
import * as Browser from '../../../libs/Browser';
import * as IOU from '../../../libs/actions/IOU';
@@ -241,19 +242,13 @@ class ReportActionCompose extends React.Component {
}
componentDidMount() {
- // This callback is used in the contextMenuActions to manage giving focus back to the compose input.
- // TODO: we should clean up this convoluted code and instead move focus management to something like ReportFooter.js or another higher up component
- ReportActionComposeFocusManager.onComposerFocus(() => {
- if (!this.willBlurTextInputOnTapOutside || !this.props.isFocused) {
- return;
- }
-
- this.focus(false);
- });
-
this.unsubscribeNavigationBlur = this.props.navigation.addListener('blur', () => KeyDownListener.removeKeyDownPressListner(this.focusComposerOnKeyPress));
- this.unsubscribeNavigationFocus = this.props.navigation.addListener('focus', () => KeyDownListener.addKeyDownPressListner(this.focusComposerOnKeyPress));
+ this.unsubscribeNavigationFocus = this.props.navigation.addListener('focus', () => {
+ KeyDownListener.addKeyDownPressListner(this.focusComposerOnKeyPress);
+ this.setUpComposeFocusManager();
+ });
KeyDownListener.addKeyDownPressListner(this.focusComposerOnKeyPress);
+ this.setUpComposeFocusManager();
this.updateComment(this.comment);
@@ -312,6 +307,18 @@ class ReportActionCompose extends React.Component {
this.calculateMentionSuggestion();
}
+ setUpComposeFocusManager() {
+ // This callback is used in the contextMenuActions to manage giving focus back to the compose input.
+ // TODO: we should clean up this convoluted code and instead move focus management to something like ReportFooter.js or another higher up component
+ ReportActionComposeFocusManager.onComposerFocus(() => {
+ if (!this.willBlurTextInputOnTapOutside || !this.props.isFocused) {
+ return;
+ }
+
+ this.focus(false);
+ });
+ }
+
getDefaultSuggestionsValues() {
return {
suggestedEmojis: [],
@@ -1110,7 +1117,7 @@ class ReportActionCompose extends React.Component {
>
)}
-
+
-
+
{
diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js
index 85f0d406465d..90a1cdb464fe 100644
--- a/src/pages/home/report/ReportActionsList.js
+++ b/src/pages/home/report/ReportActionsList.js
@@ -169,7 +169,7 @@ function ReportActionsList(props) {
ref={reportScrollManager.ref}
data={props.sortedReportActions}
renderItem={renderItem}
- contentContainerStyle={[styles.chatContentScrollView, shouldShowReportRecipientLocalTime]}
+ contentContainerStyle={[styles.chatContentScrollView, shouldShowReportRecipientLocalTime ? styles.pt0 : {}]}
keyExtractor={keyExtractor}
initialRowHeight={32}
initialNumToRender={calculateInitialNumToRender()}
diff --git a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
index 71ed4f044d4a..c1054dfad6c1 100644
--- a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
+++ b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
@@ -1,5 +1,5 @@
import React, {useCallback, useEffect, useState} from 'react';
-import {ActivityIndicator, View, InteractionManager, LayoutAnimation} from 'react-native';
+import {ActivityIndicator, View, InteractionManager} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import PaymentMethodList from '../PaymentMethodList';
@@ -230,47 +230,27 @@ function BasePaymentsPage(props) {
[setShouldShowDefaultDeleteMenu, setShowConfirmDeleteContent, resetSelectedPaymentMethodData],
);
- const hidePasswordPrompt = useCallback(
- (shouldClearSelectedData = true) => {
- setShowPassword({
- shouldShowPasswordPrompt: false,
- passwordButtonText: '',
- });
- if (shouldClearSelectedData) {
- resetSelectedPaymentMethodData();
- }
-
- // Due to iOS modal freeze issue, password modal freezes the app when closed.
- // LayoutAnimation undoes the running animation.
- LayoutAnimation.configureNext(LayoutAnimation.create(50, LayoutAnimation.Types.easeInEaseOut, LayoutAnimation.Properties.opacity));
- },
- [setShowPassword, resetSelectedPaymentMethodData],
- );
+ const makeDefaultPaymentMethod = useCallback(() => {
+ // Find the previous default payment method so we can revert if the MakeDefaultPaymentMethod command errors
+ const paymentMethods = PaymentUtils.formatPaymentMethods(props.bankAccountList, props.cardList);
- const makeDefaultPaymentMethod = useCallback(
- (password = '') => {
- // Find the previous default payment method so we can revert if the MakeDefaultPaymentMethod command errors
- const paymentMethods = PaymentUtils.formatPaymentMethods(props.bankAccountList, props.cardList);
-
- const previousPaymentMethod = _.find(paymentMethods, (method) => method.isDefault);
- const currentPaymentMethod = _.find(paymentMethods, (method) => method.methodID === paymentMethod.methodID);
- if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.BANK_ACCOUNT) {
- PaymentMethods.makeDefaultPaymentMethod(password, paymentMethod.selectedPaymentMethod.bankAccountID, null, previousPaymentMethod, currentPaymentMethod);
- } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD) {
- PaymentMethods.makeDefaultPaymentMethod(password, null, paymentMethod.selectedPaymentMethod.fundID, previousPaymentMethod, currentPaymentMethod);
- }
- resetSelectedPaymentMethodData();
- },
- [
- paymentMethod.methodID,
- paymentMethod.selectedPaymentMethod.bankAccountID,
- paymentMethod.selectedPaymentMethod.fundID,
- paymentMethod.selectedPaymentMethodType,
- props.bankAccountList,
- props.cardList,
- resetSelectedPaymentMethodData,
- ],
- );
+ const previousPaymentMethod = _.find(paymentMethods, (method) => method.isDefault);
+ const currentPaymentMethod = _.find(paymentMethods, (method) => method.methodID === paymentMethod.methodID);
+ if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.BANK_ACCOUNT) {
+ PaymentMethods.makeDefaultPaymentMethod(paymentMethod.selectedPaymentMethod.bankAccountID, null, previousPaymentMethod, currentPaymentMethod);
+ } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD) {
+ PaymentMethods.makeDefaultPaymentMethod(null, paymentMethod.selectedPaymentMethod.fundID, previousPaymentMethod, currentPaymentMethod);
+ }
+ resetSelectedPaymentMethodData();
+ }, [
+ paymentMethod.methodID,
+ paymentMethod.selectedPaymentMethod.bankAccountID,
+ paymentMethod.selectedPaymentMethod.fundID,
+ paymentMethod.selectedPaymentMethodType,
+ props.bankAccountList,
+ props.cardList,
+ resetSelectedPaymentMethodData,
+ ]);
const deletePaymentMethod = useCallback(() => {
if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PAYPAL) {
@@ -386,13 +366,10 @@ function BasePaymentsPage(props) {
// Close corresponding selected payment method modals which are open
if (shouldShowDefaultDeleteMenu) {
hideDefaultDeleteMenu();
- } else if (showPassword.shouldShowPasswordPrompt) {
- hidePasswordPrompt();
}
}
}, [
hideDefaultDeleteMenu,
- hidePasswordPrompt,
paymentMethod.methodID,
paymentMethod.selectedPaymentMethodType,
props.bankAccountList,
@@ -509,7 +486,7 @@ function BasePaymentsPage(props) {
deletePaymentMethod();
}}
onCancel={hideDefaultDeleteMenu}
- contentStyles={!isSmallScreenWidth ? [styles.sidebarPopover] : undefined}
+ contentStyles={!isSmallScreenWidth ? [styles.sidebarPopover, styles.willChangeTransform] : undefined}
title={translate('paymentsPage.deleteAccount')}
prompt={translate('paymentsPage.deleteConfirmation')}
confirmText={translate('common.delete')}
diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
index 04a56a07eeb4..ea81413fcbb5 100644
--- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
+++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
@@ -1,6 +1,7 @@
import React, {useCallback, useState, useEffect, useRef} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
+import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
import MagicCodeInput from '../../../../../components/MagicCodeInput';
@@ -71,6 +72,7 @@ function BaseValidateCodeForm(props) {
const [validateCode, setValidateCode] = useState('');
const loginData = props.loginList[props.contactMethod];
const inputValidateCodeRef = useRef();
+ const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin');
useEffect(() => {
if (!props.hasMagicCodeBeenSent) {
@@ -134,6 +136,7 @@ function BaseValidateCodeForm(props) {
value={validateCode}
onChangeText={onTextInput}
errorText={formError.validateCode ? props.translate(formError.validateCode) : ErrorUtils.getLatestErrorMessage(props.account)}
+ hasError={!_.isEmpty(validateLoginError)}
onFulfill={validateAndSubmitForm}
autoFocus
shouldDelayFocus={shouldDelayFocus}
@@ -168,7 +171,7 @@ function BaseValidateCodeForm(props) {
User.clearContactMethodErrors(props.contactMethod, 'validateLogin')}
>
diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js
index 328d8837f724..6cad4cc12869 100644
--- a/src/pages/tasks/TaskAssigneeSelectorModal.js
+++ b/src/pages/tasks/TaskAssigneeSelectorModal.js
@@ -160,7 +160,7 @@ function TaskAssigneeSelectorModal(props) {
// Clear out the state value, set the assignee and navigate back to the NewTaskPage
setSearchValue('');
Task.setAssigneeValue(option.login, option.accountID, props.task.shareDestination, OptionsListUtils.isCurrentUser(option));
- return Navigation.goBack();
+ return Navigation.goBack(ROUTES.NEW_TASK);
}
// Check to see if we're editing a task and if so, update the assignee
diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js
index ce82aeeffe50..7a4250d8cbe9 100644
--- a/src/pages/tasks/TaskShareDestinationSelectorModal.js
+++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js
@@ -130,7 +130,7 @@ function TaskShareDestinationSelectorModal(props) {
// Clear out the state value, set the assignee and navigate back to the NewTaskPage
setSearchValue('');
Task.setShareDestinationValue(option.reportID);
- Navigation.goBack();
+ Navigation.goBack(ROUTES.NEW_TASK);
}
};
diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseSection.js b/src/pages/workspace/reimburse/WorkspaceReimburseSection.js
index a53cc01d51d2..eb8305f23140 100644
--- a/src/pages/workspace/reimburse/WorkspaceReimburseSection.js
+++ b/src/pages/workspace/reimburse/WorkspaceReimburseSection.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useState, useEffect} from 'react';
import PropTypes from 'prop-types';
import {ActivityIndicator, View} from 'react-native';
import lodashGet from 'lodash/get';
@@ -32,102 +32,91 @@ const propTypes = {
translate: PropTypes.func.isRequired,
};
-class WorkspaceReimburseSection extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- shouldShowLoadingSpinner: false,
- };
+function WorkspaceReimburseSection(props) {
+ const [shouldShowLoadingSpinner, setShouldShowLoadingSpinner] = useState(false);
+ const achState = lodashGet(props.reimbursementAccount, 'achData.state', '');
+ const hasVBA = achState === BankAccount.STATE.OPEN;
+ const reimburseReceiptsUrl = `reports?policyID=${props.policy.id}&from=all&type=expense&showStates=Archived&isAdvancedFilterMode=true`;
+ const debounceSetShouldShowLoadingSpinner = _.debounce(() => {
+ const isLoading = props.reimbursementAccount.isLoading || false;
+ if (isLoading !== shouldShowLoadingSpinner) {
+ setShouldShowLoadingSpinner(isLoading);
+ }
+ }, CONST.TIMING.SHOW_LOADING_SPINNER_DEBOUNCE_TIME);
+ useEffect(() => {
+ debounceSetShouldShowLoadingSpinner();
+ }, [debounceSetShouldShowLoadingSpinner]);
- this.debounceSetShouldShowLoadingSpinner = _.debounce(this.setShouldShowLoadingSpinner.bind(this), CONST.TIMING.SHOW_LOADING_SPINNER_DEBOUNCE_TIME);
+ if (props.network.isOffline) {
+ return (
+
+
+ {`${props.translate('common.youAppearToBeOffline')} ${props.translate('common.thisFeatureRequiresInternet')}`}
+
+
+ );
}
- componentDidUpdate() {
- this.debounceSetShouldShowLoadingSpinner();
+ // If the reimbursementAccount is loading but not enough time has passed to show a spinner, then render nothing.
+ if (props.reimbursementAccount.isLoading && !shouldShowLoadingSpinner) {
+ return null;
}
- setShouldShowLoadingSpinner() {
- const shouldShowLoadingSpinner = this.props.reimbursementAccount.isLoading || false;
- if (shouldShowLoadingSpinner !== this.state.shouldShowLoadingSpinner) {
- this.setState({shouldShowLoadingSpinner});
- }
+ if (shouldShowLoadingSpinner) {
+ return (
+
+
+
+ );
}
- render() {
- const achState = lodashGet(this.props.reimbursementAccount, 'achData.state', '');
- const hasVBA = achState === BankAccount.STATE.OPEN;
- const reimburseReceiptsUrl = `reports?policyID=${this.props.policy.id}&from=all&type=expense&showStates=Archived&isAdvancedFilterMode=true`;
-
- if (this.props.network.isOffline) {
- return (
+ return (
+ <>
+ {hasVBA ? (
Link.openOldDotLink(reimburseReceiptsUrl),
+ icon: Expensicons.Bank,
+ shouldShowRightIcon: true,
+ iconRight: Expensicons.NewWindow,
+ wrapperStyle: [styles.cardMenuItem],
+ link: () => Link.buildOldDotURL(reimburseReceiptsUrl),
+ },
+ ]}
>
- {`${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('common.thisFeatureRequiresInternet')}`}
+ {props.translate('workspace.reimburse.fastReimbursementsVBACopy')}
- );
- }
-
- // If the reimbursementAccount is loading but not enough time has passed to show a spinner, then render nothing.
- if (this.props.reimbursementAccount.isLoading && !this.state.shouldShowLoadingSpinner) {
- return null;
- }
-
- if (this.state.shouldShowLoadingSpinner) {
- return (
-
-
+
+ {props.translate('workspace.reimburse.unlockNoVBACopy')}
+
+
-
- );
- }
-
- return (
- <>
- {hasVBA ? (
- Link.openOldDotLink(reimburseReceiptsUrl),
- icon: Expensicons.Bank,
- shouldShowRightIcon: true,
- iconRight: Expensicons.NewWindow,
- wrapperStyle: [styles.cardMenuItem],
- link: () => Link.buildOldDotURL(reimburseReceiptsUrl),
- },
- ]}
- >
-
- {this.props.translate('workspace.reimburse.fastReimbursementsVBACopy')}
-
-
- ) : (
-
-
- {this.props.translate('workspace.reimburse.unlockNoVBACopy')}
-
-
-
- )}
- >
- );
- }
+
+ )}
+ >
+ );
}
WorkspaceReimburseSection.propTypes = propTypes;
+WorkspaceReimburseSection.displayName = 'WorkspaceReimburseSection';
export default WorkspaceReimburseSection;
diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js
index c8449d16d176..707221a7c747 100644
--- a/src/styles/StyleUtils.js
+++ b/src/styles/StyleUtils.js
@@ -1222,6 +1222,22 @@ function getMentionTextColor(isOurMention) {
return isOurMention ? themeColors.ourMentionText : themeColors.mentionText;
}
+/**
+ * Returns padding vertical based on number of lines
+ * @param {Number} numberOfLines
+ * @returns {Object}
+ */
+function getComposeTextAreaPadding(numberOfLines) {
+ let paddingValue = 5;
+ if (numberOfLines === 1) paddingValue = 9;
+ // In case numberOfLines = 3, there will be a Expand Icon appearing at the top left, so it has to be recalculated so that the textArea can be full height
+ if (numberOfLines === 3) paddingValue = 8;
+ return {
+ paddingTop: paddingValue,
+ paddingBottom: paddingValue,
+ };
+}
+
/**
* Returns style object for the mobile on WEB
* @param {Number} windowHeight
@@ -1357,6 +1373,7 @@ export {
getEmojiPickerListHeight,
getMentionStyle,
getMentionTextColor,
+ getComposeTextAreaPadding,
getHeightOfMagicCodeInput,
getOuterModalStyle,
getWrappingStyle,
diff --git a/src/styles/containerComposeStyles/index.js b/src/styles/containerComposeStyles/index.js
new file mode 100644
index 000000000000..23a4d7ed7720
--- /dev/null
+++ b/src/styles/containerComposeStyles/index.js
@@ -0,0 +1,6 @@
+import styles from '../styles';
+
+// We need to set paddingVertical = 0 on web to avoid displaying a normal pointer on some parts of compose box when not in focus
+const containerComposeStyles = [styles.textInputComposeSpacing, {paddingVertical: 0}];
+
+export default containerComposeStyles;
diff --git a/src/styles/containerComposeStyles/index.native.js b/src/styles/containerComposeStyles/index.native.js
new file mode 100644
index 000000000000..002331581108
--- /dev/null
+++ b/src/styles/containerComposeStyles/index.native.js
@@ -0,0 +1,5 @@
+import styles from '../styles';
+
+const containerComposeStyles = [styles.textInputComposeSpacing];
+
+export default containerComposeStyles;
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 27d216e41a59..105b05e9475c 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -22,10 +22,14 @@ import pointerEventsAuto from './pointerEventsAuto';
import getPopOverVerticalOffset from './getPopOverVerticalOffset';
import overflowXHidden from './overflowXHidden';
import CONST from '../CONST';
+import * as Browser from '../libs/Browser';
import cursor from './utilities/cursor';
import userSelect from './utilities/userSelect';
import textUnderline from './utilities/textUnderline';
+// touchCallout is an iOS safari only property that controls the display of the callout information when you touch and hold a target
+const touchCalloutNone = Browser.isMobileSafari() ? {WebkitTouchCallout: 'none'} : {};
+
const picker = {
backgroundColor: themeColors.transparent,
color: themeColors.text,
@@ -130,6 +134,7 @@ const webViewStyles = {
borderColor: themeColors.border,
borderRadius: variables.componentBorderRadiusNormal,
borderWidth: 1,
+ ...touchCalloutNone,
},
p: {
@@ -3432,6 +3437,11 @@ const styles = {
...wordBreak.breakWord,
},
+ taskDescriptionMenuItem: {
+ maxWidth: '100%',
+ ...wordBreak.breakWord,
+ },
+
taskTitleDescription: {
fontFamily: fontFamily.EXP_NEUE,
fontSize: variables.fontSizeLabel,
@@ -3529,6 +3539,10 @@ const styles = {
left: 0,
right: 0,
}),
+
+ willChangeTransform: {
+ willChange: 'transform',
+ },
};
export default styles;
diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js
index 86592e6f1aaf..76ac7990d133 100644
--- a/tests/unit/OptionsListUtilsTest.js
+++ b/tests/unit/OptionsListUtilsTest.js
@@ -522,6 +522,14 @@ describe('OptionsListUtils', () => {
expect(results.userToInvite).not.toBe(null);
expect(results.userToInvite.login).toBe('+18003243233');
+ // When we use a search term for contact number that contains alphabet characters
+ results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '998243aaaa');
+
+ // Then we shouldn't have any results or user to invite
+ expect(results.recentReports.length).toBe(0);
+ expect(results.personalDetails.length).toBe(0);
+ expect(results.userToInvite).toBe(null);
+
// Test Concierge's existence in new group options
results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE);