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),