diff --git a/android/app/build.gradle b/android/app/build.gradle
index c6a9c3147118..f957ea7c5854 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -90,8 +90,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001037007
- versionName "1.3.70-7"
+ versionCode 1001037105
+ versionName "1.3.71-5"
}
flavorDimensions "default"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 03dcc7770df0..07e1cfae80ac 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.3.70
+ 1.3.71
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.70.7
+ 1.3.71.5
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 941d232244e1..49d12cf93594 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.3.70
+ 1.3.71
CFBundleSignature
????
CFBundleVersion
- 1.3.70.7
+ 1.3.71.5
diff --git a/package-lock.json b/package-lock.json
index 382dcf45f55e..6a366d6c61a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.70-7",
+ "version": "1.3.71-5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.70-7",
+ "version": "1.3.71-5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 0073dedb741c..0605d44d89fc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.70-7",
+ "version": "1.3.71-5",
"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.",
diff --git a/src/components/DistanceRequest.js b/src/components/DistanceRequest.js
index bf5a4cb9548b..74868cf6520b 100644
--- a/src/components/DistanceRequest.js
+++ b/src/components/DistanceRequest.js
@@ -109,10 +109,11 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken,
const lastWaypointIndex = numberOfWaypoints - 1;
const isLoadingRoute = lodashGet(transaction, 'comment.isLoading', false);
const hasRouteError = !!lodashGet(transaction, 'errorFields.route');
- const haveWaypointsChanged = !_.isEqual(previousWaypoints, waypoints);
const doesRouteExist = lodashHas(transaction, 'routes.route0.geometry.coordinates');
const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints);
- const shouldFetchRoute = (!doesRouteExist || haveWaypointsChanged) && !isLoadingRoute && _.size(validatedWaypoints) > 1;
+ const previousValidatedWaypoints = usePrevious(validatedWaypoints);
+ const haveValidatedWaypointsChanged = !_.isEqual(previousValidatedWaypoints, validatedWaypoints);
+ const shouldFetchRoute = (!doesRouteExist || haveValidatedWaypointsChanged) && !isLoadingRoute && _.size(validatedWaypoints) > 1;
const waypointMarkers = useMemo(
() =>
_.filter(
diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js
index 88d3df3b7001..c8906889612f 100644
--- a/src/components/MenuItem.js
+++ b/src/components/MenuItem.js
@@ -1,6 +1,7 @@
import _ from 'underscore';
-import React from 'react';
+import React, {useEffect} from 'react';
import {View} from 'react-native';
+import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import Text from './Text';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
@@ -34,6 +35,7 @@ const defaultProps = {
shouldShowBasicTitle: false,
shouldShowDescriptionOnTop: false,
shouldShowHeaderTitle: false,
+ shouldParseTitle: false,
wrapperStyle: [],
style: styles.popoverMenuItem,
titleStyle: {},
@@ -79,6 +81,7 @@ const defaultProps = {
const MenuItem = React.forwardRef((props, ref) => {
const {isSmallScreenWidth} = useWindowDimensions();
+ const [html, setHtml] = React.useState('');
const isDeleted = _.contains(props.style, styles.offlineFeedback.deleted);
const descriptionVerticalMargin = props.shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1;
@@ -106,6 +109,16 @@ const MenuItem = React.forwardRef((props, ref) => {
const fallbackAvatarSize = props.viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT;
+ const titleRef = React.useRef('');
+ useEffect(() => {
+ if (!props.title || (titleRef.current.length && titleRef.current === props.title) || !props.shouldParseTitle) {
+ return;
+ }
+ const parser = new ExpensiMark();
+ setHtml(parser.replace(convertToLTR(props.title)));
+ titleRef.current = props.title;
+ }, [props.title, props.shouldParseTitle]);
+
return (
{(isHovered) => (
@@ -224,7 +237,9 @@ const MenuItem = React.forwardRef((props, ref) => {
{Boolean(props.title) && Boolean(props.shouldRenderAsHTML) && }
- {Boolean(props.title) && !props.shouldRenderAsHTML && (
+ {Boolean(props.shouldParseTitle) && Boolean(html.length) && !props.shouldRenderAsHTML && ${html}`} />}
+
+ {!props.shouldRenderAsHTML && !html.length && Boolean(props.title) && (
{
{convertToLTR(props.title)}
)}
+
{Boolean(props.shouldShowTitleIcon) && (
Navigation.navigate(ROUTES.getMoneyRequestDescriptionRoute(props.iouType, props.reportID))}
diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js
index 902e21b9ce25..712c7ded6ab0 100644
--- a/src/components/ReportActionItem/MoneyRequestView.js
+++ b/src/components/ReportActionItem/MoneyRequestView.js
@@ -133,6 +133,7 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, trans
Navigation.navigate(ROUTES.getTaskReportDescriptionRoute(props.report.reportID))}
diff --git a/src/hooks/usePrivatePersonalDetails.js b/src/hooks/usePrivatePersonalDetails.js
index 37eb63dcd0fd..14c1e42e629a 100644
--- a/src/hooks/usePrivatePersonalDetails.js
+++ b/src/hooks/usePrivatePersonalDetails.js
@@ -1,4 +1,5 @@
import {useEffect, useContext} from 'react';
+import _ from 'underscore';
import * as PersonalDetails from '../libs/actions/PersonalDetails';
import {NetworkContext} from '../components/OnyxProvider';
@@ -9,7 +10,8 @@ export default function usePrivatePersonalDetails() {
const {isOffline} = useContext(NetworkContext);
useEffect(() => {
- if (isOffline || Boolean(PersonalDetails.getPrivatePersonalDetails())) {
+ const personalDetails = PersonalDetails.getPrivatePersonalDetails();
+ if (isOffline || (Boolean(personalDetails) && !_.isUndefined(personalDetails.isLoading))) {
return;
}
PersonalDetails.openPersonalDetailsPage();
diff --git a/src/libs/LocalePhoneNumber.js b/src/libs/LocalePhoneNumber.ts
similarity index 82%
rename from src/libs/LocalePhoneNumber.js
rename to src/libs/LocalePhoneNumber.ts
index e5c7cbfa45ba..962040aee049 100644
--- a/src/libs/LocalePhoneNumber.js
+++ b/src/libs/LocalePhoneNumber.ts
@@ -3,20 +3,17 @@ import Str from 'expensify-common/lib/str';
import {parsePhoneNumber} from 'awesome-phonenumber';
import ONYXKEYS from '../ONYXKEYS';
-let countryCodeByIP;
+let countryCodeByIP: number;
Onyx.connect({
key: ONYXKEYS.COUNTRY_CODE,
- callback: (val) => (countryCodeByIP = val || 1),
+ callback: (val) => (countryCodeByIP = val ?? 1),
});
/**
* Returns a locally converted phone number for numbers from the same region
* and an internationally converted phone number with the country code for numbers from other regions
- *
- * @param {String} number
- * @returns {String}
*/
-function formatPhoneNumber(number) {
+function formatPhoneNumber(number: string): string {
if (!number) {
return '';
}
@@ -26,7 +23,7 @@ function formatPhoneNumber(number) {
// return the string untouched if it's not a phone number
if (!parsedPhoneNumber.valid) {
- if (parsedPhoneNumber.number && parsedPhoneNumber.number.international) {
+ if (parsedPhoneNumber.number?.international) {
return parsedPhoneNumber.number.international;
}
return numberWithoutSMSDomain;
diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js
index 4ca8b48d284e..dccabd74772b 100644
--- a/src/libs/TransactionUtils.js
+++ b/src/libs/TransactionUtils.js
@@ -384,4 +384,5 @@ export {
isDistanceRequest,
hasMissingSmartscanFields,
getWaypointIndex,
+ waypointHasValidAddress,
};
diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts
index 16a974db25ff..4196fb4c3281 100644
--- a/src/libs/actions/Transaction.ts
+++ b/src/libs/actions/Transaction.ts
@@ -6,6 +6,7 @@ import * as CollectionUtils from '../CollectionUtils';
import * as API from '../API';
import {RecentWaypoint, Transaction} from '../../types/onyx';
import {WaypointCollection} from '../../types/onyx/Transaction';
+import * as TransactionUtils from '../TransactionUtils';
let recentWaypoints: RecentWaypoint[] = [];
Onyx.connect({
@@ -50,15 +51,6 @@ function addStop(transactionID: string) {
[`waypoint${newLastIndex}`]: {},
},
},
-
- // Clear the existing route so that we don't show an old route
- routes: {
- route0: {
- geometry: {
- coordinates: null,
- },
- },
- },
});
}
@@ -114,7 +106,8 @@ function removeWaypoint(transactionID: string, currentIndex: string) {
}
const waypointValues = Object.values(existingWaypoints);
- waypointValues.splice(index, 1);
+ const removed = waypointValues.splice(index, 1);
+ const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0] ?? {});
const reIndexedWaypoints: WaypointCollection = {};
waypointValues.forEach((waypoint, idx) => {
@@ -124,23 +117,29 @@ function removeWaypoint(transactionID: string, currentIndex: string) {
// Onyx.merge won't remove the null nested object values, this is a workaround
// to remove nested keys while also preserving other object keys
// Doing a deep clone of the transaction to avoid mutating the original object and running into a cache issue when using Onyx.set
- const newTransaction: Transaction = {
+ let newTransaction: Transaction = {
...transaction,
comment: {
...transaction.comment,
waypoints: reIndexedWaypoints,
},
- // Clear the existing route so that we don't show an old route
- routes: {
- route0: {
- distance: null,
- geometry: {
- coordinates: null,
- type: '',
+ };
+
+ if (!isRemovedWaypointEmpty) {
+ newTransaction = {
+ ...newTransaction,
+ // Clear the existing route so that we don't show an old route
+ routes: {
+ route0: {
+ distance: null,
+ geometry: {
+ type: '',
+ coordinates: null,
+ },
},
},
- },
- };
+ };
+ }
Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, newTransaction);
}
diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js
index 687570af12e6..a760627e53cc 100644
--- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js
+++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js
@@ -42,12 +42,6 @@ const propTypes = {
/** Callback when a emoji was inserted */
onInsertedEmoji: PropTypes.func.isRequired,
- /** The current selection */
- selection: PropTypes.shape({
- start: PropTypes.number.isRequired,
- end: PropTypes.number.isRequired,
- }).isRequired,
-
...SuggestionProps.baseProps,
};
diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js
index 79b5d1d66e36..a76025b67b1e 100644
--- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js
+++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js
@@ -1,4 +1,4 @@
-import React, {useState, useCallback, useRef, useImperativeHandle} from 'react';
+import React, {useState, useCallback, useRef, useImperativeHandle, useEffect} from 'react';
import PropTypes from 'prop-types';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
@@ -45,6 +45,7 @@ const defaultProps = {
function SuggestionMention({
value,
setValue,
+ selection,
setSelection,
isComposerFullSize,
personalDetails,
@@ -231,12 +232,9 @@ function SuggestionMention({
[getMentionOptions, personalDetails, resetSuggestions, setHighlightedMentionIndex, value],
);
- const onSelectionChange = useCallback(
- (e) => {
- calculateMentionSuggestion(e.nativeEvent.selection.end);
- },
- [calculateMentionSuggestion],
- );
+ useEffect(() => {
+ calculateMentionSuggestion(selection.end);
+ }, [selection, calculateMentionSuggestion]);
const updateShouldShowSuggestionMenuToFalse = useCallback(() => {
setSuggestionValues((prevState) => {
@@ -262,12 +260,11 @@ function SuggestionMention({
forwardedRef,
() => ({
resetSuggestions,
- onSelectionChange,
triggerHotkeyActions,
setShouldBlockSuggestionCalc,
updateShouldShowSuggestionMenuToFalse,
}),
- [onSelectionChange, resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse],
+ [resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse],
);
if (!isMentionSuggestionsMenuVisible) {
diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.js b/src/pages/home/report/ReportActionCompose/Suggestions.js
index ed2ab9586d52..60cb9de4ccfb 100644
--- a/src/pages/home/report/ReportActionCompose/Suggestions.js
+++ b/src/pages/home/report/ReportActionCompose/Suggestions.js
@@ -66,8 +66,7 @@ function Suggestions({
const onSelectionChange = useCallback((e) => {
const emojiHandler = suggestionEmojiRef.current.onSelectionChange(e);
- const mentionHandler = suggestionMentionRef.current.onSelectionChange(e);
- return emojiHandler || mentionHandler;
+ return emojiHandler;
}, []);
const updateShouldShowSuggestionMenuToFalse = useCallback(() => {
@@ -102,6 +101,7 @@ function Suggestions({
value,
setValue,
setSelection,
+ selection,
isComposerFullSize,
updateComment,
composerHeight,
@@ -116,7 +116,6 @@ function Suggestions({
ref={suggestionEmojiRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...baseProps}
- selection={selection}
onInsertedEmoji={onInsertedEmoji}
resetKeyboardInput={resetKeyboardInput}
/>
diff --git a/src/pages/home/report/ReportActionCompose/suggestionProps.js b/src/pages/home/report/ReportActionCompose/suggestionProps.js
index 24cf51b018c4..12447929b980 100644
--- a/src/pages/home/report/ReportActionCompose/suggestionProps.js
+++ b/src/pages/home/report/ReportActionCompose/suggestionProps.js
@@ -7,6 +7,12 @@ const baseProps = {
/** Callback to update the current input value */
setValue: PropTypes.func.isRequired,
+ /** The current selection value */
+ selection: PropTypes.shape({
+ start: PropTypes.number.isRequired,
+ end: PropTypes.number.isRequired,
+ }).isRequired,
+
/** Callback to update the current selection */
setSelection: PropTypes.func.isRequired,
diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js
index 68c5163643b5..8e1fb6aafdd4 100644
--- a/src/pages/home/report/ReportActionItemParentAction.js
+++ b/src/pages/home/report/ReportActionItemParentAction.js
@@ -1,5 +1,5 @@
import React from 'react';
-import {View} from 'react-native';
+import {View, Image} from 'react-native';
import lodashGet from 'lodash/get';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
@@ -15,6 +15,7 @@ import withLocalize from '../../../components/withLocalize';
import ReportActionItem from './ReportActionItem';
import reportActionPropTypes from './reportActionPropTypes';
import * as ReportActionsUtils from '../../../libs/ReportActionsUtils';
+import EmptyStateBackgroundImage from '../../../../assets/images/empty-state_background-fade.png';
const propTypes = {
/** Flag to show, hide the thread divider line */
@@ -60,6 +61,11 @@ function ReportActionItemParentAction(props) {
onClose={() => Report.navigateToConciergeChatAndDeleteReport(props.report.reportID)}
>
+
{parentReportAction && (
{
- const reportIDs = SidebarUtils.getOrderedReportIDs(currentReportID, chatReports, betas, policies, priorityMode, allReportActions);
+ const reportIDs = SidebarUtils.getOrderedReportIDs(null, chatReports, betas, policies, priorityMode, allReportActions);
if (deepEqual(reportIDsRef.current, reportIDs)) {
return reportIDsRef.current;
}
// We need to update existing reports only once while loading because they are updated several times during loading and causes this regression: https://github.com/Expensify/App/issues/24596#issuecomment-1681679531
- if (!isLoading || !reportIDsRef.current || (_.isEmpty(reportIDsRef.current) && currentReportID)) {
+ if (!isLoading || !reportIDsRef.current) {
reportIDsRef.current = reportIDs;
}
return reportIDsRef.current || [];
- }, [allReportActions, betas, chatReports, currentReportID, policies, priorityMode, isLoading]);
+ }, [allReportActions, betas, chatReports, policies, priorityMode, isLoading]);
+
+ // We need to make sure the current report is in the list of reports, but we do not want
+ // to have to re-generate the list every time the currentReportID changes. To do that
+ // we first generate the list as if there was no current report, then here we check if
+ // the current report is missing from the list, which should very rarely happen. In this
+ // case we re-generate the list a 2nd time with the current report included.
+ const optionListItemsWithCurrentReport = useMemo(() => {
+ if (currentReportID && !_.contains(optionListItems, currentReportID)) {
+ return SidebarUtils.getOrderedReportIDs(currentReportID, chatReports, betas, policies, priorityMode, allReportActions);
+ }
+ return optionListItems;
+ }, [currentReportID, optionListItems, chatReports, betas, policies, priorityMode, allReportActions]);
const currentReportIDRef = useRef(currentReportID);
currentReportIDRef.current = currentReportID;
@@ -100,7 +112,7 @@ function SidebarLinksData({isFocused, allReportActions, betas, chatReports, curr
// Data props:
isActiveReport={isActiveReport}
isLoading={isLoading}
- optionListItems={optionListItems}
+ optionListItems={optionListItemsWithCurrentReport}
/>
);
diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js
index 99050e73bda0..9fb600e40753 100644
--- a/src/pages/tasks/NewTaskPage.js
+++ b/src/pages/tasks/NewTaskPage.js
@@ -162,6 +162,7 @@ function NewTaskPage(props) {
title={description}
onPress={() => Navigation.navigate(ROUTES.NEW_TASK_DESCRIPTION)}
shouldShowRightIcon
+ shouldParseTitle
numberOfLinesTitle={2}
titleStyle={styles.flex1}
/>
diff --git a/src/styles/styles.js b/src/styles/styles.js
index c7ba61916f51..ef69eed9c6b6 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -29,6 +29,8 @@ 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'} : {};
+// to prevent vertical text offset in Safari for badges, new lineHeight values have been added
+const lineHeightBadge = Browser.isSafari() ? {lineHeight: variables.lineHeightXSmall} : {lineHeight: variables.lineHeightNormal};
const picker = (theme) => ({
backgroundColor: theme.transparent,
@@ -758,7 +760,7 @@ const styles = (theme) => ({
badgeText: {
color: theme.text,
fontSize: variables.fontSizeSmall,
- lineHeight: variables.lineHeightNormal,
+ ...lineHeightBadge,
...whiteSpace.noWrap,
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index 6592acd84aad..d91c881e1ffd 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -88,6 +88,7 @@ export default {
optionRowHeightCompact: 52,
optionsListSectionHeaderHeight: getValueUsingPixelRatio(32, 38),
overlayOpacity: 0.6,
+ lineHeightXSmall: getValueUsingPixelRatio(11, 17),
lineHeightSmall: getValueUsingPixelRatio(14, 16),
lineHeightNormal: getValueUsingPixelRatio(16, 21),
lineHeightLarge: getValueUsingPixelRatio(18, 24),