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 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 1.1.86
+ 1.1.87CFBundleSignature????CFBundleURLTypes
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.1.86.5
+ 1.1.87.8ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
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 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 1.1.86
+ 1.1.87CFBundleSignature????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:
- //
- // 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:
+ //