diff --git a/.github/ISSUE_TEMPLATE/Performance.md b/.github/ISSUE_TEMPLATE/Performance.md index df1881aaf5d2..49871a616bc3 100644 --- a/.github/ISSUE_TEMPLATE/Performance.md +++ b/.github/ISSUE_TEMPLATE/Performance.md @@ -2,7 +2,7 @@ name: Report a performance issue about: Use to report an issue or propose a solution related to app performance title: "[Performance] " -labels: Engineering +labels: Engineering, Daily --- If you haven’t already, check out our [contributing guidelines](https://github.com/Expensify/ReactNativeChat/blob/main/CONTRIBUTING.md) for onboarding and email contributors@expensify.com to request to join our Slack channel! diff --git a/.github/ISSUE_TEMPLATE/Standard.md b/.github/ISSUE_TEMPLATE/Standard.md index f0c8561d2a8a..514d25f8b29f 100644 --- a/.github/ISSUE_TEMPLATE/Standard.md +++ b/.github/ISSUE_TEMPLATE/Standard.md @@ -1,7 +1,7 @@ --- name: Standard issue template about: A standard template to follow when creating a new issue in this repository -labels: AutoAssignerTriage +labels: AutoAssignerTriage, Daily --- If you haven’t already, check out our [contributing guidelines](https://github.com/Expensify/ReactNativeChat/blob/main/CONTRIBUTING.md) for onboarding and email contributors@expensify.com to request to join our Slack channel! diff --git a/.github/workflows/deployBlocker.yml b/.github/workflows/deployBlocker.yml index fcbcdf9c5aa6..cce29a3112c1 100644 --- a/.github/workflows/deployBlocker.yml +++ b/.github/workflows/deployBlocker.yml @@ -24,7 +24,7 @@ jobs: run: | echo "DEPLOY_BLOCKER_URL=${{ github.event.issue.html_url }}" >> $GITHUB_ENV echo "DEPLOY_BLOCKER_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV - echo "DEPLOY_BLOCKER_TITLE=${{ github.event.issue.title }}" >> $GITHUB_ENV + echo "DEPLOY_BLOCKER_TITLE=$(sed -e "s/'/'\\\\''/g; s/\`/\\\\\`/g; 1s/^/'/; \$s/\$/'/" <<< ${{ github.event.issue.title }})" >> $GITHUB_ENV - name: Get URL, title, & number of new deploy blocker (pull request) if: ${{ github.event_name == 'pull_request' }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 947cfba1851d..c55a3f68ca0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ This is the most common scenario for contributors. The Expensify team posts new #### Proposing a job that Expensify hasn’t posted -In this scenario, it’s possible that you found a bug or enhancement that we haven’t posted to the [Upwork job list](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2) or [Github repository](https://github.com/Expensify/Expensify.cash/issues?q=is%3Aissue). This is an opportunity to propose a job, and (optionally) a solution. If it's a valid job proposal, we will compensate you for the solution and give an additional bonus of $150 for proactively proposing the job. In this case, please take the following steps: +In this scenario, it’s possible that you found a bug or enhancement that we haven’t posted to the [Upwork job list](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2) or [Github repository](https://github.com/Expensify/Expensify.cash/issues?q=is%3Aissue). This is an opportunity to propose a job, and (optionally) a solution. If it's a valid job proposal, we will compensate you for the solution and give an additional bonus of $250 for proactively proposing the job. In this case, please take the following steps: 1. Check to ensure an issue does not already exist in the New Expensify Issue list or Upwork job list. Please use your best judgement to search for similar titles and issue descriptions. 2. If your bug or enhancement matches an existing issue, please feel free to comment on that GitHub issue with your findings if you think it’ll help solve a problem. diff --git a/README.md b/README.md index 104687db9a5e..c3ee08137fc0 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ This application is built with the following principles. 1. UI inputs push data to the server (React component -> Action -> XHR to server). 1. Go to 1 1. **Offline first** - - All data that is brought into the app and is necessary to display the app when offline should be stored on disk in persistent storage (eg. localStorage on browser platforms). [AsyncStorage](https://react-native-community.github.io/async-storage/) is a cross-platform abstraction layer that is used to access persistent storage. + - All data that is brought into the app and is necessary to display the app when offline should be stored on disk in persistent storage (eg. localStorage on browser platforms). [AsyncStorage](https://reactnative.dev/docs/asyncstorage) is a cross-platform abstraction layer that is used to access persistent storage. - All data that is displayed, comes from persistent storage. 1. **UI Binds to data on disk** - Onyx is a Pub/Sub library to connect the application to the data stored on disk. diff --git a/__mocks__/@react-native-firebase/perf.js b/__mocks__/@react-native-firebase/perf.js new file mode 100644 index 000000000000..2d1ec238274a --- /dev/null +++ b/__mocks__/@react-native-firebase/perf.js @@ -0,0 +1 @@ +export default () => {}; diff --git a/android/app/build.gradle b/android/app/build.gradle index a3a6cd83df46..004a45bedeee 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -150,8 +150,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001008002 - versionName "1.0.80-2" + versionCode 1001008003 + versionName "1.0.80-3" } splits { abi { diff --git a/android/build.gradle b/android/build.gradle index 66a9c1a64a72..0f9aece0cfa0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -28,8 +28,6 @@ buildscript { allprojects { repositories { - maven { url "https://dl.bintray.com/onfido/maven" } - mavenLocal() maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm diff --git a/ios/ExpensifyCash.xcodeproj/project.pbxproj b/ios/ExpensifyCash.xcodeproj/project.pbxproj index eb03211ebd49..18e0eafb0d1f 100644 --- a/ios/ExpensifyCash.xcodeproj/project.pbxproj +++ b/ios/ExpensifyCash.xcodeproj/project.pbxproj @@ -527,12 +527,14 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ExpensifyCash/Pods-ExpensifyCash-frameworks.sh", + "${PODS_ROOT}/hermes-engine/destroot/Library/Frameworks/iphoneos/hermes.framework", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Onfido/Onfido.framework/Onfido", "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/LinkKit/LinkKit.framework/LinkKit", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Onfido.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", @@ -712,12 +714,14 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ExpensifyCash-ExpensifyCashTests/Pods-ExpensifyCash-ExpensifyCashTests-frameworks.sh", + "${PODS_ROOT}/hermes-engine/destroot/Library/Frameworks/iphoneos/hermes.framework", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Onfido/Onfido.framework/Onfido", "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/LinkKit/LinkKit.framework/LinkKit", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Onfido.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", @@ -926,6 +930,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386"; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -986,6 +991,7 @@ COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386"; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/ios/ExpensifyCash/Info.plist b/ios/ExpensifyCash/Info.plist index c69435b11314..8fa71fe6b76f 100644 --- a/ios/ExpensifyCash/Info.plist +++ b/ios/ExpensifyCash/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.0.80.2 + 1.0.80.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/ExpensifyCashTests/Info.plist b/ios/ExpensifyCashTests/Info.plist index ced27dd4ac02..978608834649 100644 --- a/ios/ExpensifyCashTests/Info.plist +++ b/ios/ExpensifyCashTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.0.80.2 + 1.0.80.3 diff --git a/ios/Podfile b/ios/Podfile index 78bd8b4ce645..f0fff9d30d96 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -14,7 +14,10 @@ target 'ExpensifyCash' do pod 'Permission-LocationWhenInUse', :path => "#{permissions_path}/LocationWhenInUse" config = use_native_modules! - use_react_native!(:path => config["reactNativePath"]) + use_react_native!( + :path => config["reactNativePath"], + :hermes_enabled => true + ) target 'ExpensifyCashTests' do inherit! :complete @@ -24,10 +27,11 @@ target 'ExpensifyCash' do # Enables Flipper. # # Note that if you have use_frameworks! enabled, Flipper will not work and - # you should disable these next few lines. - use_flipper!('Flipper' => '0.75.1', 'Flipper-Folly' => '2.5.3', 'Flipper-RSocket' => '1.3.1') + # you should disable the next line. + use_flipper!() + post_install do |installer| - flipper_post_install(installer) + react_native_post_install(installer) installer.pods_project.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3f4e4773ee98..3be3a461ff59 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -177,6 +177,7 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/UserDefaults (7.5.0): - GoogleUtilities/Logger + - hermes-engine (0.7.2) - libevent (2.1.12) - nanopb (2.30908.0): - nanopb/decode (= 2.30908.0) @@ -194,7 +195,7 @@ PODS: - RNPermissions - Permission-LocationWhenInUse (3.0.1): - RNPermissions - - Plaid (2.1.2) + - Plaid (2.1.3) - PromisesObjC (2.0.0) - Protobuf (3.17.0) - RCT-Folly (2020.01.13.00): @@ -206,6 +207,11 @@ PODS: - boost-for-react-native - DoubleConversion - glog + - RCT-Folly/Futures (2020.01.13.00): + - boost-for-react-native + - DoubleConversion + - glog + - libevent - RCTRequired (0.64.1) - RCTTypeSafety (0.64.1): - FBLazyVector (= 0.64.1) @@ -263,6 +269,16 @@ PODS: - React-jsinspector (= 0.64.1) - React-perflogger (= 0.64.1) - Yoga + - React-Core/Hermes (0.64.1): + - glog + - hermes-engine + - RCT-Folly (= 2020.01.13.00) + - RCT-Folly/Futures + - React-cxxreact (= 0.64.1) + - React-jsi (= 0.64.1) + - React-jsiexecutor (= 0.64.1) + - React-perflogger (= 0.64.1) + - Yoga - React-Core/RCTActionSheetHeaders (0.64.1): - glog - RCT-Folly (= 2020.01.13.00) @@ -573,26 +589,28 @@ DEPENDENCIES: - EXHaptics (from `../node_modules/expo-haptics/ios`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) - - Flipper (= 0.75.1) + - Flipper (~> 0.75.1) - Flipper-DoubleConversion (= 1.1.7) - - Flipper-Folly (= 2.5.3) + - Flipper-Folly (~> 2.5.3) - Flipper-Glog (= 0.3.6) - Flipper-PeerTalk (~> 0.0.4) - - Flipper-RSocket (= 1.3.1) - - FlipperKit (= 0.75.1) - - FlipperKit/Core (= 0.75.1) - - FlipperKit/CppBridge (= 0.75.1) - - FlipperKit/FBCxxFollyDynamicConvert (= 0.75.1) - - FlipperKit/FBDefines (= 0.75.1) - - FlipperKit/FKPortForwarding (= 0.75.1) - - FlipperKit/FlipperKitHighlightOverlay (= 0.75.1) - - FlipperKit/FlipperKitLayoutPlugin (= 0.75.1) - - FlipperKit/FlipperKitLayoutTextSearchable (= 0.75.1) - - FlipperKit/FlipperKitNetworkPlugin (= 0.75.1) - - FlipperKit/FlipperKitReactPlugin (= 0.75.1) - - FlipperKit/FlipperKitUserDefaultsPlugin (= 0.75.1) - - FlipperKit/SKIOSNetworkPlugin (= 0.75.1) + - Flipper-RSocket (~> 1.3) + - FlipperKit (~> 0.75.1) + - FlipperKit/Core (~> 0.75.1) + - FlipperKit/CppBridge (~> 0.75.1) + - FlipperKit/FBCxxFollyDynamicConvert (~> 0.75.1) + - FlipperKit/FBDefines (~> 0.75.1) + - FlipperKit/FKPortForwarding (~> 0.75.1) + - FlipperKit/FlipperKitHighlightOverlay (~> 0.75.1) + - FlipperKit/FlipperKitLayoutPlugin (~> 0.75.1) + - FlipperKit/FlipperKitLayoutTextSearchable (~> 0.75.1) + - FlipperKit/FlipperKitNetworkPlugin (~> 0.75.1) + - FlipperKit/FlipperKitReactPlugin (~> 0.75.1) + - FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.75.1) + - FlipperKit/SKIOSNetworkPlugin (~> 0.75.1) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - hermes-engine (~> 0.7.2) + - libevent (~> 2.1.12) - "onfido-react-native-sdk (from `../node_modules/@onfido/react-native-sdk`)" - Permission-LocationAccuracy (from `../node_modules/react-native-permissions/ios/LocationAccuracy`) - Permission-LocationAlways (from `../node_modules/react-native-permissions/ios/LocationAlways`) @@ -605,6 +623,7 @@ DEPENDENCIES: - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - React-Core (from `../node_modules/react-native/`) - React-Core/DevSupport (from `../node_modules/react-native/`) + - React-Core/Hermes (from `../node_modules/react-native/`) - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) @@ -687,6 +706,7 @@ SPEC REPOS: - GoogleAppMeasurement - GoogleDataTransport - GoogleUtilities + - hermes-engine - libevent - nanopb - Onfido @@ -868,6 +888,7 @@ SPEC CHECKSUMS: GoogleAppMeasurement: 6b6a08fd9c71f4dbc89e0e812acca81d797aa342 GoogleDataTransport: 85fd18ff3019bb85d3f2c551d04c481dedf71fc9 GoogleUtilities: eea970f4a389963963bffe8d8fabe43540678b9c + hermes-engine: 7d97ba46a1e29bacf3e3c61ecb2804a5ddd02d4f libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 Onfido: 116a268e4cb8b767c15285e8071c2e8304673cdf @@ -876,7 +897,7 @@ SPEC CHECKSUMS: Permission-LocationAccuracy: e8adff9ede1b23b43b7054a4500113d515fc87a8 Permission-LocationAlways: 7f7f373d086af7a81b2f4f20d65d29266ca2043b Permission-LocationWhenInUse: 3ae82a9feb5da4e94e386dba17c7dd3531af9feb - Plaid: c02276ccc630a726a9ed790bf923d29839ff4017 + Plaid: f55c6acdc249245c6778a4045757eb4e839cca61 PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 Protobuf: 7327d4444215b5f18e560a97f879ff5503c4581c RCT-Folly: ec7a233ccc97cc556cf7237f0db1ff65b986f27c @@ -943,6 +964,6 @@ SPEC CHECKSUMS: Yoga: a7de31c64fe738607e7a3803e3f591a4b1df7393 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 2b0558e25f01204512de38c2f656a63b5d86d19c +PODFILE CHECKSUM: 2092930e33e7aa4d68cdf783d6174465bd3ce380 COCOAPODS: 1.10.1 diff --git a/package-lock.json b/package-lock.json index b955b2077ef8..fda49353ba70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "expensify.cash", - "version": "1.0.80-2", + "version": "1.0.80-3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 868ff2d6e21b..b525b4fd9693 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "expensify.cash", - "version": "1.0.80-2", + "version": "1.0.80-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "Expensify.cash is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.js b/src/CONST.js index 1b0a49e36d18..303f4f04611b 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -84,6 +84,7 @@ const CONST = { PAY_WITH_EXPENSIFY: 'payWithExpensify', FREE_PLAN: 'freePlan', DEFAULT_ROOMS: 'defaultRooms', + INTERNATIONALIZATION: 'internationalization', }, BUTTON_STATES: { DEFAULT: 'default', @@ -193,6 +194,7 @@ const CONST = { HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', HOMEPAGE_REPORTS_LOADED: 'homepage_reports_loaded', SWITCH_REPORT: 'switch_report', + SIDEBAR_LOADED: 'sidebar_loaded', COLD: 'cold', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, }, diff --git a/src/Expensify.js b/src/Expensify.js index d933be7ccc1d..53cef50574e6 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import {View, AppState} from 'react-native'; import Onyx, {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; import BootSplash from './libs/BootSplash'; import listenToStorageEvents from './libs/listenToStorageEvents'; @@ -21,6 +22,7 @@ import {growlRef} from './libs/Growl'; import Navigation from './libs/Navigation/Navigation'; import ROUTES from './ROUTES'; import StartupTimer from './libs/StartupTimer'; +import {setRedirectToWorkspaceNewAfterSignIn} from './libs/actions/Session'; // Initialize the store when the app loads for the first time Onyx.init({ @@ -35,6 +37,7 @@ Onyx.init({ [ONYXKEYS.IOU]: { loading: false, error: false, creatingIOUTransaction: false, isRetrievingCurrency: false, }, + [ONYXKEYS.IS_SIDEBAR_LOADED]: false, }, registerStorageEventListener: (onStorageEvent) => { listenToStorageEvents(onStorageEvent); @@ -69,6 +72,12 @@ const propTypes = { /** Whether the initial data needed to render the app is ready */ initialReportDataLoaded: PropTypes.bool, + + /** Tells us if the sidebar has rendered */ + isSidebarLoaded: PropTypes.bool, + + /** List of betas */ + betas: PropTypes.arrayOf(PropTypes.string), }; const defaultProps = { @@ -79,6 +88,8 @@ const defaultProps = { }, updateAvailable: false, initialReportDataLoaded: false, + isSidebarLoaded: false, + betas: [], }; class Expensify extends PureComponent { @@ -129,12 +140,16 @@ class Expensify extends PureComponent { const previousAuthToken = lodashGet(prevProps, 'session.authToken', null); if (this.getAuthToken() && !previousAuthToken) { BootSplash.show({fade: true}); - if (lodashGet(this.props, 'session.redirectToWorkspaceNewAfterSignIn', false)) { - Navigation.navigate(ROUTES.WORKSPACE_NEW); - } } - if (this.getAuthToken() && this.props.initialReportDataLoaded) { + if (this.getAuthToken() + && !_.isEmpty(this.props.betas) + && lodashGet(this.props, 'session.redirectToWorkspaceNewAfterSignIn', false)) { + setRedirectToWorkspaceNewAfterSignIn(false); + Navigation.navigate(ROUTES.WORKSPACE_NEW); + } + + if (this.getAuthToken() && this.props.initialReportDataLoaded && this.props.isSidebarLoaded) { BootSplash.getVisibilityStatus() .then((value) => { if (value !== 'visible') { @@ -188,6 +203,9 @@ export default withOnyx({ session: { key: ONYXKEYS.SESSION, }, + betas: { + key: ONYXKEYS.BETAS, + }, updateAvailable: { key: ONYXKEYS.UPDATE_AVAILABLE, initWithStoredValues: false, @@ -195,4 +213,7 @@ export default withOnyx({ initialReportDataLoaded: { key: ONYXKEYS.INITIAL_REPORT_DATA_LOADED, }, + isSidebarLoaded: { + key: ONYXKEYS.IS_SIDEBAR_LOADED, + }, })(Expensify); diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index ded87710b8e1..71b269290712 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -16,6 +16,9 @@ export default { // Boolean flag set whenever we are waiting for the reconnection callbacks to finish. IS_LOADING_AFTER_RECONNECT: 'isLoadingAfterReconnect', + // Boolean flag set whenever the sidebar has loaded + IS_SIDEBAR_LOADED: 'isSidebarLoaded', + NETWORK_REQUEST_QUEUE: 'networkRequestQueue', // What the active route is for our navigator. Global route that determines what views to display. diff --git a/src/components/CommunicationsLink.js b/src/components/CommunicationsLink.js index c60ced905fc8..bbaa924ebc08 100644 --- a/src/components/CommunicationsLink.js +++ b/src/components/CommunicationsLink.js @@ -44,16 +44,15 @@ const CommunicationsLink = props => ( > {props.children} - ) - : props.children} - {!props.isSmallScreenWidth - && ( + ) : ( + {props.children} diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js new file mode 100644 index 000000000000..88a25971d7a3 --- /dev/null +++ b/src/components/LocalePicker.js @@ -0,0 +1,88 @@ +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import styles from '../styles/styles'; +import Picker from './Picker'; +import Text from './Text'; +import compose from '../libs/compose'; +import {setLocale} from '../libs/actions/App'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import ONYXKEYS from '../ONYXKEYS'; +import CONST from '../CONST'; +import Permissions from '../libs/Permissions'; +import {translate} from '../libs/translate'; + +const propTypes = { + /** Indicates which locale the user currently has selected */ + preferredLocale: PropTypes.string, + + /** Indicates size of a picker component and whether to render the label or not */ + size: PropTypes.oneOf(['normal', 'small']), + + /** Beta features list */ + betas: PropTypes.arrayOf(PropTypes.string), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + preferredLocale: CONST.DEFAULT_LOCALE, + size: 'normal', + betas: [], +}; + +const localesToLanguages = { + default: { + value: 'en', + label: translate('en', 'preferencesPage.languages.english'), + }, + es: { + value: 'es', + label: translate('es', 'preferencesPage.languages.spanish'), + }, +}; + +const LocalePicker = ({ + // eslint-disable-next-line no-shadow + preferredLocale, translate, betas, size, +}) => { + if (!Permissions.canUseInternationalization(betas)) { + return null; + } + + return ( + <> + {size === 'normal' && ( + + {translate('preferencesPage.language')} + + )} + { + if (locale !== preferredLocale) { + setLocale(locale); + } + }} + items={Object.values(localesToLanguages)} + size={size} + value={preferredLocale} + /> + + ); +}; + +LocalePicker.defaultProps = defaultProps; +LocalePicker.propTypes = propTypes; +LocalePicker.displayName = 'LocalePicker'; + +export default compose( + withLocalize, + withOnyx({ + preferredLocale: { + key: ONYXKEYS.PREFERRED_LOCALE, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + }), +)(LocalePicker); diff --git a/src/components/OptionsList.js b/src/components/OptionsList.js index dbb6e968da42..cfb25b7e5e19 100644 --- a/src/components/OptionsList.js +++ b/src/components/OptionsList.js @@ -78,6 +78,9 @@ const propTypes = { /** Whether to disable the interactivity of the list's option row(s) */ disableRowInteractivity: PropTypes.bool, + + /** Callback to execute when the SectionList lays out */ + onLayout: PropTypes.func, }; const defaultProps = { @@ -99,6 +102,7 @@ const defaultProps = { showTitleTooltip: false, optionMode: undefined, disableRowInteractivity: false, + onLayout: undefined, }; class OptionsList extends Component { @@ -109,6 +113,8 @@ class OptionsList extends Component { this.renderSectionHeader = this.renderSectionHeader.bind(this); this.extractKey = this.extractKey.bind(this); this.onScrollToIndexFailed = this.onScrollToIndexFailed.bind(this); + this.viewabilityConfig = {viewAreaCoveragePercentThreshold: 95}; + this.didLayout = false; } shouldComponentUpdate(nextProps) { @@ -228,6 +234,18 @@ class OptionsList extends Component { renderItem={this.renderItem} renderSectionHeader={this.renderSectionHeader} extraData={this.props.focusedIndex} + initialNumToRender={5} + maxToRenderPerBatch={5} + windowSize={5} + viewabilityConfig={this.viewabilityConfig} + onViewableItemsChanged={() => { + if (this.didLayout) { + return; + } + + this.didLayout = true; + this.props.onLayout(); + }} /> ); diff --git a/src/components/Picker/PickerPropTypes.js b/src/components/Picker/PickerPropTypes.js index 633966a2bef4..3a6077b5886c 100644 --- a/src/components/Picker/PickerPropTypes.js +++ b/src/components/Picker/PickerPropTypes.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Icon from '../Icon'; +import styles from '../../styles/styles'; import {DownArrow} from '../Icon/Expensicons'; const propTypes = { @@ -36,13 +37,32 @@ const propTypes = { /** An icon to display with the picker */ icon: PropTypes.func, + + /** Size of a picker component */ + size: PropTypes.oneOf(['normal', 'small']), }; const defaultProps = { useDisabledStyles: false, disabled: false, placeholder: {}, value: null, - icon: () => , + icon: size => ( + <> + {size === 'small' + ? ( + + ) + : ( + + )} + + ), }; export { diff --git a/src/components/Picker/index.js b/src/components/Picker/index.js index 427d38184ef3..b963c81601b8 100644 --- a/src/components/Picker/index.js +++ b/src/components/Picker/index.js @@ -13,19 +13,29 @@ const Picker = ({ value, icon, disabled, -}) => ( - -); + size, +}) => { + let pickerStyles; + if (size === 'small') { + pickerStyles = styles.pickerSmall; + } else { + pickerStyles = useDisabledStyles ? pickerDisabledStyles : styles.picker; + } + + return ( + icon(size)} + disabled={disabled} + fixAndroidTouchableBug + /> + ); +}; Picker.propTypes = pickerPropTypes.propTypes; Picker.defaultProps = pickerPropTypes.defaultProps; diff --git a/src/components/PressableWithSecondaryInteraction/index.js b/src/components/PressableWithSecondaryInteraction/index.js index cc9645d98180..bf6c981f4d9a 100644 --- a/src/components/PressableWithSecondaryInteraction/index.js +++ b/src/components/PressableWithSecondaryInteraction/index.js @@ -30,7 +30,9 @@ class PressableWithSecondaryInteraction extends Component { */ executeSecondaryInteractionOnContextMenu(e) { const selection = window.getSelection().toString(); - e.preventDefault(); + if (this.props.preventDefaultContentMenu) { + e.preventDefault(); + } this.props.onSecondaryInteraction(e, selection); } diff --git a/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js b/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js index 372eabd424cd..beb257922b0e 100644 --- a/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js +++ b/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js @@ -15,12 +15,16 @@ const propTypes = { /** The ref to the search input (may be null on small screen widths) */ forwardedRef: PropTypes.func, + + /** Prevent the default ContextMenu on web/Desktop */ + preventDefaultContentMenu: PropTypes.bool, }; const defaultProps = { forwardedRef: () => {}, onPressIn: () => {}, onPressOut: () => {}, + preventDefaultContentMenu: true, }; export {propTypes, defaultProps}; diff --git a/src/components/ScreenWrapper.js b/src/components/ScreenWrapper.js index 094b8b597fdd..f3ac8a258d06 100644 --- a/src/components/ScreenWrapper.js +++ b/src/components/ScreenWrapper.js @@ -8,6 +8,7 @@ import styles, {getSafeAreaPadding} from '../styles/styles'; import HeaderGap from './HeaderGap'; import KeyboardShortcut from '../libs/KeyboardShortcut'; import onScreenTransitionEnd from '../libs/onScreenTransitionEnd'; +import Navigation from '../libs/Navigation/Navigation'; const propTypes = { /** Array of additional styles to add */ @@ -32,9 +33,6 @@ const propTypes = { navigation: PropTypes.shape({ // Method to attach listener to Navigation state. addListener: PropTypes.func.isRequired, - - // Returns to the previous navigation state e.g. if this is inside a Modal we will dismiss it - goBack: PropTypes.func, }), }; @@ -45,7 +43,6 @@ const defaultProps = { onTransitionEnd: () => {}, navigation: { addListener: () => {}, - goBack: () => {}, }, }; @@ -59,7 +56,7 @@ class ScreenWrapper extends React.Component { componentDidMount() { this.unsubscribeEscapeKey = KeyboardShortcut.subscribe('Escape', () => { - this.props.navigation.goBack(); + Navigation.dismissModal(); }, [], true); this.unsubscribeTransitionEnd = onScreenTransitionEnd(this.props.navigation, () => { diff --git a/src/components/TextInputAutoWidth.js b/src/components/TextInputAutoWidth.js index 18a04bb9e335..9dbc83537cd6 100644 --- a/src/components/TextInputAutoWidth.js +++ b/src/components/TextInputAutoWidth.js @@ -42,15 +42,13 @@ class TextInputAutoWidth extends React.Component { render() { const propsWithoutStyles = _.omit(this.props, ['inputStyle', 'textStyle']); return ( - <> - - - + + {/* Text input component doesn't support auto grow by default. We're using a hidden text input to achieve that. @@ -63,7 +61,7 @@ class TextInputAutoWidth extends React.Component { > {this.props.value || this.props.placeholder} - + ); } } diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js index 5ace8e0e8d9a..44f386ef3d64 100755 --- a/src/components/TextInputFocusable/index.js +++ b/src/components/TextInputFocusable/index.js @@ -118,6 +118,7 @@ class TextInputFocusable extends React.Component { this.dragNDropListener = this.dragNDropListener.bind(this); this.handlePaste = this.handlePaste.bind(this); this.handlePastedHTML = this.handlePastedHTML.bind(this); + this.handleWheel = this.handleWheel.bind(this); } componentDidMount() { @@ -141,6 +142,7 @@ class TextInputFocusable extends React.Component { document.addEventListener('dragleave', this.dragNDropListener); document.addEventListener('drop', this.dragNDropListener); this.textInput.addEventListener('paste', this.handlePaste); + this.textInput.addEventListener('wheel', this.handleWheel); } } @@ -168,6 +170,7 @@ class TextInputFocusable extends React.Component { document.removeEventListener('dragleave', this.dragNDropListener); document.removeEventListener('drop', this.dragNDropListener); this.textInput.removeEventListener('paste', this.handlePaste); + this.textInput.removeEventListener('wheel', this.handleWheel); } } @@ -304,6 +307,18 @@ class TextInputFocusable extends React.Component { } } + /** + * Manually scrolls the text input, then prevents the event from being passed up to the parent. + * @param {Object} event native Event + */ + handleWheel(event) { + if (event.target === document.activeElement) { + this.textInput.scrollTop += event.deltaY; + event.preventDefault(); + event.stopPropagation(); + } + } + /** * Check the current scrollHeight of the textarea (minus any padding) and * divide by line height to get the total number of rows for the textarea. diff --git a/src/components/withLocalize.js b/src/components/withLocalize.js index 78eca06f6077..435a2df71629 100755 --- a/src/components/withLocalize.js +++ b/src/components/withLocalize.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import getComponentDisplayName from '../libs/getComponentDisplayName'; -import compose from '../libs/compose'; import ONYXKEYS from '../ONYXKEYS'; import {translate} from '../libs/translate'; import DateUtils from '../libs/DateUtils'; @@ -30,59 +29,123 @@ const withLocalizePropTypes = { fromLocalPhone: PropTypes.func.isRequired, }; -function withLocalizeHOC(WrappedComponent) { - const WithLocalize = (props) => { - const translations = { - translate: (phrase, variables) => translate(props.preferredLocale, phrase, variables), - numberFormat: (number, options) => numberFormat(props.preferredLocale, number, options), - timestampToRelative: timestamp => DateUtils.timestampToRelative(props.preferredLocale, timestamp), - timestampToDateTime: (timestamp, includeTimezone) => DateUtils.timestampToDateTime( - props.preferredLocale, - timestamp, - includeTimezone, - ), - toLocalPhone: number => toLocalPhone(props.preferredLocale, number), - fromLocalPhone: number => fromLocalPhone(props.preferredLocale, number), - }; - return ( - - ); - }; - WithLocalize.displayName = `WithLocalize(${getComponentDisplayName(WrappedComponent)})`; - WithLocalize.propTypes = { +export default (WrappedComponent) => { + const propTypes = { + /** The user's preferred locale e.g. 'en', 'es-ES' */ preferredLocale: PropTypes.string, + + /** Passed ref from whatever component is wrapped in the HOC */ forwardedRef: PropTypes.oneOfType([ PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)}), ]), }; - WithLocalize.defaultProps = { + + const defaultProps = { preferredLocale: CONST.DEFAULT_LOCALE, forwardedRef: undefined, }; - return React.forwardRef((props, ref) => ( + + class WithLocalize extends React.Component { + constructor(props) { + super(props); + + this.translate = this.translate.bind(this); + this.numberFormat = this.numberFormat.bind(this); + this.timestampToRelative = this.timestampToRelative.bind(this); + this.timestampToDateTime = this.timestampToDateTime.bind(this); + this.fromLocalPhone = this.fromLocalPhone.bind(this); + this.toLocalPhone = this.toLocalPhone.bind(this); + } + + /** + * @param {String} phrase + * @param {Object} [variables] + * @returns {String} + */ + translate(phrase, variables) { + return translate(this.props.preferredLocale, phrase, variables); + } + + /** + * @param {Number} number + * @param {Intl.NumberFormatOptions} options + * @returns {String} + */ + numberFormat(number, options) { + return numberFormat(this.props.preferredLocale, number, options); + } + + /** + * @param {Number} timestamp + * @returns {String} + */ + timestampToRelative(timestamp) { + return DateUtils.timestampToRelative(this.props.preferredLocale, timestamp); + } + + /** + * @param {Number} timestamp + * @param {Boolean} [includeTimezone] + * @returns {String} + */ + timestampToDateTime(timestamp, includeTimezone) { + return DateUtils.timestampToDateTime( + this.props.preferredLocale, + timestamp, + includeTimezone, + ); + } + + /** + * @param {Number} number + * @returns {String} + */ + toLocalPhone(number) { + return toLocalPhone(this.props.preferredLocale, number); + } + + /** + * @param {Number} number + * @returns {String} + */ + fromLocalPhone(number) { + return fromLocalPhone(this.props.preferredLocale, number); + } + + render() { + return ( + + ); + } + } + + WithLocalize.propTypes = propTypes; + WithLocalize.defaultProps = defaultProps; + + const withForwardedRef = React.forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading )); -} -export default compose( - withOnyx({ + + withForwardedRef.displayName = `withLocalize(${getComponentDisplayName(WrappedComponent)})`; + + return withOnyx({ preferredLocale: { key: ONYXKEYS.NVP_PREFERRED_LOCALE, }, - }), - withLocalizeHOC, -); + })(withForwardedRef); +}; export { withLocalizePropTypes, diff --git a/src/libs/Firebase/index.js b/src/libs/Firebase/index.js new file mode 100644 index 000000000000..a8c7a34f869f --- /dev/null +++ b/src/libs/Firebase/index.js @@ -0,0 +1,6 @@ +/** Web does not use Firebase for performance tracing */ + +export default { + startTrace() {}, + stopTrace() {}, +}; diff --git a/src/libs/Firebase/index.native.js b/src/libs/Firebase/index.native.js new file mode 100644 index 000000000000..af627091a0ae --- /dev/null +++ b/src/libs/Firebase/index.native.js @@ -0,0 +1,56 @@ +/* eslint-disable no-unused-vars */ +import perf from '@react-native-firebase/perf'; +import {isDevelopment} from '../Environment/Environment'; +import Log from '../Log'; + +const traceMap = {}; + +/** + * @param {String} customEventName + */ +function startTrace(customEventName) { + const start = global.performance.now(); + if (isDevelopment()) { + return; + } + + if (traceMap[customEventName]) { + return; + } + + perf().startTrace(customEventName) + .then((trace) => { + traceMap[customEventName] = { + trace, + start, + }; + }); +} + +/** + * @param {String} customEventName + */ +function stopTrace(customEventName) { + const stop = global.performance.now(); + + if (isDevelopment()) { + return; + } + + const {trace, start} = traceMap[customEventName]; + if (!trace) { + return; + } + + trace.stop(); + + // Uncomment to inspect logs on release builds + // Log.info(`sidebar_loaded: ${stop - start} ms`, true); + + delete traceMap[customEventName]; +} + +export default { + startTrace, + stopTrace, +}; diff --git a/src/libs/KeyboardShortcut/index.js b/src/libs/KeyboardShortcut/index.js index 3cc302801e99..1f82b4cab687 100644 --- a/src/libs/KeyboardShortcut/index.js +++ b/src/libs/KeyboardShortcut/index.js @@ -5,6 +5,7 @@ const events = {}; /** * Checks if an event for that key is configured and if so, runs it. * @param {Event} event + * @private */ function bindHandlerToKeyupEvent(event) { if (events[event.keyCode] === undefined) { @@ -12,9 +13,10 @@ function bindHandlerToKeyupEvent(event) { } const eventCallbacks = events[event.keyCode]; + const reversedEventCallbacks = [...eventCallbacks].reverse(); // Loop over all the callbacks - eventCallbacks.forEach((callback) => { + _.every(reversedEventCallbacks, (callback) => { const pressedModifiers = _.all(callback.modifiers, (modifier) => { if (modifier === 'shift' && !event.shiftKey) { return false; @@ -50,7 +52,7 @@ function bindHandlerToKeyupEvent(event) { return false; }); if (!pressedModifiers || pressedExtraModifiers) { - return; + return true; } // If configured to do so, prevent input text control to trigger this event @@ -59,13 +61,16 @@ function bindHandlerToKeyupEvent(event) { || event.target.nodeName === 'TEXTAREA' || event.target.contentEditable === 'true' )) { - return; + return true; } if (_.isFunction(callback.callback)) { callback.callback(event); } event.preventDefault(); + + // Short circuit the loop because the event is triggered + return false; }); } @@ -73,6 +78,52 @@ function bindHandlerToKeyupEvent(event) { document.removeEventListener('keydown', bindHandlerToKeyupEvent, {capture: true}); document.addEventListener('keydown', bindHandlerToKeyupEvent, {capture: true}); +/** + * Returns keyCode for a given key + * @param {String} key The key to watch, i.e. 'K' or 'Escape' + * @returns {Number} The key's keyCode, i.e. 75 or 27 + * @private + */ +function getKeyCode(key) { + // For keys that have longer names we must catch and return the correct key key.charCodeAt(0) would return the + // key code for 'E' (the letter at index 0 in the string) not 'Escape' + switch (key) { + case 'Enter': + return 13; + case 'Escape': + return 27; + default: + return key.charCodeAt(0); + } +} + +/** + * Unsubscribes to a keyboard event. + * @param {Number} key The key to stop watching + * @private + */ +function unsubscribe(key) { + const keyCode = getKeyCode(key); + events[keyCode].pop(); +} + +/** + * Subscribes to a keyboard event. + * @param {String} key The key to watch, i.e. 'K' or 'Escape' + * @param {Function} callback The callback to call + * @param {String|Array} modifiers Can either be shift or control + * @param {Boolean} captureOnInputs Should we capture the event on inputs too? + * @returns {Function} clean up method + */ +function subscribe(key, callback, modifiers = 'shift', captureOnInputs = false) { + const keyCode = getKeyCode(key); + if (events[keyCode] === undefined) { + events[keyCode] = []; + } + events[keyCode].push({callback, modifiers: _.isArray(modifiers) ? modifiers : [modifiers], captureOnInputs}); + return () => unsubscribe(key); +} + /** * Module storing the different keyboard shortcut * @@ -85,49 +136,7 @@ document.addEventListener('keydown', bindHandlerToKeyupEvent, {capture: true}); * The "subClass" is used by pages to bind /unbind with no worries */ const KeyboardShortcut = { - /** - * Returns keyCode for a given key - * @param {String} key The key to watch, i.e. 'K' or 'Escape' - * @returns {Number} The key's keyCode, i.e. 75 or 27 - */ - getKeyCode(key) { - // For keys that have longer names we must catch and return the correct key key.charCodeAt(0) would return the - // key code for 'E' (the letter at index 0 in the string) not 'Escape' - switch (key) { - case 'Enter': - return 13; - case 'Escape': - return 27; - default: - return key.charCodeAt(0); - } - }, - - /** - * Subscribes to a keyboard event. - * @param {String} key The key to watch, i.e. 'K' or 'Escape' - * @param {Function} callback The callback to call - * @param {String|Array} modifiers Can either be shift or control - * @param {Boolean} captureOnInputs Should we capture the event on inputs too? - * @returns {Function} clean up method - */ - subscribe(key, callback, modifiers = 'shift', captureOnInputs = false) { - const keyCode = this.getKeyCode(key); - if (events[keyCode] === undefined) { - events[keyCode] = []; - } - events[keyCode].push({callback, modifiers: _.isArray(modifiers) ? modifiers : [modifiers], captureOnInputs}); - return () => this.unsubscribe(key); - }, - - /** - * Unsubscribes to a keyboard event. - * @param {Number} key The key to stop watching - */ - unsubscribe(key) { - const keyCode = this.getKeyCode(key); - delete events[keyCode]; - }, + subscribe, }; export default KeyboardShortcut; diff --git a/src/libs/LocalePhoneNumber.js b/src/libs/LocalePhoneNumber.js index 4d51e711d705..3d47e7c6a5a9 100644 --- a/src/libs/LocalePhoneNumber.js +++ b/src/libs/LocalePhoneNumber.js @@ -10,7 +10,7 @@ import translations from '../languages/translations'; * * @param {String} locale eg 'en', 'es-ES' * @param {String} number - * @returns {string} + * @returns {String} */ function toLocalPhone(locale, number) { const numString = lodashTrim(number); @@ -31,7 +31,7 @@ function toLocalPhone(locale, number) { * * @param {String} locale eg 'en', 'es-ES' * @param {String} number - * @returns {string} + * @returns {String} */ function fromLocalPhone(locale, number) { const numString = lodashTrim(number); diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 5ba802e792bf..028ff82ea443 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -178,24 +178,23 @@ class AuthScreens extends React.Component { Timing.end(CONST.TIMING.HOMEPAGE_INITIAL_RENDER); + let searchShortcutModifiers = ['control']; + let groupShortcutModifiers = ['control', 'shift']; + + if (getOperatingSystem() === CONST.OS.MAC_OS) { + searchShortcutModifiers = ['meta']; + groupShortcutModifiers = ['meta', 'shift']; + } + // Listen for the key K being pressed so that focus can be given to // the chat switcher, or new group chat // based on the key modifiers pressed and the operating system - if (getOperatingSystem() === CONST.OS.MAC_OS) { - KeyboardShortcut.subscribe('K', () => { - Navigation.navigate(ROUTES.SEARCH); - }, ['meta'], true); - KeyboardShortcut.subscribe('K', () => { - Navigation.navigate(ROUTES.NEW_GROUP); - }, ['meta', 'shift'], true); - } else { - KeyboardShortcut.subscribe('K', () => { - Navigation.navigate(ROUTES.SEARCH); - }, ['control'], true); - KeyboardShortcut.subscribe('K', () => { - Navigation.navigate(ROUTES.NEW_GROUP); - }, ['control', 'shift'], true); - } + this.unsubscribeSearchShortcut = KeyboardShortcut.subscribe('K', () => { + Navigation.navigate(ROUTES.SEARCH); + }, searchShortcutModifiers, true); + this.unsubscribeGroupShortcut = KeyboardShortcut.subscribe('K', () => { + Navigation.navigate(ROUTES.NEW_GROUP); + }, groupShortcutModifiers, true); } shouldComponentUpdate(nextProps) { @@ -215,7 +214,12 @@ class AuthScreens extends React.Component { } componentWillUnmount() { - KeyboardShortcut.unsubscribe('K'); + if (this.unsubscribeSearchShortcut) { + this.unsubscribeSearchShortcut(); + } + if (this.unsubscribeGroupShortcut) { + this.unsubscribeGroupShortcut(); + } NetworkConnection.stopListeningForReconnect(); clearInterval(this.interval); this.interval = null; diff --git a/src/libs/Permissions.js b/src/libs/Permissions.js index 7063273b245c..1ca31ce88712 100644 --- a/src/libs/Permissions.js +++ b/src/libs/Permissions.js @@ -51,10 +51,19 @@ function canUseDefaultRooms(betas) { return _.contains(betas, CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas); } +/** + * @param {Array} betas + * @returns {Boolean} + */ +function canUseInternationalization(betas) { + return _.contains(betas, CONST.BETAS.INTERNATIONALIZATION) || canUseAllBetas(betas); +} + export default { canUseChronos, canUseIOU, canUsePayWithExpensify, canUseFreePlan, canUseDefaultRooms, + canUseInternationalization, }; diff --git a/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js b/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js index aca9134a16ab..75f6d3f31d08 100644 --- a/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js +++ b/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js @@ -10,7 +10,7 @@ import CONFIG from '../../../CONFIG'; */ function updateUnread(totalCount) { const hasUnread = totalCount !== 0; - document.title = hasUnread ? `(NEW!) ${CONFIG.SITE_TITLE}` : CONFIG.SITE_TITLE; + document.title = hasUnread ? `(${totalCount}) ${CONFIG.SITE_TITLE}` : CONFIG.SITE_TITLE; document.getElementById('favicon').href = hasUnread ? CONFIG.FAVICON.UNREAD : CONFIG.FAVICON.DEFAULT; } diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index 9635948348cd..e3b339fdc7ab 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -1,6 +1,16 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; +import Firebase from '../Firebase'; +import CONST from '../../CONST'; + +let isSidebarLoaded; + +Onyx.connect({ + key: ONYXKEYS.IS_SIDEBAR_LOADED, + callback: val => isSidebarLoaded = val, + initWithStoredValues: false, +}); /** * @param {String} url @@ -17,7 +27,17 @@ function setLocale(locale) { Onyx.merge(ONYXKEYS.NVP_PREFERRED_LOCALE, locale); } +function setSidebarLoaded() { + if (isSidebarLoaded) { + return; + } + + Onyx.set(ONYXKEYS.IS_SIDEBAR_LOADED, true); + Firebase.stopTrace(CONST.TIMING.SIDEBAR_LOADED); +} + export { setCurrentURL, setLocale, + setSidebarLoaded, }; diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index f6474b581cd9..5f995433e465 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -13,6 +13,8 @@ function getPaymentMethods() { return API.Get({ returnValueList: 'bankAccountList, cardList, userWallet, nameValuePairs', name: 'paypalMeAddress', + includeDeleted: false, + includeNotIssued: false, }) .then((response) => { Onyx.multiSet({ diff --git a/src/libs/actions/Session.js b/src/libs/actions/Session.js index 3540939041fd..94bf1ff064a1 100644 --- a/src/libs/actions/Session.js +++ b/src/libs/actions/Session.js @@ -299,6 +299,15 @@ function continueSessionFromECom(accountID, validateCode, twoFactorAuthCode) { }); } +/** + * Sets the redirectToWorkspaceNewAfterSignIn flag in the session variable + * + * @param {Boolean} shouldRedirect + */ +function setRedirectToWorkspaceNewAfterSignIn(shouldRedirect) { + Onyx.merge(ONYXKEYS.SESSION, {redirectToWorkspaceNewAfterSignIn: shouldRedirect}); +} + export { continueSessionFromECom, fetchAccountDetails, @@ -309,4 +318,5 @@ export { resendValidationLink, resetPassword, restartSignin, + setRedirectToWorkspaceNewAfterSignIn, }; diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 583753ec8d3e..a206b5f249ac 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -19,6 +19,12 @@ Onyx.connect({ }, }); +let currentPreferredLocale; +Onyx.connect({ + key: ONYXKEYS.PREFERRED_LOCALE, + callback: val => currentPreferredLocale = val, +}); + /** * Clears the Onyx store and redirects to the sign in page. * Normally this method would live in Session.js, but that would cause a circular dependency with Network.js. @@ -37,12 +43,16 @@ function redirectToSignIn(errorMessage) { } const activeClients = currentActiveClients; + const preferredLocale = currentPreferredLocale; // We must set the authToken to null so we can navigate to "signin" it's not possible to navigate to the route as // it only exists when the authToken is null. Onyx.set(ONYXKEYS.SESSION, {authToken: null}) .then(() => { Onyx.clear().then(() => { + if (preferredLocale) { + Onyx.set(ONYXKEYS.PREFERRED_LOCALE, preferredLocale); + } if (errorMessage) { Onyx.set(ONYXKEYS.SESSION, {error: errorMessage}); } diff --git a/src/libs/actions/Timing.js b/src/libs/actions/Timing.js index 4d1eae31a36f..a0cbae149360 100644 --- a/src/libs/actions/Timing.js +++ b/src/libs/actions/Timing.js @@ -1,5 +1,6 @@ import getPlatform from '../getPlatform'; import {Graphite_Timer} from '../API'; +import {isDevelopment} from '../Environment/Environment'; let timestampData = {}; @@ -26,14 +27,19 @@ function end(eventName, secondaryName = '') { : `expensify.cash.${eventName}`; console.debug(`Timing:${grafanaEventName}`, eventTime); + delete timestampData[eventName]; + + // eslint-disable-next-line no-undef + if (isDevelopment()) { + // Don't create traces on dev as this will mess up the accuracy of data in release builds of the app + return; + } Graphite_Timer({ name: grafanaEventName, value: eventTime, platform: `${getPlatform()}`, }); - - delete timestampData[eventName]; } } diff --git a/src/libs/numberFormat/index.android.js b/src/libs/numberFormat/index.native.js similarity index 100% rename from src/libs/numberFormat/index.android.js rename to src/libs/numberFormat/index.native.js diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index c36129e5d319..ad99904118c9 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -92,24 +92,13 @@ const DetailsPage = ({ imageStyles={[styles.avatarLarge]} source={details.avatar} /> - {details.displayName && isSMSLogin - ? ( - - - {toLocalPhone(details.displayName)} - - - ) : ( - - {details.displayName || null} - - )} + {details.displayName && ( + + {isSMSLogin ? toLocalPhone(details.displayName) : details.displayName} + + )} {details.login ? ( - + {translate(isSMSLogin ? 'common.phoneNumber' diff --git a/src/pages/ReimbursementAccount/IdentityForm.js b/src/pages/ReimbursementAccount/IdentityForm.js index ce0a0383f330..3122d5e6532b 100644 --- a/src/pages/ReimbursementAccount/IdentityForm.js +++ b/src/pages/ReimbursementAccount/IdentityForm.js @@ -96,7 +96,7 @@ const IdentityForm = ({ onChangeText={val => onFieldChange('ssnLast4', val)} /> onFieldChange('street', val)} diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 564e0181679c..7244c9460a34 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -128,7 +128,7 @@ class SearchPage extends Component { } = getSearchOptions( this.props.reports, this.props.personalDetails, - this.state.searchValue, + this.state.searchValue.trim(), this.props.betas, ); this.setState({ diff --git a/src/pages/ValidateLogin2FANewWorkspacePage.js b/src/pages/ValidateLogin2FANewWorkspacePage.js index bbe5b804a200..6e9c07d44b9b 100644 --- a/src/pages/ValidateLogin2FANewWorkspacePage.js +++ b/src/pages/ValidateLogin2FANewWorkspacePage.js @@ -3,9 +3,10 @@ import {TextInput, View} from 'react-native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import validateLinkPropTypes from './validateLinkPropTypes'; -import {continueSessionFromECom} from '../libs/actions/Session'; +import {continueSessionFromECom, setRedirectToWorkspaceNewAfterSignIn} from '../libs/actions/Session'; import styles from '../styles/styles'; import ExpensifyCashLogo from '../components/ExpensifyCashLogo'; import variables from '../styles/variables'; @@ -30,6 +31,9 @@ const propTypes = { /** The accountID and validateCode are passed via the URL */ route: validateLinkPropTypes, + /** List of betas */ + betas: PropTypes.arrayOf(PropTypes.string), + ...withLocalizePropTypes, }; @@ -38,6 +42,7 @@ const defaultProps = { params: {}, }, session: {}, + betas: [], }; class ValidateLogin2FANewWorkspacePage extends Component { constructor(props) { @@ -62,7 +67,12 @@ class ValidateLogin2FANewWorkspacePage extends Component { // by calling dismissModal(), the /v/... route is removed from history so the user will get taken to `/` // if they cancel out of the new workspace modal. Navigation.dismissModal(); - Navigation.navigate(ROUTES.WORKSPACE_NEW); + + if (_.isEmpty(this.props.betas)) { + setRedirectToWorkspaceNewAfterSignIn(true); + } else { + Navigation.navigate(ROUTES.WORKSPACE_NEW); + } } } @@ -144,5 +154,8 @@ export default compose( session: { key: ONYXKEYS.SESSION, }, + betas: { + key: ONYXKEYS.BETAS, + }, }), )(ValidateLogin2FANewWorkspacePage); diff --git a/src/pages/ValidateLoginNewWorkspacePage.js b/src/pages/ValidateLoginNewWorkspacePage.js index 5e672f1a4d9b..5a1b8ab1f1c8 100644 --- a/src/pages/ValidateLoginNewWorkspacePage.js +++ b/src/pages/ValidateLoginNewWorkspacePage.js @@ -2,12 +2,13 @@ import {Component} from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; +import _ from 'underscore'; import validateLinkPropTypes from './validateLinkPropTypes'; import compose from '../libs/compose'; import ONYXKEYS from '../ONYXKEYS'; import Navigation from '../libs/Navigation/Navigation'; import ROUTES from '../ROUTES'; -import {continueSessionFromECom} from '../libs/actions/Session'; +import {continueSessionFromECom, setRedirectToWorkspaceNewAfterSignIn} from '../libs/actions/Session'; const propTypes = { /* Onyx Props */ @@ -20,6 +21,9 @@ const propTypes = { /** The accountID and validateCode are passed via the URL */ route: validateLinkPropTypes, + + /** List of betas */ + betas: PropTypes.arrayOf(PropTypes.string), }; const defaultProps = { @@ -27,6 +31,7 @@ const defaultProps = { params: {}, }, session: {}, + betas: [], }; class ValidateLoginNewWorkspacePage extends Component { componentDidMount() { @@ -39,7 +44,11 @@ class ValidateLoginNewWorkspacePage extends Component { // by calling dismissModal(), the /v/... route is removed from history so the user will get taken to `/` // if they cancel out of the new workspace modal. Navigation.dismissModal(); - Navigation.navigate(ROUTES.WORKSPACE_NEW); + if (_.isEmpty(this.props.betas)) { + setRedirectToWorkspaceNewAfterSignIn(true); + } else { + Navigation.navigate(ROUTES.WORKSPACE_NEW); + } return; } @@ -63,5 +72,8 @@ export default compose( session: { key: ONYXKEYS.SESSION, }, + betas: { + key: ONYXKEYS.BETAS, + }, }), )(ValidateLoginNewWorkspacePage); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index ae87bc4b6921..dfe7cea28c1b 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,4 +1,5 @@ import React from 'react'; +import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import styles from '../../styles/styles'; import ReportView from './report/ReportView'; @@ -8,6 +9,7 @@ import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; import {updateCurrentlyViewedReportID} from '../../libs/actions/Report'; +import ONYXKEYS from '../../ONYXKEYS'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -18,6 +20,13 @@ const propTypes = { reportID: PropTypes.string, }).isRequired, }).isRequired, + + /** Tells us if the sidebar has rendered */ + isSidebarLoaded: PropTypes.bool, +}; + +const defaultProps = { + isSidebarLoaded: false, }; class ReportScreen extends React.Component { @@ -84,6 +93,10 @@ class ReportScreen extends React.Component { } render() { + if (!this.props.isSidebarLoaded) { + return null; + } + return ( - {code} + {`${code}\uFE0F`} ); } @@ -322,7 +324,7 @@ class EmojiPickerMenu extends Component { this.setState({highlightedIndex: index})} - emoji={code} + emoji={`${code}\uFE0F`} isHighlighted={index === this.state.highlightedIndex} emojiSize={this.emojiSize} /> diff --git a/src/pages/home/report/EmojiPickerMenu/index.native.js b/src/pages/home/report/EmojiPickerMenu/index.native.js index b1a4d1302506..6708330dffca 100644 --- a/src/pages/home/report/EmojiPickerMenu/index.native.js +++ b/src/pages/home/report/EmojiPickerMenu/index.native.js @@ -58,7 +58,7 @@ class EmojiPickerMenu extends Component { if (item.header) { return ( - {item.code} + {`${item.code}\uFE0F`} ); } @@ -66,7 +66,7 @@ class EmojiPickerMenu extends Component { return ( ); diff --git a/src/pages/home/report/ParticipantLocalTime.js b/src/pages/home/report/ParticipantLocalTime.js index 595ccb40ae29..cfd902da46e4 100644 --- a/src/pages/home/report/ParticipantLocalTime.js +++ b/src/pages/home/report/ParticipantLocalTime.js @@ -42,9 +42,14 @@ class ParticipantLocalTime extends React.Component { getParticipantLocalTime() { const reportRecipientTimezone = lodashGet(this.props.participant, 'timezone', {}); - return moment().tz(reportRecipientTimezone.selected).format('LT'); - } + const reportRecipientDay = moment().tz(reportRecipientTimezone.selected).format('dddd'); + const currentUserDay = moment().tz(this.props.currentUserTimezone.selected).format('dddd'); + if (reportRecipientDay !== currentUserDay) { + return `${moment().tz(reportRecipientTimezone.selected).format('LT')} ${reportRecipientDay}`; + } + return `${moment().tz(reportRecipientTimezone.selected).format('LT')}`; + } render() { // Moment.format does not return AM or PM values immediately. diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index d0bd5fef9e77..f67ea162dafd 100755 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -452,7 +452,7 @@ class ReportActionCompose extends React.Component { ]} > {shouldShowReportRecipientLocalTime - && } + && } this.props.isSmallScreenWidth && canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={this.showPopover} + preventDefaultContentMenu={!this.props.draftMessage} > {hovered => ( diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 1cf5bf99fce8..84befd28f4c8 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -95,6 +95,8 @@ const ReportActionItemSingle = ({ ReportActionItemSingle.propTypes = propTypes; ReportActionItemSingle.defaultProps = defaultProps; +ReportActionItemSingle.displayName = 'ReportActionItemSingle'; + export default compose( withLocalize, withOnyx({ diff --git a/src/pages/home/report/ReportView.js b/src/pages/home/report/ReportView.js index 8481be201ed8..1eb4ea57589a 100644 --- a/src/pages/home/report/ReportView.js +++ b/src/pages/home/report/ReportView.js @@ -47,6 +47,7 @@ const ReportView = ({reportID, session}) => ( ReportView.propTypes = propTypes; ReportView.defaultProps = defaultProps; +ReportView.displayName = 'ReportView'; export default withOnyx({ session: { diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 48a9c30b842e..d846b67e9e29 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -20,7 +20,7 @@ import CONST from '../../../CONST'; import {participantPropTypes} from './optionPropTypes'; import themeColors from '../../../styles/themes/default'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; - +import * as App from '../../../libs/actions/App'; const propTypes = { /** Toggles the navigation menu open and closed */ @@ -179,6 +179,7 @@ class SidebarLinks extends React.Component { showTitleTooltip disableFocusOptions={this.props.isSmallScreenWidth} optionMode={this.props.priorityMode === CONST.PRIORITY_MODE.GSD ? 'compact' : 'default'} + onLayout={App.setSidebarLoaded} /> diff --git a/src/pages/home/sidebar/SidebarScreen.js b/src/pages/home/sidebar/SidebarScreen.js index fcfd60c59950..b4f9dabeb2ed 100755 --- a/src/pages/home/sidebar/SidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen.js @@ -23,6 +23,7 @@ import { } from '../../../components/Icon/Expensicons'; import Permissions from '../../../libs/Permissions'; import ONYXKEYS from '../../../ONYXKEYS'; +import Firebase from '../../../libs/Firebase'; const propTypes = { /** Beta features list */ @@ -47,6 +48,10 @@ class SidebarScreen extends Component { }; } + componentDidMount() { + Firebase.startTrace(CONST.TIMING.SIDEBAR_LOADED); + } + /** * Method called when a Create Menu item is selected. */ diff --git a/src/pages/iou/IOUModal.js b/src/pages/iou/IOUModal.js index 284148db3ea3..660ca27b0422 100755 --- a/src/pages/iou/IOUModal.js +++ b/src/pages/iou/IOUModal.js @@ -20,6 +20,7 @@ import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndica import ScreenWrapper from '../../components/ScreenWrapper'; import CONST from '../../CONST'; import KeyboardAvoidingView from '../../components/KeyboardAvoidingView'; +import * as PersonalDetails from '../../libs/actions/PersonalDetails'; /** * IOU modal for requesting money and splitting bills. @@ -129,6 +130,7 @@ class IOUModal extends Component { } componentDidMount() { + PersonalDetails.fetchLocalCurrency(); setIOUSelectedCurrency(this.props.myPersonalDetails.localCurrencyCode); } @@ -287,9 +289,9 @@ class IOUModal extends Component { - {didScreenTransitionEnd && !this.props.iou.isRetrievingCurrency && ( + {didScreenTransitionEnd && ( <> {currentStep === Steps.IOUAmount && ( { // Add all bank accounts besides the wallet if (bankAccount.type !== CONST.BANK_ACCOUNT_TYPES.WALLET) { + const formattedBankAccountNumber = bankAccount.accountNumber + ? `${this.props.translate('paymentMethodList.accountLastFour')} ${ + bankAccount.accountNumber.slice(-4) + }` + : null; combinedPaymentMethods.push({ type: MENU_ITEM, title: bankAccount.addressName, // eslint-disable-next-line - description: `${this.props.translate('paymentMethodList.accountLastFour')} ${bankAccount.accountNumber.slice(-4)}`, + description: formattedBankAccountNumber, icon: Bank, onPress: e => this.props.onPress(e, bankAccount.bankAccountID), key: `bankAccount-${bankAccount.bankAccountID}`, @@ -99,12 +104,15 @@ class PaymentMethodList extends Component { _.each(this.props.cardList, (card) => { // Add all cards besides the "cash" card if (card.cardName !== CONST.CARD_TYPES.DEFAULT_CASH) { + const formattedCardNumber = card.cardNumber + ? `${this.props.translate('paymentMethodList.cardLastFour')} ${card.cardNumber.slice(-4)}` + : null; combinedPaymentMethods.push({ type: MENU_ITEM, title: card.cardName, // eslint-disable-next-line - description: `${this.props.translate('paymentMethodList.cardLastFour')} ${card.cardNumber.slice(-4)}`, + description: formattedCardNumber, icon: CreditCard, onPress: e => this.props.onPress(e, card.cardID), key: `card-${card.cardID}`, diff --git a/src/pages/settings/PreferencesPage.js b/src/pages/settings/PreferencesPage.js index 009150001840..03e8c23c017c 100755 --- a/src/pages/settings/PreferencesPage.js +++ b/src/pages/settings/PreferencesPage.js @@ -4,6 +4,7 @@ import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import LocalePicker from '../../components/LocalePicker'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import ONYXKEYS from '../../ONYXKEYS'; @@ -17,7 +18,6 @@ import Switch from '../../components/Switch'; import Picker from '../../components/Picker'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; -import {setLocale} from '../../libs/actions/App'; const propTypes = { /** The chat priority mode */ @@ -29,20 +29,16 @@ const propTypes = { expensifyNewsStatus: PropTypes.bool, }), - /** Indicates which locale the user currently has selected */ - preferredLocale: PropTypes.string, - ...withLocalizePropTypes, }; const defaultProps = { priorityMode: CONST.PRIORITY_MODE.DEFAULT, user: {}, - preferredLocale: CONST.DEFAULT_LOCALE, }; const PreferencesPage = ({ - priorityMode, user, translate, preferredLocale, + priorityMode, user, translate, }) => { const priorityModes = { default: { @@ -57,17 +53,6 @@ const PreferencesPage = ({ }, }; - const localesToLanguages = { - default: { - value: 'en', - label: translate('preferencesPage.languages.english'), - }, - es: { - value: 'es', - label: translate('preferencesPage.languages.spanish'), - }, - }; - return ( {priorityModes[priorityMode].description} - - {translate('preferencesPage.language')} - - { - if (locale !== preferredLocale) { - setLocale(locale); - } - }} - items={Object.values(localesToLanguages)} - value={preferredLocale} - /> + @@ -142,8 +116,5 @@ export default compose( user: { key: ONYXKEYS.USER, }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, }), )(PreferencesPage); diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index df883255c03a..c8d7e5ba0b09 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -193,8 +193,8 @@ class ProfilePage extends Component { && this.props.myPersonalDetails.pronouns === this.state.selfSelectedPronouns); // Disables button if none of the form values have changed - const isButtonDisabled = (this.props.myPersonalDetails.firstName === this.state.firstName) - && (this.props.myPersonalDetails.lastName === this.state.lastName) + const isButtonDisabled = (this.props.myPersonalDetails.firstName === this.state.firstName.trim()) + && (this.props.myPersonalDetails.lastName === this.state.lastName.trim()) && (this.props.myPersonalDetails.timezone.selected === this.state.selectedTimezone) && (this.props.myPersonalDetails.timezone.automatic === this.state.isAutomaticTimezone) && arePronounsUnchanged; diff --git a/src/pages/signin/LoginForm.js b/src/pages/signin/LoginForm.js index 547c43760fe9..d6f7d8463830 100755 --- a/src/pages/signin/LoginForm.js +++ b/src/pages/signin/LoginForm.js @@ -88,15 +88,6 @@ class LoginForm extends React.Component { autoFocus={canFocusInputOnScreenFocus()} /> - -