diff --git a/README.md b/README.md index 85cf05ac619e..55d9b8f1478e 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ to help run our Unit tests. 2. This will allow you to attach a debugger in your IDE, React Developer Tools, or your browser. 3. For more information on how to attach a debugger, see [React Native Debugging Documentation](https://reactnative.dev/docs/debugging#chrome-developer-tools) -Alternatively, you can also setup debugger using [Flipper](https://fbflipper.com/). After installation, press `⌘D` and select "Open Debugger". This will open Flipper window. To view data stored by Onyx, go to Plugin Manager and install `async-storage` plugin. +Alternatively, you can also set up debugger using [Flipper](https://fbflipper.com/). After installation, press `⌘D` and select "Open Debugger". This will open Flipper window. To view data stored by Onyx, go to Plugin Manager and install `async-storage` plugin. ## Android Our React Native Android app now uses the `Hermes` JS engine which requires your browser for remote debugging. These instructions are specific to Chrome since that's what the Hermes documentation provided. @@ -304,7 +304,7 @@ This application is built with the following principles. - The UI should never call any Onyx methods except for `Onyx.connect()`. That is the job of Actions (see next section). - The UI always triggers an Action when something needs to happen (eg. a person inputs data, the UI triggers an Action with this data). - The UI should be as flexible as possible when it comes to: - - Incomplete or missing data. Always assume data is incomplete or not there. For example, when a comment is pushed to the client from a pusher event, it's possible that Onyx does not have data for that report yet. That's OK. A partial report object is added to Onyx for the report key `report_1234 = {reportID: 1234, isUnread: true}`. Then there is code that monitors Onyx for reports with incomplete data, and calls `fetchChatReportsByIDs(1234)` to get the full data for that report. The UI should be able to gracefully handle the report object not being complete. In this example, the sidebar wouldn't display any report that doesn't have a report name. + - Incomplete or missing data. Always assume data is incomplete or not there. For example, when a comment is pushed to the client from a pusher event, it's possible that Onyx does not have data for that report yet. That's OK. A partial report object is added to Onyx for the report key `report_1234 = {reportID: 1234, isUnread: true}`. Then there is code that monitors Onyx for reports with incomplete data, and calls `fetchChatReportsByIDs(1234)` to get the full data for that report. The UI should be able to gracefully handle the report object not being complete. In this example, the sidebar wouldn't display any report that does not have a report name. - The order that actions are done in. All actions should be done in parallel instead of sequence. - Parallel actions are asynchronous methods that don't return promises. Any number of these actions can be called at one time and it doesn't matter what order they happen in or when they complete. - In-Sequence actions are asynchronous methods that return promises. This is necessary when one asynchronous method depends on the results from a previous asynchronous method. Example: Making an XHR to `command=CreateChatReport` which returns a reportID which is used to call `command=Get&rvl=reportStuff`. diff --git a/android/app/build.gradle b/android/app/build.gradle index 79527d76cbe7..4196ac8a5291 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -152,8 +152,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001018605 - versionName "1.1.86-5" + versionCode 1001018708 + versionName "1.1.87-8" } splits { abi { diff --git a/contributingGuides/API.md b/contributingGuides/API.md index 1c0841c2d640..697ac01c15cc 100644 --- a/contributingGuides/API.md +++ b/contributingGuides/API.md @@ -43,14 +43,16 @@ The data will automatically be sent to the user via Pusher. #### WRITE Response Errors When there is an error on a WRITE response (`jsonCode!==200`), the error must come back to the client on the HTTPS response. The error is only relevant to the client that made the request and it wouldn't make sense to send it out to all connected clients. -Error messages should be returned and stored as a String under the `error` property. If absolutely needed, additional error properties can be stored under other, more specific fields that sit at the same level as `error`: +Error messages should be returned and stored as an object under the `errors` property, keyed by an integer [microtime](https://github.com/Expensify/Web-Expensify/blob/25d056c9c531ea7f12c9bf3283ec554dd5d1d316/lib/Onyx.php#L148-L154). If absolutely needed, additional error properties can be stored under other, more specific fields that sit at the same level as `errors`: ```php [ 'onyxMethod' => Onyx::METHOD_MERGE, 'key' => OnyxKeys::WALLET_ADDITIONAL_DETAILS, 'value' => [ - 'error' => 'We\'re having trouble verifying your SSN. Please enter the full 9 digits of your SSN.', - 'errorCode' => 'ssnError' + 'errors' => [ + Onyx::getErrorMicroTime() => 'We\'re having trouble verifying your SSN. Please enter the full 9 digits of your SSN.', + ], + 'errorCode' => 'ssnError', ], ] ``` diff --git a/desktop/main.js b/desktop/main.js index 0eb944fca509..f7bd55411f4d 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -31,7 +31,17 @@ app.commandLine.appendSwitch('enable-network-information-downlink-max'); // Initialize the right click menu // See https://github.com/sindresorhus/electron-context-menu -contextMenu(); +// Add the Paste and Match Style command to the context menu +contextMenu({ + append: (defaultActions, parameters) => [ + new MenuItem({ + // Only enable the menu item for Editable context which supports paste + visible: parameters.isEditable && parameters.editFlags.canPaste, + role: 'pasteAndMatchStyle', + accelerator: 'CmdOrCtrl+Shift+V', + }), + ], +}); // Send all autoUpdater logs to a log file: ~/Library/Logs/new.expensify.desktop/main.log // See https://www.npmjs.com/package/electron-log @@ -202,6 +212,13 @@ const mainWindow = (() => { }], })); + // Register the custom Paste and Match Style command and place it near the default shortcut of the same role. + const editMenu = _.find(systemMenu.items, item => item.role === 'editmenu'); + editMenu.submenu.insert(6, new MenuItem({ + role: 'pasteAndMatchStyle', + accelerator: 'CmdOrCtrl+Shift+V', + })); + const appMenu = _.find(systemMenu.items, item => item.role === 'appmenu'); appMenu.submenu.insert(1, updateAppMenuItem); appMenu.submenu.insert(2, keyboardShortcutsMenu); diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 68b2d20f6da4..6e5fcc0ab6ab 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.86 + 1.1.87 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.1.86.5 + 1.1.87.8 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 4dd5cc25cccf..de1502589e1c 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.1.86 + 1.1.87 CFBundleSignature ???? CFBundleVersion - 1.1.86.5 + 1.1.87.8 diff --git a/package-lock.json b/package-lock.json index 76a0c02de9e8..c3ed16be4b56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.86-5", + "version": "1.1.87-8", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -28903,9 +28903,9 @@ } }, "electron": { - "version": "17.4.10", - "resolved": "https://registry.npmjs.org/electron/-/electron-17.4.10.tgz", - "integrity": "sha512-4v5Xwa4rZjWf0LmpYOaBXG8ZQ1rpPEpww+MCe4uuwenFsx3QSLSXmek720EY7drQa/O1YyvcZ1pr2sDBMIq0mA==", + "version": "17.4.11", + "resolved": "https://registry.npmjs.org/electron/-/electron-17.4.11.tgz", + "integrity": "sha512-mdSWM2iY/Bh5bKzd5drYS3mf8JWyR9P9UXZA2uLEZ+1fhgLEVkY9qu501QHoMsKlNwgn96EreQC+dfdQ75VTcA==", "dev": true, "requires": { "@electron/get": "^1.13.0", @@ -31480,7 +31480,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true } } @@ -31667,7 +31667,7 @@ "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "requires": { "pend": "~1.2.0" @@ -41265,7 +41265,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, "optional": true } @@ -42074,7 +42074,7 @@ "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, "performance-now": { @@ -43019,7 +43019,7 @@ "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true, "optional": true }, @@ -45584,7 +45584,7 @@ "semver-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true, "optional": true }, @@ -50501,7 +50501,7 @@ "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "requires": { "buffer-crc32": "~0.2.3", diff --git a/package.json b/package.json index d587efc1a1ad..d6907f7ddff2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.86-5", + "version": "1.1.87-8", "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.", @@ -151,7 +151,7 @@ "copy-webpack-plugin": "^6.0.3", "css-loader": "^5.2.4", "diff-so-fancy": "^1.3.0", - "electron": "^17.4.5", + "electron": "^17.4.11", "electron-builder": "23.3.1", "electron-notarize": "^1.2.1", "electron-reloader": "^1.2.1", diff --git a/src/CONST.js b/src/CONST.js index b981c6e92624..a8a5df4be82b 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -208,6 +208,7 @@ const CONST = { CURRENCY: { USD: 'USD', }, + EXAMPLE_PHONE_NUMBER: '+15005550006', CONCIERGE_CHAT_NAME: 'Concierge', CLOUDFRONT_URL, USE_EXPENSIFY_URL, @@ -477,6 +478,7 @@ const CONST = { EMOJI_PICKER_ITEM_HEIGHT: 40, EMOJI_PICKER_HEADER_HEIGHT: 38, COMPOSER_MAX_HEIGHT: 125, + CHAT_FOOTER_MIN_HEIGHT: 65, CHAT_SKELETON_VIEW: { AVERAGE_ROW_HEIGHT: 80, HEIGHT_FOR_ROW_COUNT: { diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 355cf8d37c2c..17302ba46467 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -28,9 +28,6 @@ export default { // Note: These are Persisted Requests - not all requests in the main queue as the key name might lead one to believe PERSISTED_REQUESTS: 'networkRequestQueue', - // What the active route is for our navigator. Global route that determines what views to display. - CURRENT_URL: 'currentURL', - // Stores current date CURRENT_DATE: 'currentDate', @@ -180,8 +177,8 @@ export default { // Is Keyboard shortcuts modal open? IS_SHORTCUTS_MODAL_OPEN: 'isShortcutsModalOpen', - // Is close acount modal open? - IS_CLOSE_ACCOUNT_MODAL_OPEN: 'isCloseAccountModalOpen', + // Data related to user closing their account (loading status and error message) + CLOSE_ACCOUNT: 'closeAccount', // Stores information about active wallet transfer amount, selectedAccountID, status, etc WALLET_TRANSFER: 'walletTransfer', diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index aa37769156e3..765ca641a3f4 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -1,126 +1,66 @@ -import React, {PureComponent} from 'react'; -import { - View, StyleSheet, Animated, -} from 'react-native'; +import _ from 'underscore'; +import React from 'react'; +import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; import Avatar from './Avatar'; -import themeColors from '../styles/themes/default'; import styles from '../styles/styles'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import SpinningIndicatorAnimation from '../styles/animation/SpinningIndicatorAnimation'; import Tooltip from './Tooltip'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import ONYXKEYS from '../ONYXKEYS'; +import policyMemberPropType from '../pages/policyMemberPropType'; +import * as Policy from '../libs/actions/Policy'; const propTypes = { - /** Is user active? */ - isActive: PropTypes.bool, - /** URL for the avatar */ source: PropTypes.string.isRequired, /** Avatar size */ size: PropTypes.string, - // Whether we show the sync indicator - isSyncing: PropTypes.bool, - /** To show a tooltip on hover */ tooltipText: PropTypes.string, - ...withLocalizePropTypes, + /** The employee list of all policies (coming from Onyx) */ + policiesMemberList: PropTypes.objectOf(policyMemberPropType), }; const defaultProps = { - isActive: false, size: 'default', - isSyncing: false, tooltipText: '', + policiesMemberList: {}, }; -class AvatarWithIndicator extends PureComponent { - constructor(props) { - super(props); - - this.animation = new SpinningIndicatorAnimation(); - } - - componentDidMount() { - if (!this.props.isSyncing) { - return; - } - - this.animation.start(); - } - - componentDidUpdate(prevProps) { - if (!prevProps.isSyncing && this.props.isSyncing) { - this.animation.start(); - } else if (prevProps.isSyncing && !this.props.isSyncing) { - this.animation.stop(); - } - } - - componentWillUnmount() { - this.animation.stop(); - } - - /** - * Returns user status as text - * - * @returns {String} - */ - userStatus() { - if (this.props.isSyncing) { - return this.props.translate('profilePage.syncing'); - } - - if (this.props.isActive) { - return this.props.translate('profilePage.online'); - } - - if (!this.props.isActive) { - return this.props.translate('profilePage.offline'); - } - } - - render() { - const indicatorStyles = [ - styles.alignItemsCenter, - styles.justifyContentCenter, - this.props.size === 'large' ? styles.statusIndicatorLarge : styles.statusIndicator, - this.props.isActive ? styles.statusIndicatorOnline : styles.statusIndicatorOffline, - this.animation.getSyncingStyles(), - ]; - - return ( - - - - - - - {this.props.isSyncing && ( - - )} - - - - ); - } -} +const AvatarWithIndicator = (props) => { + const isLarge = props.size === 'large'; + const indicatorStyles = [ + styles.alignItemsCenter, + styles.justifyContentCenter, + isLarge ? styles.statusIndicatorLarge : styles.statusIndicator, + ]; + + const hasPolicyMemberError = _.some(props.policiesMemberList, policyMembers => Policy.hasPolicyMemberError(policyMembers)); + return ( + + + + {hasPolicyMemberError && ( + + )} + + + ); +}; AvatarWithIndicator.defaultProps = defaultProps; AvatarWithIndicator.propTypes = propTypes; -export default withLocalize(AvatarWithIndicator); +AvatarWithIndicator.displayName = 'AvatarWithIndicator'; + +export default withOnyx({ + policiesMemberList: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST, + }, +})(AvatarWithIndicator); diff --git a/src/components/BlockingViews/FullPageNotFoundView.js b/src/components/BlockingViews/FullPageNotFoundView.js index 9b9afdcd8b83..8a4447bb0cdc 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.js +++ b/src/components/BlockingViews/FullPageNotFoundView.js @@ -1,9 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {View} from 'react-native'; import BlockingView from './BlockingView'; import * as Expensicons from '../Icon/Expensicons'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; +import HeaderWithCloseButton from '../HeaderWithCloseButton'; +import Navigation from '../../libs/Navigation/Navigation'; +import styles from '../../styles/styles'; const propTypes = { /** Props to fetch translation features */ @@ -24,11 +28,21 @@ const defaultProps = { const FullPageNotFoundView = (props) => { if (props.shouldShow) { return ( - + <> + Navigation.dismissModal()} + onCloseButtonPress={() => Navigation.dismissModal()} + /> + + + + + ); } diff --git a/src/components/CopySelectionHelper.js b/src/components/CopySelectionHelper.js index 5f00bab3146b..119910bb4c73 100644 --- a/src/components/CopySelectionHelper.js +++ b/src/components/CopySelectionHelper.js @@ -1,4 +1,5 @@ import React from 'react'; +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import CONST from '../CONST'; import KeyboardShortcut from '../libs/KeyboardShortcut'; import Clipboard from '../libs/Clipboard'; @@ -25,8 +26,16 @@ class CopySelectionHelper extends React.Component { } copySelectionToClipboard() { - const selectionMarkdown = SelectionScraper.getAsMarkdown(); - Clipboard.setString(selectionMarkdown); + const selection = SelectionScraper.getCurrentSelection(); + if (!selection) { + return; + } + const parser = new ExpensiMark(); + if (!Clipboard.canSetHtml()) { + Clipboard.setString(parser.htmlToMarkdown(selection)); + return; + } + Clipboard.setHtml(selection, parser.htmlToText(selection)); } render() { diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js index 196a4da3d403..2422e4745b73 100644 --- a/src/components/OfflineWithFeedback.js +++ b/src/components/OfflineWithFeedback.js @@ -124,6 +124,7 @@ const OfflineWithFeedback = (props) => { OfflineWithFeedback.propTypes = propTypes; OfflineWithFeedback.defaultProps = defaultProps; +OfflineWithFeedback.displayName = 'OfflineWithFeedback'; export default compose( withLocalize, diff --git a/src/components/OptionsSelector.js b/src/components/OptionsSelector.js index 2f113f58dd03..c41677a0d775 100755 --- a/src/components/OptionsSelector.js +++ b/src/components/OptionsSelector.js @@ -220,8 +220,8 @@ class OptionsSelector extends Component { allOptions: newOptions, focusedIndex: newFocusedIndex, }, () => { - // If we just selected a new option on a multiple-selection page, scroll to the top - if (this.props.selectedOptions.length > prevProps.selectedOptions.length) { + // If we just toggled an option on a multi-selection page, scroll to top + if (this.props.selectedOptions.length !== prevProps.selectedOptions.length) { this.scrollToIndex(0); return; } diff --git a/src/components/PressableWithSecondaryInteraction/index.js b/src/components/PressableWithSecondaryInteraction/index.js index 6dd1495f9380..a6b21a59e1cb 100644 --- a/src/components/PressableWithSecondaryInteraction/index.js +++ b/src/components/PressableWithSecondaryInteraction/index.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import React, {Component} from 'react'; import {Pressable} from 'react-native'; import {LongPressGestureHandler, State} from 'react-native-gesture-handler'; -import SelectionScraper from '../../libs/SelectionScraper'; import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes'; import styles from '../../styles/styles'; import hasHoverSupport from '../../libs/hasHoverSupport'; @@ -54,12 +53,11 @@ class PressableWithSecondaryInteraction extends Component { * https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event */ executeSecondaryInteractionOnContextMenu(e) { - const selection = SelectionScraper.getAsMarkdown(); e.stopPropagation(); if (this.props.preventDefaultContentMenu) { e.preventDefault(); } - this.props.onSecondaryInteraction(e, selection); + this.props.onSecondaryInteraction(e); } render() { diff --git a/src/components/ScreenWrapper.js b/src/components/ScreenWrapper.js deleted file mode 100644 index bc6a416dbd01..000000000000 --- a/src/components/ScreenWrapper.js +++ /dev/null @@ -1,147 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import PropTypes from 'prop-types'; -import {View} from 'react-native'; -import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; -import {withOnyx} from 'react-native-onyx'; -import styles from '../styles/styles'; -import * as StyleUtils from '../styles/StyleUtils'; -import HeaderGap from './HeaderGap'; -import KeyboardShortcutsModal from './KeyboardShortcutsModal'; -import KeyboardShortcut from '../libs/KeyboardShortcut'; -import onScreenTransitionEnd from '../libs/onScreenTransitionEnd'; -import Navigation from '../libs/Navigation/Navigation'; -import compose from '../libs/compose'; -import ONYXKEYS from '../ONYXKEYS'; -import CONST from '../CONST'; -import withNavigation from './withNavigation'; - -const propTypes = { - /** Array of additional styles to add */ - style: PropTypes.arrayOf(PropTypes.object), - - /** Returns a function as a child to pass insets to or a node to render without insets */ - children: PropTypes.oneOfType([ - PropTypes.node, - PropTypes.func, - ]).isRequired, - - /** Whether to include padding bottom */ - includePaddingBottom: PropTypes.bool, - - /** Whether to include padding top */ - includePaddingTop: PropTypes.bool, - - // Called when navigated Screen's transition is finished. - onTransitionEnd: PropTypes.func, - - // react-navigation navigation object available to screen components - navigation: PropTypes.shape({ - // Method to attach listener to Navigation state. - addListener: PropTypes.func.isRequired, - }), - - /** Details about any modals being used */ - modal: PropTypes.shape({ - /** Indicates when an Alert modal is about to be visible */ - willAlertModalBecomeVisible: PropTypes.bool, - }), - -}; - -const defaultProps = { - style: [], - includePaddingBottom: true, - includePaddingTop: true, - onTransitionEnd: () => {}, - navigation: { - addListener: () => {}, - }, - modal: {}, -}; - -class ScreenWrapper extends React.Component { - constructor(props) { - super(props); - - this.state = { - didScreenTransitionEnd: false, - }; - } - - componentDidMount() { - const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.ESCAPE; - this.unsubscribeEscapeKey = KeyboardShortcut.subscribe(shortcutConfig.shortcutKey, () => { - if (this.props.modal.willAlertModalBecomeVisible) { - return; - } - - Navigation.dismissModal(); - }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true); - - this.unsubscribeTransitionEnd = onScreenTransitionEnd(this.props.navigation, () => { - this.setState({didScreenTransitionEnd: true}); - this.props.onTransitionEnd(); - }); - } - - componentWillUnmount() { - if (this.unsubscribeEscapeKey) { - this.unsubscribeEscapeKey(); - } - if (this.unsubscribeTransitionEnd) { - this.unsubscribeTransitionEnd(); - } - } - - render() { - return ( - - {(insets) => { - const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets); - const paddingStyle = {}; - - if (this.props.includePaddingTop) { - paddingStyle.paddingTop = paddingTop; - } - - if (this.props.includePaddingBottom) { - paddingStyle.paddingBottom = paddingBottom; - } - - return ( - - - {// If props.children is a function, call it to provide the insets to the children. - _.isFunction(this.props.children) - ? this.props.children({ - insets, - didScreenTransitionEnd: this.state.didScreenTransitionEnd, - }) - : this.props.children - } - - - ); - }} - - ); - } -} - -ScreenWrapper.propTypes = propTypes; -ScreenWrapper.defaultProps = defaultProps; - -export default compose( - withNavigation, - withOnyx({ - modal: { - key: ONYXKEYS.MODAL, - }, - }), -)(ScreenWrapper); diff --git a/src/components/ScreenWrapper/BaseScreenWrapper.js b/src/components/ScreenWrapper/BaseScreenWrapper.js new file mode 100644 index 000000000000..a80a013a1c66 --- /dev/null +++ b/src/components/ScreenWrapper/BaseScreenWrapper.js @@ -0,0 +1,115 @@ +import {KeyboardAvoidingView, View} from 'react-native'; +import React from 'react'; +import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import _ from 'underscore'; +import {withOnyx} from 'react-native-onyx'; +import CONST from '../../CONST'; +import KeyboardShortcut from '../../libs/KeyboardShortcut'; +import Navigation from '../../libs/Navigation/Navigation'; +import onScreenTransitionEnd from '../../libs/onScreenTransitionEnd'; +import * as StyleUtils from '../../styles/StyleUtils'; +import styles from '../../styles/styles'; +import HeaderGap from '../HeaderGap'; +import KeyboardShortcutsModal from '../KeyboardShortcutsModal'; +import OfflineIndicator from '../OfflineIndicator'; +import compose from '../../libs/compose'; +import withNavigation from '../withNavigation'; +import withWindowDimensions from '../withWindowDimensions'; +import ONYXKEYS from '../../ONYXKEYS'; +import {withNetwork} from '../OnyxProvider'; +import {propTypes, defaultProps} from './propTypes'; + +class BaseScreenWrapper extends React.Component { + constructor(props) { + super(props); + + this.state = { + didScreenTransitionEnd: false, + }; + } + + componentDidMount() { + const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.ESCAPE; + this.unsubscribeEscapeKey = KeyboardShortcut.subscribe(shortcutConfig.shortcutKey, () => { + if (this.props.modal.willAlertModalBecomeVisible) { + return; + } + + Navigation.dismissModal(); + }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true); + + this.unsubscribeTransitionEnd = onScreenTransitionEnd(this.props.navigation, () => { + this.setState({didScreenTransitionEnd: true}); + this.props.onTransitionEnd(); + }); + } + + componentWillUnmount() { + if (this.unsubscribeEscapeKey) { + this.unsubscribeEscapeKey(); + } + if (this.unsubscribeTransitionEnd) { + this.unsubscribeTransitionEnd(); + } + } + + render() { + return ( + + {(insets) => { + const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets); + const paddingStyle = {}; + + if (this.props.includePaddingTop) { + paddingStyle.paddingTop = paddingTop; + } + + // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked. + if (this.props.includePaddingBottom || this.props.network.isOffline) { + paddingStyle.paddingBottom = paddingBottom; + } + + return ( + + + + {// If props.children is a function, call it to provide the insets to the children. + _.isFunction(this.props.children) + ? this.props.children({ + insets, + didScreenTransitionEnd: this.state.didScreenTransitionEnd, + }) + : this.props.children + } + + {this.props.isSmallScreenWidth && ( + + )} + + + ); + }} + + ); + } +} + +BaseScreenWrapper.propTypes = propTypes; +BaseScreenWrapper.defaultProps = defaultProps; + +export default compose( + withNavigation, + withWindowDimensions, + withOnyx({ + modal: { + key: ONYXKEYS.MODAL, + }, + }), + withNetwork(), +)(BaseScreenWrapper); diff --git a/src/components/ScreenWrapper/index.android.js b/src/components/ScreenWrapper/index.android.js new file mode 100644 index 000000000000..55067bd50923 --- /dev/null +++ b/src/components/ScreenWrapper/index.android.js @@ -0,0 +1,17 @@ +import React from 'react'; +import BaseScreenWrapper from './BaseScreenWrapper'; +import {defaultProps, propTypes} from './propTypes'; + +const ScreenWrapper = props => ( + + {props.children} + +); +ScreenWrapper.propTypes = propTypes; +ScreenWrapper.defaultProps = defaultProps; + +export default ScreenWrapper; diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js new file mode 100644 index 000000000000..29c537442985 --- /dev/null +++ b/src/components/ScreenWrapper/index.js @@ -0,0 +1,16 @@ +import React from 'react'; +import BaseScreenWrapper from './BaseScreenWrapper'; +import {defaultProps, propTypes} from './propTypes'; + +const ScreenWrapper = props => ( + + {props.children} + +); +ScreenWrapper.propTypes = propTypes; +ScreenWrapper.defaultProps = defaultProps; + +export default ScreenWrapper; diff --git a/src/components/ScreenWrapper/propTypes.js b/src/components/ScreenWrapper/propTypes.js new file mode 100644 index 000000000000..ce1858ef20bd --- /dev/null +++ b/src/components/ScreenWrapper/propTypes.js @@ -0,0 +1,42 @@ +import PropTypes from 'prop-types'; + +const propTypes = { + /** Array of additional styles to add */ + style: PropTypes.arrayOf(PropTypes.object), + + /** Returns a function as a child to pass insets to or a node to render without insets */ + children: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.func, + ]).isRequired, + + /** Whether to include padding bottom */ + includePaddingBottom: PropTypes.bool, + + /** Whether to include padding top */ + includePaddingTop: PropTypes.bool, + + // Called when navigated Screen's transition is finished. + onTransitionEnd: PropTypes.func, + + /** The behavior to pass to the KeyboardAvoidingView, requires some trial and error depending on the layout/devices used. + * Search 'switch(behavior)' in ./node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js for more context */ + keyboardAvoidingViewBehavior: PropTypes.oneOf(['padding', 'height', 'position']), + + /** Details about any modals being used */ + modal: PropTypes.shape({ + /** Indicates when an Alert modal is about to be visible */ + willAlertModalBecomeVisible: PropTypes.bool, + }), +}; + +const defaultProps = { + style: [], + includePaddingBottom: true, + includePaddingTop: true, + onTransitionEnd: () => {}, + modal: {}, + keyboardAvoidingViewBehavior: 'padding', +}; + +export {propTypes, defaultProps}; diff --git a/src/languages/en.js b/src/languages/en.js index c1ea9b8db04b..3c52d05baaf6 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -81,7 +81,7 @@ export default { error: { invalidAmount: 'Invalid amount', acceptedTerms: 'You must accept the Terms of Service to continue', - phoneNumber: 'Please enter a valid phone number, with the country code (e.g. +1234567890)', + phoneNumber: `Please enter a valid phone number, with the country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, }, please: 'Please', contactUs: 'contact us', @@ -324,7 +324,6 @@ export default { enterMessageHere: 'Enter message here', closeAccountWarning: 'Closing your account cannot be undone.', closeAccountPermanentlyDeleteData: 'This will permanently delete all of your unsubmitted expense data. Type your phone number or email address to confirm.', - closeAccountSuccess: 'Account closed successfully', closeAccountActionRequired: 'Looks like you need to complete some actions before closing your account. Check out the guide', closeAccountTryAgainAfter: 'and try again after.', enterDefaultContact: 'Enter your default contact method', @@ -587,7 +586,7 @@ export default { callMeByMyName: 'Call me by my name', }, messages: { - errorMessageInvalidPhone: 'Please enter a valid phone number without brackets or dashes. If you\'re outside the US please include your country code, eg. +447782339811', + errorMessageInvalidPhone: `Please enter a valid phone number without brackets or dashes. If you're outside the US please include your country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, }, onfidoStep: { acceptTerms: 'By continuing with the request to activate your Expensify wallet, you confirm that you have read, understand and accept ', @@ -861,10 +860,9 @@ export default { invite: { invitePeople: 'Invite new members', personalMessagePrompt: 'Add a personal message (optional)', - pleaseSelectUser: 'Please select a user from contacts.', genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', welcomeNote: ({workspaceName}) => `You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`, - pleaseEnterValidLogin: 'Please ensure the email or phone number is valid (e.g. +15005550006).', + pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, }, editor: { nameInputLabel: 'Name', diff --git a/src/languages/es.js b/src/languages/es.js index ccc8b1ef36d1..c9e90af747cc 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -81,7 +81,7 @@ export default { error: { invalidAmount: 'Monto no válido', acceptedTerms: 'Debes aceptar los Términos de servicio para continuar', - phoneNumber: 'Ingresa un teléfono válido, incluyendo el código de país (p. ej. +1234567890)', + phoneNumber: `Ingresa un teléfono válido, incluyendo el código de país (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`, }, please: 'Por favor', contactUs: 'contáctenos', @@ -324,7 +324,6 @@ export default { enterMessageHere: 'Ingresa el mensaje aquí', closeAccountWarning: 'Una vez cerrada tu cuenta no se puede revertir.', closeAccountPermanentlyDeleteData: 'Esta acción eliminará permanentemente toda la información de tus gastos no enviados. Escribe tu número de teléfono o correo electrónico para confirmar', - closeAccountSuccess: 'Cuenta cerrada exitosamente', closeAccountActionRequired: 'Parece que necesitas completar algunas acciones antes de cerrar tu cuenta. Mira la guía', closeAccountTryAgainAfter: 'e intenta nuevamente', enterDefaultContact: 'Tu método de contacto predeterminado', @@ -587,7 +586,7 @@ export default { callMeByMyName: 'Llámame por mi nombre', }, messages: { - errorMessageInvalidPhone: 'Por favor, introduce un número de teléfono válido sin paréntesis o guiones. Si reside fuera de Estados Unidos, por favor incluye el prefijo internacional. P. ej. +447782339811', + errorMessageInvalidPhone: `Por favor, introduce un número de teléfono válido sin paréntesis o guiones. Si reside fuera de Estados Unidos, por favor incluye el prefijo internacional (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`, }, onfidoStep: { acceptTerms: 'Al continuar con la solicitud para activar su billetera Expensify, confirma que ha leído, comprende y acepta ', @@ -863,10 +862,9 @@ export default { invite: { invitePeople: 'Invitar nuevos miembros', personalMessagePrompt: 'Agregar un mensaje personal (Opcional)', - pleaseSelectUser: 'Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. +15005550006).', genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', welcomeNote: ({workspaceName}) => `¡Has sido invitado a ${workspaceName}! Descargue la aplicación móvil Expensify en use.expensify.com/download para comenzar a rastrear sus gastos.`, - pleaseEnterValidLogin: 'Asegúrese de que el correo electrónico o el número de teléfono sean válidos (e.g. +15005550006).', + pleaseEnterValidLogin: `Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`, }, editor: { nameInputLabel: 'Nombre', diff --git a/src/libs/Clipboard/index.js b/src/libs/Clipboard/index.js index 808e75b765ee..f50b53e36d4f 100644 --- a/src/libs/Clipboard/index.js +++ b/src/libs/Clipboard/index.js @@ -1,4 +1,34 @@ // on Web/desktop this import will be replaced with `react-native-web` import {Clipboard} from 'react-native-web'; +import lodashGet from 'lodash/get'; -export default Clipboard; +const canSetHtml = () => lodashGet(navigator, 'clipboard.write'); + +/** + * Writes the content as HTML if the web client supports it. + * @param {String} html HTML representation + * @param {String} text Plain text representation + */ +const setHtml = (html, text) => { + if (!html || !text) { + return; + } + + if (!canSetHtml()) { + throw new Error('clipboard.write is not supported on this platform, thus HTML cannot be copied.'); + } + + navigator.clipboard.write([ + // eslint-disable-next-line no-undef + new ClipboardItem({ + 'text/html': new Blob([html], {type: 'text/html'}), + 'text/plain': new Blob([text], {type: 'text/plain'}), + }), + ]); +}; + +export default { + ...Clipboard, + canSetHtml, + setHtml, +}; diff --git a/src/libs/Clipboard/index.native.js b/src/libs/Clipboard/index.native.js index db249165a421..c3d4ed69c17e 100644 --- a/src/libs/Clipboard/index.native.js +++ b/src/libs/Clipboard/index.native.js @@ -1,3 +1,9 @@ import Clipboard from '@react-native-community/clipboard'; -export default Clipboard; +export default { + ...Clipboard, + + // We don't want to set HTML on native platforms so noop them. + canSetHtml: () => false, + setHtml: () => {}, +}; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 4779e0ae1ca9..aa6a9324845f 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -3,6 +3,7 @@ import Onyx, {withOnyx} from 'react-native-onyx'; import moment from 'moment'; import _ from 'underscore'; import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import * as StyleUtils from '../../../styles/StyleUtils'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import CONST from '../../../CONST'; @@ -86,6 +87,9 @@ const modalScreenListeners = { const propTypes = { ...windowDimensionsPropTypes, + + /** The current path as reported by the NavigationContainer */ + currentPath: PropTypes.string.isRequired, }; class AuthScreens extends React.Component { @@ -115,7 +119,6 @@ class AuthScreens extends React.Component { App.openApp(this.props.allPolicies); App.fixAccountAndReloadData(); - App.setUpPoliciesAndNavigate(this.props.session); Timing.end(CONST.TIMING.HOMEPAGE_INITIAL_RENDER); const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH; @@ -133,6 +136,11 @@ class AuthScreens extends React.Component { } shouldComponentUpdate(nextProps) { + // we perform this check here instead of componentDidUpdate to skip an unnecessary re-render + if (this.props.currentPath !== nextProps.currentPath) { + App.setUpPoliciesAndNavigate(nextProps.session, nextProps.currentPath); + } + return nextProps.isSmallScreenWidth !== this.props.isSmallScreenWidth; } diff --git a/src/libs/Navigation/AppNavigator/BaseDrawerNavigator.js b/src/libs/Navigation/AppNavigator/BaseDrawerNavigator.js index 7114959db546..96664751686b 100644 --- a/src/libs/Navigation/AppNavigator/BaseDrawerNavigator.js +++ b/src/libs/Navigation/AppNavigator/BaseDrawerNavigator.js @@ -49,6 +49,17 @@ class BaseDrawerNavigator extends Component { }; } + componentDidUpdate(prevProps) { + if (prevProps.isSmallScreenWidth === this.props.isSmallScreenWidth) { + return; + } + + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + defaultStatus: Navigation.getDefaultDrawerState(this.props.isSmallScreenWidth), + }); + } + render() { const content = ( ( @@ -13,7 +16,7 @@ const AppNavigator = props => ( ? ( // These are the protected screens and only accessible when an authToken is present - + ) : ( diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index 6891244a2760..7f9328138120 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -10,11 +10,6 @@ import ONYXKEYS from '../../ONYXKEYS'; import linkingConfig from './linkingConfig'; import navigationRef from './navigationRef'; -let resolveNavigationIsReadyPromise; -let navigationIsReadyPromise = new Promise((resolve) => { - resolveNavigationIsReadyPromise = resolve; -}); - let isLoggedIn = false; Onyx.connect({ key: ONYXKEYS.SESSION, @@ -191,23 +186,6 @@ function isActiveRoute(routePath) { return getActiveRoute().substring(1) === routePath; } -/** - * @returns {Promise} - */ -function isNavigationReady() { - return navigationIsReadyPromise; -} - -function setIsNavigationReady() { - resolveNavigationIsReadyPromise(); -} - -function resetIsNavigationReady() { - navigationIsReadyPromise = new Promise((resolve) => { - resolveNavigationIsReadyPromise = resolve; - }); -} - export default { canNavigate, navigate, @@ -218,9 +196,6 @@ export default { closeDrawer, getDefaultDrawerState, setDidTapNotification, - isNavigationReady, - setIsNavigationReady, - resetIsNavigationReady, }; export { diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index 80c7dedec073..aac2594d6939 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -4,7 +4,6 @@ import {getPathFromState, NavigationContainer, DefaultTheme} from '@react-naviga import * as Navigation from './Navigation'; import linkingConfig from './linkingConfig'; import AppNavigator from './AppNavigator'; -import * as App from '../actions/App'; import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; import Log from '../Log'; import colors from '../../styles/colors'; @@ -31,6 +30,10 @@ class NavigationRoot extends Component { constructor(props) { super(props); + this.state = { + currentPath: '', + }; + this.parseAndStoreRoute = this.parseAndStoreRoute.bind(this); } @@ -43,15 +46,16 @@ class NavigationRoot extends Component { return; } - const path = getPathFromState(state, linkingConfig.config); + const currentPath = getPathFromState(state, linkingConfig.config); // Don't log the route transitions from OldDot because they contain authTokens - if (path.includes('/transition')) { + if (currentPath.includes('/transition')) { Log.info('Navigating from transition link from OldDot using short lived authToken'); } else { - Log.info('Navigating to route', false, {path}); + Log.info('Navigating to route', false, {path: currentPath}); } - App.setCurrentURL(path); + + this.setState({currentPath}); } render() { @@ -72,7 +76,7 @@ class NavigationRoot extends Component { enabled: false, }} > - + ); } diff --git a/src/libs/SelectionScraper/index.js b/src/libs/SelectionScraper/index.js index 7f0a9d69959d..99405259eaea 100644 --- a/src/libs/SelectionScraper/index.js +++ b/src/libs/SelectionScraper/index.js @@ -1,10 +1,9 @@ import render from 'dom-serializer'; -import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import {parseDocument} from 'htmlparser2'; import {Element} from 'domhandler'; import _ from 'underscore'; import Str from 'expensify-common/lib/str'; -import {isCommentTag} from '../../components/HTMLEngineProvider/htmlEngineUtils'; +import * as htmlEngineUtils from '../../components/HTMLEngineProvider/htmlEngineUtils'; const elementsWillBeSkipped = ['html', 'body']; const tagAttribute = 'data-testid'; @@ -14,70 +13,69 @@ const tagAttribute = 'data-testid'; * @returns {String} HTML of selection as String */ const getHTMLOfSelection = () => { - if (window.getSelection) { - const selection = window.getSelection(); - - if (selection.rangeCount > 0) { - const div = document.createElement('div'); - - // HTML tag of markdown comments is in data-testid attribute (em, strong, blockquote..). Our goal here is to - // find that nodes and replace that tag with the one inside data-testid, so ExpensiMark can parse it. - // Simply, we want to replace this: - // bold - // to this: - // bold - // - // We traverse all ranges, and get closest node with data-testid and replace its contents with contents of - // range. - for (let i = 0; i < selection.rangeCount; i++) { - const range = selection.getRangeAt(i); - - const clonedSelection = range.cloneContents(); - - // If clonedSelection has no text content this data has no meaning to us. - if (clonedSelection.textContent) { - let node = null; - - // If selection starts and ends within same text node we use its parentNode. This is because we can't - // use closest function on a [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) node. - // We are selecting closest node because nodes with data-testid can be one of the parents of the actual node. - // Assuming we selected only "block" part of following html: - //
- //
- // this is block code - //
- //
- // commonAncestorContainer: #text "this is block code" - // commonAncestorContainer.parentNode: - //
- // this is block code - //
- // and finally commonAncestorContainer.parentNode.closest('data-testid') is targeted dom. - if (range.commonAncestorContainer instanceof HTMLElement) { - node = range.commonAncestorContainer.closest(`[${tagAttribute}]`); - } else { - node = range.commonAncestorContainer.parentNode.closest(`[${tagAttribute}]`); - } - - // This means "range.commonAncestorContainer" is a text node. We simply get its parent node. - if (!node) { - node = range.commonAncestorContainer.parentNode; - } - - node = node.cloneNode(); - node.appendChild(clonedSelection); - div.appendChild(node); - } + // If browser doesn't support Selection API, return an empty string. + if (!window.getSelection) { + return ''; + } + const selection = window.getSelection(); + + if (selection.rangeCount <= 0) { + return window.getSelection().toString(); + } + + const div = document.createElement('div'); + + // HTML tag of markdown comments is in data-testid attribute (em, strong, blockquote..). Our goal here is to + // find that nodes and replace that tag with the one inside data-testid, so ExpensiMark can parse it. + // Simply, we want to replace this: + // bold + // to this: + // bold + // + // We traverse all ranges, and get closest node with data-testid and replace its contents with contents of + // range. + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + + const clonedSelection = range.cloneContents(); + + // If clonedSelection has no text content this data has no meaning to us. + if (clonedSelection.textContent) { + let node = null; + + // If selection starts and ends within same text node we use its parentNode. This is because we can't + // use closest function on a [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) node. + // We are selecting closest node because nodes with data-testid can be one of the parents of the actual node. + // Assuming we selected only "block" part of following html: + //
+ //
+ // this is block code + //
+ //
+ // commonAncestorContainer: #text "this is block code" + // commonAncestorContainer.parentNode: + //
+ // this is block code + //
+ // and finally commonAncestorContainer.parentNode.closest('data-testid') is targeted dom. + if (range.commonAncestorContainer instanceof HTMLElement) { + node = range.commonAncestorContainer.closest(`[${tagAttribute}]`); + } else { + node = range.commonAncestorContainer.parentNode.closest(`[${tagAttribute}]`); } - return div.innerHTML; - } + // This means "range.commonAncestorContainer" is a text node. We simply get its parent node. + if (!node) { + node = range.commonAncestorContainer.parentNode; + } - return window.getSelection().toString(); + node = node.cloneNode(); + node.appendChild(clonedSelection); + div.appendChild(node); + } } - // If browser doesn't support Selection API, returns empty string. - return ''; + return div.innerHTML; }; /** @@ -104,7 +102,7 @@ const replaceNodes = (dom) => { } // Adding a new line after each comment here, because adding after each range is not working for chrome. - if (isCommentTag(dom.attribs[tagAttribute])) { + if (htmlEngineUtils.isCommentTag(dom.attribs[tagAttribute])) { dom.children.push(new Element('br', {})); } } @@ -128,24 +126,17 @@ const replaceNodes = (dom) => { }; /** - * Reads html of selection, replaces with proper tags used for markdown, parses to markdown. - * @returns {String} parsed html as String + * Resolves the current selection to values and produces clean HTML. + * @returns {String} resolved selection in the HTML format */ -const getAsMarkdown = () => { - const selectionHtml = getHTMLOfSelection(); - - const domRepresentation = parseDocument(selectionHtml); - domRepresentation.children = _.map(domRepresentation.children, c => replaceNodes(c)); +const getCurrentSelection = () => { + const domRepresentation = parseDocument(getHTMLOfSelection()); + domRepresentation.children = _.map(domRepresentation.children, replaceNodes); const newHtml = render(domRepresentation); - - const parser = new ExpensiMark(); - - return parser.htmlToMarkdown(newHtml); + return newHtml || ''; }; -const SelectionScraper = { - getAsMarkdown, +export default { + getCurrentSelection, }; - -export default SelectionScraper; diff --git a/src/libs/SelectionScraper/index.native.js b/src/libs/SelectionScraper/index.native.js index 871a23988810..3872ece30b66 100644 --- a/src/libs/SelectionScraper/index.native.js +++ b/src/libs/SelectionScraper/index.native.js @@ -1,9 +1,4 @@ -/** - * This is a no-op component for native devices because they wouldn't be able to support Selection API like - * a website. - */ -const SelectionParser = { - getAsMarkdown: () => '', +export default { + // This is a no-op function for native devices because they wouldn't be able to support Selection API like a website. + getCurrentSelection: () => '', }; - -export default SelectionParser; diff --git a/src/libs/VisualViewport/index.js b/src/libs/VisualViewport/index.js new file mode 100644 index 000000000000..cc6be038209c --- /dev/null +++ b/src/libs/VisualViewport/index.js @@ -0,0 +1,16 @@ +/** + * Add a visual viewport resize listener if available. Return a function to remove the listener. + * + * @param {Function} onViewportResize + * @returns {Function} + */ +function addViewportResizeListener(onViewportResize) { + if (!window.visualViewport) { + return () => {}; + } + + window.visualViewport.addEventListener('resize', onViewportResize); + return () => window.visualViewport.removeEventListener('resize', onViewportResize); +} + +export default addViewportResizeListener; diff --git a/src/libs/VisualViewport/index.native.js b/src/libs/VisualViewport/index.native.js new file mode 100644 index 000000000000..823e3c1feef6 --- /dev/null +++ b/src/libs/VisualViewport/index.native.js @@ -0,0 +1,11 @@ + +/** + * Visual Viewport is not available on native, so return an empty function. + * + * @returns {Function} + */ +function addViewportResizeListener() { + return () => {}; +} + +export default addViewportResizeListener; diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index e9a044f5c280..b36edc546300 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -1,4 +1,5 @@ -import {AppState, Linking} from 'react-native'; +import moment from 'moment-timezone'; +import {AppState} from 'react-native'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; @@ -19,10 +20,12 @@ import ROUTES from '../../ROUTES'; import * as SessionUtils from '../SessionUtils'; let currentUserAccountID; +let currentUserEmail = ''; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { currentUserAccountID = lodashGet(val, 'accountID', ''); + currentUserEmail = lodashGet(val, 'email', ''); }, }); @@ -33,6 +36,12 @@ Onyx.connect({ initWithStoredValues: false, }); +let myPersonalDetails; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS, + callback: val => myPersonalDetails = val[currentUserEmail], +}); + const allPolicies = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, @@ -45,13 +54,6 @@ Onyx.connect({ }, }); -/** - * @param {String} url - */ -function setCurrentURL(url) { - Onyx.set(ONYXKEYS.CURRENT_URL, url); -} - /** * @param {String} locale */ @@ -156,13 +158,10 @@ function fixAccountAndReloadData() { } /** - * This action runs every time the AuthScreens are mounted. The navigator may - * not be ready yet, and therefore we need to wait before navigating within this - * action and any actions this method calls. + * This action runs when the Navigator is ready and the current route changes + * + * currentPath should be the path as reported by the NavigationContainer * - * getInitialURL allows us to access params from the transition link more easily - * than trying to extract them from the navigation state. - * The transition link contains an exitTo param that contains the route to * navigate to after the user is signed in. A user can transition from OldDot * with a different account than the one they are currently signed in with, so @@ -177,33 +176,69 @@ function fixAccountAndReloadData() { * pass it in as a parameter. withOnyx guarantees that the value has been read * from Onyx because it will not render the AuthScreens until that point. * @param {Object} session + * @param {string} currentPath */ -function setUpPoliciesAndNavigate(session) { - Linking.getInitialURL() - .then((url) => { - if (!url) { - return; - } - const path = new URL(url).pathname; - const params = new URLSearchParams(url); - const exitTo = params.get('exitTo'); - const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(url, session.email); - const shouldCreateFreePolicy = !isLoggingInAsNewUser - && Str.startsWith(path, Str.normalizeUrl(ROUTES.TRANSITION_FROM_OLD_DOT)) +function setUpPoliciesAndNavigate(session, currentPath) { + if (!session || !currentPath || !currentPath.includes('exitTo')) { + return; + } + + let exitTo; + try { + const url = new URL(currentPath, CONST.NEW_EXPENSIFY_URL); + exitTo = url.searchParams.get('exitTo'); + } catch (error) { + // URLSearchParams is unsupported on iOS so we catch th error and + // silence it here since this is primarily a Web flow + return; + } + + const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(currentPath, session.email); + const shouldCreateFreePolicy = !isLoggingInAsNewUser + && Str.startsWith(currentPath, Str.normalizeUrl(ROUTES.TRANSITION_FROM_OLD_DOT)) && exitTo === ROUTES.WORKSPACE_NEW; - if (shouldCreateFreePolicy) { - Policy.createAndGetPolicyList(); - return; - } - if (!isLoggingInAsNewUser && exitTo) { - Navigation.isNavigationReady() - .then(() => { - // We must call dismissModal() to remove the /transition route from history - Navigation.dismissModal(); - Navigation.navigate(exitTo); - }); - } - }); + if (shouldCreateFreePolicy) { + Policy.createAndGetPolicyList(); + return; + } + if (!isLoggingInAsNewUser && exitTo) { + // We must call dismissModal() to remove the /transition route from history + Navigation.dismissModal(); + Navigation.navigate(exitTo); + } +} + +function openProfile() { + const oldTimezoneData = myPersonalDetails.timezone || {}; + const newTimezoneData = { + automatic: lodashGet(oldTimezoneData, 'automatic', true), + selected: moment.tz.guess(true), + }; + + API.write('OpenProfile', { + timezone: JSON.stringify(newTimezoneData), + }, { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + timezone: newTimezoneData, + }, + }, + }], + failureData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + timezone: oldTimezoneData, + }, + }, + }], + }); + + Navigation.navigate(ROUTES.SETTINGS_PROFILE); } // When the app reconnects from being offline, fetch all initialization data @@ -213,11 +248,11 @@ NetworkConnection.onReconnect(() => { }); export { - setCurrentURL, setLocale, setSidebarLoaded, getAppData, fixAccountAndReloadData, setUpPoliciesAndNavigate, + openProfile, openApp, }; diff --git a/src/libs/actions/CloseAccount.js b/src/libs/actions/CloseAccount.js index ceb78aa68bf0..a42db97529ef 100644 --- a/src/libs/actions/CloseAccount.js +++ b/src/libs/actions/CloseAccount.js @@ -1,33 +1,14 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; -let isCloseAccountModalOpen; -Onyx.connect({ - key: ONYXKEYS.IS_CLOSE_ACCOUNT_MODAL_OPEN, - callback: flag => isCloseAccountModalOpen = flag, -}); - -/** - * Set CloseAccount flag to show modal - */ -function showCloseAccountModal() { - if (isCloseAccountModalOpen) { - return; - } - Onyx.set(ONYXKEYS.IS_CLOSE_ACCOUNT_MODAL_OPEN, true); -} - /** -* Unset CloseAccount flag to hide modal + * Clear CloseAccount error message to hide modal */ -function hideCloseAccountModal() { - if (!isCloseAccountModalOpen) { - return; - } - Onyx.set(ONYXKEYS.IS_CLOSE_ACCOUNT_MODAL_OPEN, false); +function clearError() { + Onyx.merge(ONYXKEYS.CLOSE_ACCOUNT, {error: ''}); } export { - showCloseAccountModal, - hideCloseAccountModal, + // eslint-disable-next-line import/prefer-default-export + clearError, }; diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index dc9ad432fd56..7b3369403e01 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -103,6 +103,34 @@ function getPaymentMethods() { }); } +function openPaymentsPage() { + const onyxData = { + optimisticData: [ + { + onyxMethod: 'merge', + key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS, + value: true, + }, + ], + successData: [ + { + onyxMethod: 'merge', + key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS, + value: false, + }, + ], + failureData: [ + { + onyxMethod: 'merge', + key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS, + value: false, + }, + ], + }; + + return API.read('OpenPaymentsPage', {}, onyxData); +} + /** * Sets the default bank account or debit card for an Expensify Wallet * @@ -215,7 +243,6 @@ function transferWalletBalance(paymentMethod) { : CONST.PAYMENT_METHOD_ID_KEYS.DEBIT_CARD; const parameters = { [paymentMethodIDKey]: paymentMethod.methodID, - shouldReturnOnyxData: true, }; API.write('TransferWalletBalance', parameters, { @@ -288,6 +315,7 @@ export { deleteDebitCard, deletePayPalMe, getPaymentMethods, + openPaymentsPage, makeDefaultPaymentMethod, addBillingCard, kycWallRef, diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index bcb939a1b2f1..4f40b1724429 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -262,6 +262,48 @@ function setPersonalDetails(details, shouldGrowl) { }); } +function updateProfile(firstName, lastName, pronouns, timezone) { + const myPersonalDetails = personalDetails[currentUserEmail]; + API.write('UpdateProfile', { + // 'details' is an old param that will be removed in https://github.com/Expensify/Expensify/issues/220321 + details: JSON.stringify({firstName, lastName, pronouns}), + firstName, + lastName, + pronouns, + timezone: JSON.stringify(timezone), + }, { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + firstName, + lastName, + pronouns, + timezone, + displayName: getDisplayName(currentUserEmail, { + firstName, + lastName, + }), + }, + }, + }], + failureData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + firstName: myPersonalDetails.firstName, + lastName: myPersonalDetails.lastName, + pronouns: myPersonalDetails.pronouns, + timezone: myPersonalDetails.timeZone, + displayName: myPersonalDetails.displayName, + }, + }, + }], + }); +} + /** * Fetches the local currency based on location and sets currency code/symbol to Onyx */ @@ -295,14 +337,30 @@ function setAvatar(file) { /** * Replaces the user's avatar image with a default avatar - * - * @param {String} defaultAvatarURL */ -function deleteAvatar(defaultAvatarURL) { - // We don't want to save the default avatar URL in the backend since we don't want to allow - // users the option of removing the default avatar, instead we'll save an empty string - DeprecatedAPI.PersonalDetails_Update({details: JSON.stringify({avatar: ''})}); - mergeLocalPersonalDetails({avatar: defaultAvatarURL}); +function deleteAvatar() { + const defaultAvatar = ReportUtils.getDefaultAvatar(currentUserEmail); + + API.write('DeleteUserAvatar', {}, { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + avatar: defaultAvatar, + }, + }, + }], + failureData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + avatar: personalDetails[currentUserEmail].avatar, + }, + }, + }], + }); } export { @@ -315,4 +373,5 @@ export { openIOUModalPage, getMaxCharacterError, extractFirstAndLastNameFromAvailableDetails, + updateProfile, }; diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 40b96e408565..9470ea25f4e0 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -238,7 +238,6 @@ function createAndGetPolicyList() { newPolicyID = policyID; return getPolicyList(); }) - .then(Navigation.isNavigationReady) .then(() => { Navigation.dismissModal(); navigateToPolicy(newPolicyID); @@ -540,7 +539,34 @@ function subscribeToPolicyEvents() { } /** - * Checks if we have any errors stored within the POLICY_MEMBER_LIST. Determines whether we should show a red brick road error or not + * Removes an error after trying to delete a member + * + * @param {String} policyID + * @param {String} memberEmail + */ +function clearDeleteMemberError(policyID, memberEmail) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, { + [memberEmail]: { + pendingAction: null, + errors: null, + }, + }); +} + +/** + * Removes an error after trying to add a member + * + * @param {String} policyID + * @param {String} memberEmail + */ +function clearAddMemberError(policyID, memberEmail) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, { + [memberEmail]: null, + }); +} + +/** +* Checks if we have any errors stored within the POLICY_MEMBER_LIST. Determines whether we should show a red brick road error or not * Data structure: {email: {role:'bla', errors: []}, email2: {role:'bla', errors: [{1231312313: 'Unable to do X'}]}, ...} * @param {Object} policyMemberList * @returns {Boolean} @@ -567,5 +593,7 @@ export { setCustomUnitRate, updateLastAccessedWorkspace, subscribeToPolicyEvents, + clearDeleteMemberError, + clearAddMemberError, hasPolicyMemberError, }; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 4f22eb497350..d41fcf3ef974 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -12,10 +12,8 @@ import ROUTES from '../../ROUTES'; import * as Pusher from '../Pusher/pusher'; import Log from '../Log'; import NetworkConnection from '../NetworkConnection'; -import redirectToSignIn from './SignInRedirect'; import Growl from '../Growl'; import * as Localize from '../Localize'; -import * as CloseAccountActions from './CloseAccount'; import * as Link from './Link'; import getSkinToneEmojiFromIndex from '../../components/EmojiPicker/getSkinToneEmojiFromIndex'; import * as SequentialQueue from '../Network/SequentialQueue'; @@ -78,17 +76,23 @@ function updatePassword(oldPassword, password) { * @param {String} message optional reason for closing account */ function closeAccount(message) { - DeprecatedAPI.User_Delete({message}).then((response) => { - console.debug('User_Delete: ', JSON.stringify(response)); - - if (response.jsonCode === 200) { - Growl.show(Localize.translateLocal('closeAccountPage.closeAccountSuccess'), CONST.GROWL.SUCCESS); - redirectToSignIn(); - return; - } - - // Inform user that they are currently unable to close their account - CloseAccountActions.showCloseAccountModal(); + // Note: successData does not need to set isLoading to false because if the CloseAccount + // command succeeds, a Pusher response will clear all Onyx data. + API.write('CloseAccount', {message}, { + optimisticData: [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.CLOSE_ACCOUNT, + value: {isLoading: true}, + }, + ], + failureData: [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.CLOSE_ACCOUNT, + value: {isLoading: false}, + }, + ], }); } diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index 5042475d22b9..ebbff2141e26 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -341,16 +341,6 @@ function SetPassword(parameters) { return Network.post(commandName, parameters); } -/** - * @param {Object} parameters - * @param {String} parameters.message - * @returns {Promise} - */ -function User_Delete(parameters) { - const commandName = 'User_Delete'; - return Network.post(commandName, parameters); -} - /** * @param {Object} parameters * @param {String} parameters.email @@ -749,7 +739,6 @@ export { SetPassword, UpdatePolicy, User_SignUp, - User_Delete, User_IsUsingExpensifyCard, User_ReopenAccount, User_SecondaryLogin_Send, diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index 71daa5f13f72..d2ab86fccc9e 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -3,9 +3,7 @@ import _ from 'underscore'; import React from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import { - View, KeyboardAvoidingView, -} from 'react-native'; +import {View} from 'react-native'; import IdologyQuestions from './IdologyQuestions'; import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; @@ -254,19 +252,17 @@ class AdditionalDetailsStep extends React.Component { render() { if (!_.isEmpty(this.props.walletAdditionalDetails.questions)) { return ( - - - Navigation.dismissModal()} - shouldShowBackButton - onBackButtonPress={() => Wallet.setAdditionalDetailsQuestions(null)} - /> - - + + Navigation.dismissModal()} + shouldShowBackButton + onBackButtonPress={() => Wallet.setAdditionalDetailsQuestions(null)} + /> + ); } @@ -277,127 +273,125 @@ class AdditionalDetailsStep extends React.Component { const {firstName, lastName} = PersonalDetails.extractFirstAndLastNameFromAvailableDetails(this.props.currentUserPersonalDetails); return ( - - - Navigation.dismissModal()} - /> - - - {this.props.translate('additionalDetailsStep.helpText')} - - {this.props.translate('additionalDetailsStep.helpLink')} - - - this.form = el}> - - - this.clearErrorAndSetValue('legalFirstName', val)} - value={this.props.walletAdditionalDetailsDraft.legalFirstName || firstName} - errorText={this.getErrorText('legalFirstName')} - /> - this.clearErrorAndSetValue('legalLastName', val)} - value={this.props.walletAdditionalDetailsDraft.legalLastName || lastName} - errorText={this.getErrorText('legalLastName')} - /> - { - const renamedFields = { - street: 'addressStreet', - state: 'addressState', - zipCode: 'addressZip', - city: 'addressCity', - }; - _.each(values, (value, inputKey) => { - const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); - this.clearErrorAndSetValue(renamedInputKey, value); - }); - }} - errorText={this.getErrorText('addressStreet')} - hint={this.props.translate('common.noPO')} - /> - {this.props.walletAdditionalDetailsDraft.addressStreet ? ( - <> - {/** Once the user has started entering his address, show the other address fields (city, state, zip) */} - {/** We'll autofill them when the user selects a full address from the google autocomplete */} - this.clearErrorAndSetValue('addressCity', val)} - value={this.props.walletAdditionalDetailsDraft.addressCity || ''} - errorText={this.getErrorText('addressCity')} - /> - this.clearErrorAndSetValue('addressState', val)} - value={this.props.walletAdditionalDetailsDraft.addressState || ''} - errorText={this.getErrorText('addressState')} - /> - this.clearErrorAndSetValue('addressZip', val)} - value={this.props.walletAdditionalDetailsDraft.addressZip || ''} - errorText={this.getErrorText('addressZip')} - /> - - ) : null} - + + Navigation.dismissModal()} + /> + + + {this.props.translate('additionalDetailsStep.helpText')} + + {this.props.translate('additionalDetailsStep.helpLink')} + + + this.form = el}> + + this.clearErrorAndSetValue('phoneNumber', val)} - value={this.props.walletAdditionalDetailsDraft.phoneNumber || ''} - placeholder={this.props.translate('common.phoneNumberPlaceholder')} - errorText={this.getErrorText('phoneNumber')} + label={this.props.translate(this.fieldNameTranslationKeys.legalFirstName)} + onChangeText={val => this.clearErrorAndSetValue('legalFirstName', val)} + value={this.props.walletAdditionalDetailsDraft.legalFirstName || firstName} + errorText={this.getErrorText('legalFirstName')} /> - this.clearDateErrorsAndSetValue(val)} - defaultValue={this.props.walletAdditionalDetailsDraft.dob || ''} - placeholder={this.props.translate('common.dob')} - errorText={this.getErrorText('dob') || this.getErrorText('age')} - maximumDate={new Date()} + label={this.props.translate(this.fieldNameTranslationKeys.legalLastName)} + onChangeText={val => this.clearErrorAndSetValue('legalLastName', val)} + value={this.props.walletAdditionalDetailsDraft.legalLastName || lastName} + errorText={this.getErrorText('legalLastName')} /> - this.clearSSNErrorAndSetValue(val)} - value={this.props.walletAdditionalDetailsDraft.ssn || ''} - errorText={this.getErrorText('ssnFull9') || this.getErrorText('ssn')} - maxLength={shouldAskForFullSSN ? 9 : 4} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + onInputChange={(values) => { + const renamedFields = { + street: 'addressStreet', + state: 'addressState', + zipCode: 'addressZip', + city: 'addressCity', + }; + _.each(values, (value, inputKey) => { + const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); + this.clearErrorAndSetValue(renamedInputKey, value); + }); + }} + errorText={this.getErrorText('addressStreet')} + hint={this.props.translate('common.noPO')} /> + {this.props.walletAdditionalDetailsDraft.addressStreet ? ( + <> + {/** Once the user has started entering his address, show the other address fields (city, state, zip) */} + {/** We'll autofill them when the user selects a full address from the google autocomplete */} + this.clearErrorAndSetValue('addressCity', val)} + value={this.props.walletAdditionalDetailsDraft.addressCity || ''} + errorText={this.getErrorText('addressCity')} + /> + this.clearErrorAndSetValue('addressState', val)} + value={this.props.walletAdditionalDetailsDraft.addressState || ''} + errorText={this.getErrorText('addressState')} + /> + this.clearErrorAndSetValue('addressZip', val)} + value={this.props.walletAdditionalDetailsDraft.addressZip || ''} + errorText={this.getErrorText('addressZip')} + /> + + ) : null} - { - this.form.scrollTo({y: 0, animated: true}); - }} - message={this.props.walletAdditionalDetails.additionalErrorMessage} - isLoading={this.props.walletAdditionalDetails.loading} - buttonText={this.props.translate('common.saveAndContinue')} + this.clearErrorAndSetValue('phoneNumber', val)} + value={this.props.walletAdditionalDetailsDraft.phoneNumber || ''} + placeholder={this.props.translate('common.phoneNumberPlaceholder')} + errorText={this.getErrorText('phoneNumber')} /> - - - + this.clearDateErrorsAndSetValue(val)} + defaultValue={this.props.walletAdditionalDetailsDraft.dob || ''} + placeholder={this.props.translate('common.dob')} + errorText={this.getErrorText('dob') || this.getErrorText('age')} + maximumDate={new Date()} + /> + this.clearSSNErrorAndSetValue(val)} + value={this.props.walletAdditionalDetailsDraft.ssn || ''} + errorText={this.getErrorText('ssnFull9') || this.getErrorText('ssn')} + maxLength={shouldAskForFullSSN ? 9 : 4} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + /> + + { + this.form.scrollTo({y: 0, animated: true}); + }} + message={this.props.walletAdditionalDetails.additionalErrorMessage} + isLoading={this.props.walletAdditionalDetails.loading} + buttonText={this.props.translate('common.saveAndContinue')} + /> + + ); } diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index f1ac80846a05..53f25dfeffd7 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -1,7 +1,6 @@ import _ from 'underscore'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import {KeyboardAvoidingView} from 'react-native'; import ScreenWrapper from '../../components/ScreenWrapper'; import * as BankAccounts from '../../libs/actions/BankAccounts'; import ONYXKEYS from '../../ONYXKEYS'; @@ -59,14 +58,12 @@ class EnablePaymentsPage extends React.Component { if (this.props.userWallet.shouldShowFailedKYC) { return ( - - - Navigation.dismissModal()} - /> - - + + Navigation.dismissModal()} + /> + ); } diff --git a/src/pages/GetAssistancePage.js b/src/pages/GetAssistancePage.js index 030ea26bac8a..4a07075b0d3f 100644 --- a/src/pages/GetAssistancePage.js +++ b/src/pages/GetAssistancePage.js @@ -3,7 +3,6 @@ import {View} from 'react-native'; import PropTypes from 'prop-types'; import ScreenWrapper from '../components/ScreenWrapper'; import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; -import KeyboardAvoidingView from '../components/KeyboardAvoidingView'; import HeaderWithCloseButton from '../components/HeaderWithCloseButton'; import Section from '../components/Section'; import Navigation from '../libs/Navigation/Navigation'; @@ -28,36 +27,34 @@ const propTypes = { const GetAssistancePage = props => ( - - Navigation.dismissModal(true)} - shouldShowBackButton - onBackButtonPress={() => Navigation.goBack()} - /> -
Report.navigateToConciergeChat(), - icon: Expensicons.ChatBubble, - shouldShowRightIcon: true, - }, - { - title: props.translate('getAssistancePage.requestSetupCall'), - onPress: () => Navigation.navigate(ROUTES.getRequestCallRoute(props.route.params.taskID)), - icon: Expensicons.Phone, - shouldShowRightIcon: true, - }, - ]} - > - - {props.translate('getAssistancePage.description')} - -
-
+ Navigation.dismissModal(true)} + shouldShowBackButton + onBackButtonPress={() => Navigation.goBack()} + /> +
Report.navigateToConciergeChat(), + icon: Expensicons.ChatBubble, + shouldShowRightIcon: true, + }, + { + title: props.translate('getAssistancePage.requestSetupCall'), + onPress: () => Navigation.navigate(ROUTES.getRequestCallRoute(props.route.params.taskID)), + icon: Expensicons.Phone, + shouldShowRightIcon: true, + }, + ]} + > + + {props.translate('getAssistancePage.description')} + +
); diff --git a/src/pages/LogOutPreviousUserPage.js b/src/pages/LogOutPreviousUserPage.js index a68f8af34056..d9d36602721d 100644 --- a/src/pages/LogOutPreviousUserPage.js +++ b/src/pages/LogOutPreviousUserPage.js @@ -6,7 +6,6 @@ import {withOnyx} from 'react-native-onyx'; import ONYXKEYS from '../ONYXKEYS'; import * as Session from '../libs/actions/Session'; import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator'; -import Navigation from '../libs/Navigation/Navigation'; import * as SessionUtils from '../libs/SessionUtils'; const propTypes = { @@ -25,16 +24,7 @@ class LogOutPreviousUserPage extends Component { const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL, sessionEmail); if (isLoggingInAsNewUser) { Session.signOutAndRedirectToSignIn(); - return; } - - // Since we conditionally render navigators in the AppNavigator, when we - // sign out and sign back in there will be a moment where no navigator - // is rendered and the navigation state is null. We can't navigate at - // that time, so we use a promise to delay transition navigation until - // it is ready. We set the navigation ready here since we know that the - // navigator is now rendered. - Navigation.setIsNavigationReady(); }); } diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index 0e55bdbccfbc..2c8a0231c8cb 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -16,7 +16,6 @@ import ScreenWrapper from '../components/ScreenWrapper'; import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator'; import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import compose from '../libs/compose'; -import KeyboardAvoidingView from '../components/KeyboardAvoidingView'; import personalDetailsPropType from './personalDetailsPropType'; const propTypes = { @@ -211,9 +210,9 @@ class NewChatPage extends Component { maxParticipantsReached, ); return ( - + {({didScreenTransitionEnd}) => ( - + <> )} - + )} ); diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index 5f4ac31f65d4..711dfeaadada 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -46,12 +46,19 @@ const propTypes = { /** During the OAuth flow we need to use the plaidLink token that we initially connected with */ plaidLinkOAuthToken: PropTypes.string, + /** Object with various information about the user */ + user: PropTypes.shape({ + /** Is the user account validated? */ + validated: PropTypes.bool, + }), + ...withLocalizePropTypes, }; const defaultProps = { receivedRedirectURI: null, plaidLinkOAuthToken: '', + user: {}, }; class BankAccountStep extends React.Component { @@ -180,7 +187,7 @@ class BankAccountStep extends React.Component { const bankAccountRoute = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${ROUTES.BANK_ACCOUNT}`; const error = lodashGet(this.props, 'reimbursementAccount.error', ''); const loading = lodashGet(this.props, 'reimbursementAccount.loading', false); - + const validated = lodashGet(this.props, 'user.validated', false); return ( BankAccounts.setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID)} - disabled={this.props.isPlaidDisabled || !this.props.user.validated} + disabled={this.props.isPlaidDisabled || !validated} style={[styles.mt5, styles.mh3]} iconStyles={[styles.mr5]} shouldShowRightIcon @@ -234,11 +241,11 @@ class BankAccountStep extends React.Component { BankAccounts.setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL)} shouldShowRightIcon /> - {!this.props.user.validated && ( + {!validated && ( diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index 21a15777b738..9f022bd00bc4 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -14,7 +14,6 @@ import CONST from '../../CONST'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; import styles from '../../styles/styles'; -import KeyboardAvoidingView from '../../components/KeyboardAvoidingView'; import getPlaidOAuthReceivedRedirectURI from '../../libs/getPlaidOAuthReceivedRedirectURI'; import Text from '../../components/Text'; import {withNetwork} from '../../components/OnyxProvider'; @@ -219,34 +218,32 @@ class ReimbursementAccountPage extends React.Component { } return ( - - {currentStep === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT && ( - - )} - {currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && ( - - )} - {currentStep === CONST.BANK_ACCOUNT.STEP.REQUESTOR && ( - - )} - {currentStep === CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT && ( - - )} - {currentStep === CONST.BANK_ACCOUNT.STEP.VALIDATION && ( - - )} - {currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE && ( - - )} - - + {currentStep === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT && ( + + )} + {currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && ( + + )} + {currentStep === CONST.BANK_ACCOUNT.STEP.REQUESTOR && ( + + )} + {currentStep === CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT && ( + + )} + {currentStep === CONST.BANK_ACCOUNT.STEP.VALIDATION && ( + + )} + {currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE && ( + + )} + ); } diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js index 7440780e3269..d011fc7e8389 100644 --- a/src/pages/ReportSettingsPage.js +++ b/src/pages/ReportSettingsPage.js @@ -174,7 +174,7 @@ class ReportSettingsPage extends Component { title={this.props.translate('common.settings')} shouldShowBackButton onBackButtonPress={() => Navigation.goBack()} - onCloseButtonPress={() => Navigation.dismissModal(true)} + onCloseButtonPress={() => Navigation.dismissModal()} /> diff --git a/src/pages/RequestCallPage.js b/src/pages/RequestCallPage.js index cbfd52a4390c..d2b8fa817e8c 100644 --- a/src/pages/RequestCallPage.js +++ b/src/pages/RequestCallPage.js @@ -23,7 +23,6 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, import TextInput from '../components/TextInput'; import Text from '../components/Text'; import Section from '../components/Section'; -import KeyboardAvoidingView from '../components/KeyboardAvoidingView'; import * as Illustrations from '../components/Icon/Illustrations'; import * as Expensicons from '../components/Icon/Expensicons'; import * as LoginUtils from '../libs/LoginUtils'; @@ -285,86 +284,84 @@ class RequestCallPage extends Component { return ( - - Navigation.goBack()} - onCloseButtonPress={() => Navigation.dismissModal(true)} - /> - {this.props.requestCallForm.didRequestCallSucceed - ? ( - - ) : ( - <> - - -
- - {this.props.translate('requestCallPage.description')} - - this.setState({firstName})} - onChangeLastName={lastName => this.setState({lastName})} - style={[styles.mv4]} - /> - this.setState({phoneNumber})} - /> - this.setState({phoneExtension})} - containerStyles={[styles.mt4]} - /> - {this.getWaitTimeMessage()} -
-
-
- - {isBlockedFromConcierge && ( - - - {this.props.translate('requestCallPage.blockedFromConcierge')} - - )} - {!_.isEmpty(this.props.requestCallForm.error) && ( - - {this.props.requestCallForm.error} + Navigation.goBack()} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + {this.props.requestCallForm.didRequestCallSucceed + ? ( + + ) : ( + <> + + +
+ + {this.props.translate('requestCallPage.description')} - )} - - - - )} - + this.setState({firstName})} + onChangeLastName={lastName => this.setState({lastName})} + style={[styles.mv4]} + /> + this.setState({phoneNumber})} + /> + this.setState({phoneExtension})} + containerStyles={[styles.mt4]} + /> + {this.getWaitTimeMessage()} +
+
+
+ + {isBlockedFromConcierge && ( + + + {this.props.translate('requestCallPage.blockedFromConcierge')} + + )} + {!_.isEmpty(this.props.requestCallForm.error) && ( + + {this.props.requestCallForm.error} + + )} + + + + )}
); } diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 9a5756240e50..3f8534ca2b5f 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -7,7 +7,6 @@ import OptionsSelector from '../components/OptionsSelector'; import * as OptionsListUtils from '../libs/OptionsListUtils'; import ONYXKEYS from '../ONYXKEYS'; import styles from '../styles/styles'; -import KeyboardSpacer from '../components/KeyboardSpacer'; import Navigation from '../libs/Navigation/Navigation'; import ROUTES from '../ROUTES'; import withWindowDimensions, {windowDimensionsPropTypes} from '../components/withWindowDimensions'; @@ -189,7 +188,6 @@ class SearchPage extends Component { shouldShowOptions={didScreenTransitionEnd} />
- )}
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index fd9282a88021..045cf3af9271 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -16,13 +16,16 @@ import Permissions from '../../libs/Permissions'; import * as ReportUtils from '../../libs/ReportUtils'; import ReportActionsView from './report/ReportActionsView'; import ReportActionCompose from './report/ReportActionCompose'; -import KeyboardAvoidingView from '../../components/KeyboardAvoidingView'; import SwipeableView from '../../components/SwipeableView'; import CONST from '../../CONST'; import ReportActionsSkeletonView from '../../components/ReportActionsSkeletonView'; import reportActionPropTypes from './report/reportActionPropTypes'; import ArchivedReportFooter from '../../components/ArchivedReportFooter'; import toggleReportActionComposeView from '../../libs/toggleReportActionComposeView'; +import addViewportResizeListener from '../../libs/VisualViewport'; +import {withNetwork} from '../../components/OnyxProvider'; +import compose from '../../libs/compose'; +import networkPropTypes from '../../components/networkPropTypes'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -77,6 +80,9 @@ const propTypes = { /** The type of the policy */ type: PropTypes.string, })).isRequired, + + /** Information about the network */ + network: networkPropTypes.isRequired, }; const defaultProps = { @@ -113,7 +119,8 @@ class ReportScreen extends React.Component { super(props); this.onSubmitComment = this.onSubmitComment.bind(this); - this.viewportOffsetTop = this.updateViewportOffsetTop.bind(this); + this.updateViewportOffsetTop = this.updateViewportOffsetTop.bind(this); + this.removeViewportResizeListener = () => {}; this.state = { skeletonViewContainerHeight: 0, @@ -123,9 +130,7 @@ class ReportScreen extends React.Component { componentDidMount() { this.storeCurrentlyViewedReport(); - if (window.visualViewport) { - window.visualViewport.addEventListener('resize', this.viewportOffsetTop); - } + this.removeViewportResizeListener = addViewportResizeListener(this.updateViewportOffsetTop); } componentDidUpdate(prevProps) { @@ -137,9 +142,7 @@ class ReportScreen extends React.Component { componentWillUnmount() { clearTimeout(this.loadingTimerId); - if (window.visualViewport) { - window.visualViewport.removeEventListener('resize', this.viewportOffsetTop); - } + this.removeViewportResizeListener(); } /** @@ -149,12 +152,8 @@ class ReportScreen extends React.Component { Report.addComment(getReportID(this.props.route), text); } - /** - * @param {SyntheticEvent} e - */ - updateViewportOffsetTop(e) { - const viewportOffsetTop = lodashGet(e, 'target.offsetTop', 0); - this.setState({viewportOffsetTop}); + setChatFooterStyles(isOffline) { + return {...styles.chatFooter, minHeight: !isOffline ? CONST.CHAT_FOOTER_MIN_HEIGHT : 0}; } /** @@ -181,6 +180,14 @@ class ReportScreen extends React.Component { Report.updateCurrentlyViewedReportID(reportID); } + /** + * @param {SyntheticEvent} e + */ + updateViewportOffsetTop(e) { + const viewportOffsetTop = lodashGet(e, 'target.offsetTop', 0); + this.setState({viewportOffsetTop}); + } + render() { if (!this.props.isSidebarLoaded) { return null; @@ -206,57 +213,55 @@ class ReportScreen extends React.Component { } return ( - - Navigation.navigate(ROUTES.HOME)} - /> - - this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} - > - {this.shouldShowLoader() - ? ( - - ) - : ( - - )} - {(isArchivedRoom || this.props.session.shouldShowComposeInput) && ( - - { - isArchivedRoom - ? ( - Navigation.navigate(ROUTES.HOME)} + /> + + this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} + > + {this.shouldShowLoader() + ? ( + + ) + : ( + + )} + {(isArchivedRoom || this.props.session.shouldShowComposeInput) && ( + + { + isArchivedRoom + ? ( + + ) : ( + + - ) : ( - - - - ) - } - - )} - - + + ) + } + + )} + ); } @@ -265,7 +270,7 @@ class ReportScreen extends React.Component { ReportScreen.propTypes = propTypes; ReportScreen.defaultProps = defaultProps; -export default withOnyx({ +export default compose(withNetwork(), withOnyx({ isSidebarLoaded: { key: ONYXKEYS.IS_SIDEBAR_LOADED, }, @@ -292,4 +297,4 @@ export default withOnyx({ policies: { key: ONYXKEYS.COLLECTION.POLICY, }, -})(ReportScreen); +}))(ReportScreen); diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 635721245c4a..2823d7b31a89 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -1,5 +1,5 @@ -import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import _ from 'underscore'; +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import lodashGet from 'lodash/get'; import * as Expensicons from '../../../../components/Icon/Expensicons'; import * as Report from '../../../../libs/actions/Report'; @@ -97,20 +97,23 @@ export default [ // the `text` and `icon` onPress: (closePopover, {reportAction, selection}) => { const message = _.last(lodashGet(reportAction, 'message', [{}])); - const html = lodashGet(message, 'html', ''); - - const parser = new ExpensiMark(); - const reportMarkdown = parser.htmlToMarkdown(html); - - const text = selection || reportMarkdown; + const messageHtml = lodashGet(message, 'html', ''); const isAttachment = _.has(reportAction, 'isAttachment') ? reportAction.isAttachment : ReportUtils.isReportMessageAttachment(message); if (!isAttachment) { - Clipboard.setString(text); + const content = selection || messageHtml; + if (content) { + const parser = new ExpensiMark(); + if (!Clipboard.canSetHtml()) { + Clipboard.setString(parser.htmlToMarkdown(content)); + } else { + Clipboard.setHtml(content, parser.htmlToText(content)); + } + } } else { - Clipboard.setString(html); + Clipboard.setString(messageHtml); } if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index b77599ad9bea..ef0449806415 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -103,7 +103,7 @@ class PopoverReportActionContextMenu extends React.Component { * * @param {string} type - context menu type [EMAIL, LINK, REPORT_ACTION] * @param {Object} [event] - A press event. - * @param {string} [selection] - A copy text. + * @param {String} [selection] - Copied content. * @param {Element} contextMenuAnchor - popoverAnchor * @param {Number} reportID - Active Report Id * @param {Object} reportAction - ReportAction for ContextMenu diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js index 8952287fa850..aa71bcc9aba5 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js @@ -7,7 +7,7 @@ const contextMenuRef = React.createRef(); * * @param {string} type - the context menu type to display [EMAIL, LINK, REPORT_ACTION] * @param {Object} [event] - A press event. - * @param {string} [selection] - A copy text. + * @param {String} [selection] - Copied content. * @param {Element} contextMenuAnchor - popoverAnchor * @param {Number} reportID - Active Report Id * @param {Object} reportAction - ReportAction for ContextMenu diff --git a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js index 933bb4582af4..cb6c759b17df 100644 --- a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js +++ b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js @@ -15,7 +15,7 @@ const propTypes = { /** Controls the visibility of this component. */ isVisible: PropTypes.bool, - /** The copy selection of text. */ + /** The copy selection. */ selection: PropTypes.string, /** Draft message - if this is set the comment is in 'edit' mode */ diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 6e58ae1f5715..51791c723367 100755 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -496,7 +496,11 @@ class ReportActionCompose extends React.Component { const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; return ( - + {shouldShowReportRecipientLocalTime && } - - + + {!this.props.isSmallScreenWidth && } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f36440dcabaf..948cac72c2e2 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -28,6 +28,7 @@ import {withNetwork, withReportActionsDrafts} from '../../../components/OnyxProv import RenameAction from '../../../components/ReportActionItem/RenameAction'; import InlineSystemMessage from '../../../components/InlineSystemMessage'; import styles from '../../../styles/styles'; +import SelectionScraper from '../../../libs/SelectionScraper'; import * as User from '../../../libs/actions/User'; import * as ReportUtils from '../../../libs/ReportUtils'; @@ -98,13 +99,13 @@ class ReportActionItem extends Component { * Show the ReportActionContextMenu modal popover. * * @param {Object} [event] - A press event. - * @param {string} [selection] - A copy text. */ - showPopover(event, selection) { + showPopover(event) { // Block menu on the message being Edited if (this.props.draftMessage) { return; } + const selection = SelectionScraper.getCurrentSelection(); ReportActionContextMenu.showContextMenu( ContextMenuActions.CONTEXT_MENU_TYPES.REPORT_ACTION, event, diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 424353b09ca6..5ef4b3603808 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -103,7 +103,7 @@ class ReportActionsList extends React.Component { const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; const availableHeight = this.props.windowHeight - - (styles.chatFooter.minHeight + variables.contentHeaderHeight); + - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); return Math.ceil(availableHeight / minimumReportActionHeight); } diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 0ba4e2463b2e..2ee31974690d 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -67,7 +67,7 @@ class ReportTypingIndicator extends React.Component { leadingText={PersonalDetails.getDisplayName(this.state.usersTyping[0])} trailingText={` ${this.props.translate('reportTypingIndicator.isTyping')}`} textStyle={[styles.chatItemComposeSecondaryRowSubText]} - wrapperStyle={styles.chatItemComposeSecondaryRow} + wrapperStyle={[styles.chatItemComposeSecondaryRow]} leadingTextParentStyle={styles.chatItemComposeSecondaryRowOffset} /> ); @@ -78,6 +78,7 @@ class ReportTypingIndicator extends React.Component { style={[ styles.chatItemComposeSecondaryRowSubText, styles.chatItemComposeSecondaryRowOffset, + ...this.props.containerStyles, ]} numberOfLines={1} > diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index b8b408b4c815..d0f9861ee819 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -10,7 +10,6 @@ import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import compose from '../../libs/compose'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import KeyboardAvoidingView from '../../components/KeyboardAvoidingView'; import * as IOU from '../../libs/actions/IOU'; import * as CurrencySymbolUtils from '../../libs/CurrencySymbolUtils'; import {withNetwork} from '../../components/OnyxProvider'; @@ -117,21 +116,19 @@ class IOUCurrencySelection extends Component { const headerMessage = this.state.searchValue.trim() && !this.state.currencyData.length ? this.props.translate('common.noResultsFound') : ''; return ( - - - - + + ); } diff --git a/src/pages/iou/IOUModal.js b/src/pages/iou/IOUModal.js index 14a24b473aad..508456ebe9a4 100755 --- a/src/pages/iou/IOUModal.js +++ b/src/pages/iou/IOUModal.js @@ -23,7 +23,6 @@ import AnimatedStep from '../../components/AnimatedStep'; import ScreenWrapper from '../../components/ScreenWrapper'; import Tooltip from '../../components/Tooltip'; import CONST from '../../CONST'; -import KeyboardAvoidingView from '../../components/KeyboardAvoidingView'; import * as PersonalDetails from '../../libs/actions/PersonalDetails'; import withCurrentUserPersonalDetails from '../../components/withCurrentUserPersonalDetails'; import ROUTES from '../../ROUTES'; @@ -174,7 +173,7 @@ class IOUModal extends Component { * Decides our animation type based on whether we're increasing or decreasing * our step index. * @returns {String} - */ + */ getDirection() { if (this.state.previousStepIndex < this.state.currentStepIndex) { return 'in'; @@ -358,7 +357,7 @@ class IOUModal extends Component { return ( {({didScreenTransitionEnd}) => ( - + <> )} - + )} ); diff --git a/src/pages/policyMemberPropType.js b/src/pages/policyMemberPropType.js index 0e5c39e02369..22a4d355fbfb 100644 --- a/src/pages/policyMemberPropType.js +++ b/src/pages/policyMemberPropType.js @@ -9,4 +9,7 @@ export default PropTypes.shape({ * {: 'error message', : 'error message 2'} */ errors: PropTypes.objectOf(PropTypes.string), + + /** Is this action pending? */ + pendingAction: PropTypes.string, }); diff --git a/src/pages/settings/AddSecondaryLoginPage.js b/src/pages/settings/AddSecondaryLoginPage.js index 7a15526de572..afffdb7cfdbd 100755 --- a/src/pages/settings/AddSecondaryLoginPage.js +++ b/src/pages/settings/AddSecondaryLoginPage.js @@ -14,7 +14,6 @@ import ONYXKEYS from '../../ONYXKEYS'; import Button from '../../components/Button'; import ROUTES from '../../ROUTES'; import CONST from '../../CONST'; -import KeyboardAvoidingView from '../../components/KeyboardAvoidingView'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; import FixedFooter from '../../components/FixedFooter'; @@ -101,62 +100,60 @@ class AddSecondaryLoginPage extends Component { this.phoneNumberInputRef.focus(); }} > - - Navigation.navigate(ROUTES.SETTINGS_PROFILE)} - onCloseButtonPress={() => Navigation.dismissModal()} - /> - - - {this.props.translate(this.formType === CONST.LOGIN_TYPE.PHONE - ? 'addSecondaryLoginPage.enterPreferredPhoneNumberToSendValidationLink' - : 'addSecondaryLoginPage.enterPreferredEmailToSendValidationLink')} - - - this.phoneNumberInputRef = el} - value={this.state.login} - onChangeText={this.onSecondaryLoginChange} - keyboardType={this.formType === CONST.LOGIN_TYPE.PHONE - ? CONST.KEYBOARD_TYPE.PHONE_PAD : undefined} - returnKeyType="done" - /> - - - this.setState({password})} - secureTextEntry - autoCompleteType="password" - textContentType="password" - onSubmitEditing={this.submitForm} - /> - - {!_.isEmpty(this.props.user.error) && ( - - {this.props.user.error} - - )} - - -