diff --git a/src/CONST.js b/src/CONST.js
index b98cf6cf70ec..d1b6a5b52e37 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -171,6 +171,13 @@ const CONST = {
PHONE_E164_PLUS: /^\+?[1-9]\d{1,14}$/,
NON_ALPHA_NUMERIC: /[^A-Za-z0-9+]/g,
},
+
+ GROWL: {
+ SUCCESS: 'success',
+ ERROR: 'error',
+ WARNING: 'warning',
+ DURATION: 2000,
+ },
};
export default CONST;
diff --git a/src/Expensify.js b/src/Expensify.js
index 6ac9b68a25ea..431db2e8d9a1 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -16,6 +16,8 @@ import styles from './styles/styles';
import PushNotification from './libs/Notification/PushNotification';
import UpdateAppModal from './components/UpdateAppModal';
import Visibility from './libs/Visibility';
+import GrowlNotification from './components/GrowlNotification';
+import {growlRef} from './libs/Growl';
// Initialize the store when the app loads for the first time
Onyx.init({
@@ -156,6 +158,7 @@ class Expensify extends PureComponent {
}
return (
<>
+
{/* We include the modal for showing a new update at the top level so the option is always present. */}
{this.props.updateAvailable ? : null}
diff --git a/src/libs/GrowlNotification/GrowlNotificationContainer/GrowlNotificationContainerPropTypes.js b/src/components/GrowlNotification/GrowlNotificationContainer/GrowlNotificationContainerPropTypes.js
similarity index 82%
rename from src/libs/GrowlNotification/GrowlNotificationContainer/GrowlNotificationContainerPropTypes.js
rename to src/components/GrowlNotification/GrowlNotificationContainer/GrowlNotificationContainerPropTypes.js
index fb5d017e984a..1343c11821c1 100644
--- a/src/libs/GrowlNotification/GrowlNotificationContainer/GrowlNotificationContainerPropTypes.js
+++ b/src/components/GrowlNotification/GrowlNotificationContainer/GrowlNotificationContainerPropTypes.js
@@ -1,6 +1,6 @@
import {Animated} from 'react-native';
import PropTypes from 'prop-types';
-import {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
+import {windowDimensionsPropTypes} from '../../withWindowDimensions';
const propTypes = {
/** GrowlNotification content */
diff --git a/src/libs/GrowlNotification/GrowlNotificationContainer/index.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.js
similarity index 89%
rename from src/libs/GrowlNotification/GrowlNotificationContainer/index.js
rename to src/components/GrowlNotification/GrowlNotificationContainer/index.js
index 82b0b68cbf6a..b1b051107ab0 100644
--- a/src/libs/GrowlNotification/GrowlNotificationContainer/index.js
+++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.js
@@ -1,7 +1,7 @@
import React from 'react';
import {Animated} from 'react-native';
import styles from '../../../styles/styles';
-import withWindowDimensions from '../../../components/withWindowDimensions';
+import withWindowDimensions from '../../withWindowDimensions';
import propTypes from './GrowlNotificationContainerPropTypes';
const GrowlNotificationContainer = ({children, translateY, isSmallScreenWidth}) => (
diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js
new file mode 100644
index 000000000000..f718430f27b7
--- /dev/null
+++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import {Animated} from 'react-native';
+import {SafeAreaInsetsContext} from 'react-native-safe-area-context';
+import styles, {getSafeAreaPadding} from '../../../styles/styles';
+import propTypes from './GrowlNotificationContainerPropTypes';
+
+const GrowlNotificationContainer = ({children, translateY}) => (
+
+ {insets => (
+
+ {children}
+
+ )}
+
+);
+
+GrowlNotificationContainer.propTypes = propTypes;
+
+export default GrowlNotificationContainer;
diff --git a/src/libs/GrowlNotification/index.js b/src/components/GrowlNotification/index.js
similarity index 90%
rename from src/libs/GrowlNotification/index.js
rename to src/components/GrowlNotification/index.js
index 16f232b498aa..c15ed036e5cf 100644
--- a/src/libs/GrowlNotification/index.js
+++ b/src/components/GrowlNotification/index.js
@@ -6,21 +6,22 @@ import {
Directions, FlingGestureHandler, State, TouchableWithoutFeedback,
} from 'react-native-gesture-handler';
import colors from '../../styles/colors';
-import Icon from '../../components/Icon';
-import {Checkmark, Exclamation} from '../../components/Icon/Expensicons';
+import Icon from '../Icon';
+import {Checkmark, Exclamation} from '../Icon/Expensicons';
import styles from '../../styles/styles';
import GrowlNotificationContainer from './GrowlNotificationContainer';
+import CONST from '../../CONST';
const types = {
- success: {
+ [CONST.GROWL.SUCCESS]: {
icon: Checkmark,
iconColor: colors.green,
},
- error: {
+ [CONST.GROWL.ERROR]: {
icon: Exclamation,
iconColor: colors.red,
},
- warning: {
+ [CONST.GROWL.WARNING]: {
icon: Exclamation,
iconColor: colors.yellow,
},
@@ -47,9 +48,9 @@ class GrowlNotification extends Component {
*
* @param {String} bodyText
* @param {String} type
- * @param {Number} duration - 2000
+ * @param {Number} duration
*/
- show(bodyText, type, duration = 2000) {
+ show(bodyText, type, duration) {
this.setState({
bodyText,
type,
diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js
index 4f4ca39b0c60..48329e4a5530 100755
--- a/src/components/TextInputFocusable/index.js
+++ b/src/components/TextInputFocusable/index.js
@@ -3,7 +3,9 @@ import {TextInput, StyleSheet} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
+import Growl from '../../libs/Growl';
import themeColors from '../../styles/themes/default';
+import CONST from '../../CONST';
const propTypes = {
/** Maximum number of lines in the text input */
@@ -108,6 +110,11 @@ class TextInputFocusable extends React.Component {
end: initialValue.length,
},
};
+ this.selection = {
+ start: initialValue.length,
+ end: initialValue.length,
+ };
+ this.saveSelection = this.saveSelection.bind(this);
}
componentDidMount() {
@@ -176,6 +183,17 @@ class TextInputFocusable extends React.Component {
return newNumberOfLines;
}
+ /**
+ * Keeps track of user cursor position on the Composer
+ *
+ * @param {{nativeEvent: {selection: any}}} event
+ * @memberof TextInputFocusable
+ */
+ saveSelection(event) {
+ this.selection = event.nativeEvent.selection;
+ this.props.onSelectionChange(event);
+ }
+
/**
* Check the paste event for an attachment, parse the data and
* call onPasteFile from props with the selected file
@@ -185,6 +203,7 @@ class TextInputFocusable extends React.Component {
checkForAttachment(event) {
const {files, types} = event.clipboardData;
const TEXT_HTML = 'text/html';
+ const TEXT_PLAIN = 'text/plain';
if (files.length > 0) {
// Prevent the default so we do not post the file name into the text box
event.preventDefault();
@@ -192,6 +211,7 @@ class TextInputFocusable extends React.Component {
} else if (types.includes(TEXT_HTML)) {
const domparser = new DOMParser();
const embededImages = domparser.parseFromString(event.clipboardData.getData(TEXT_HTML), TEXT_HTML).images;
+ const pastedText = event.clipboardData.getData(TEXT_PLAIN);
if (embededImages.length > 0) {
event.preventDefault();
fetch(embededImages[0].src)
@@ -208,9 +228,23 @@ class TextInputFocusable extends React.Component {
return new File([x], `pasted_image.${extension}`, {});
})
.then(this.props.onPasteFile)
- .catch((error) => {
+ .catch(() => {
const errorDesc = this.props.translate('textInputFocusable.problemGettingImageYouPasted');
- alert(`${errorDesc}. \n${error.message}`);
+ Growl.show(errorDesc, CONST.GROWL.ERROR);
+
+ /*
+ * Since we intercepted the user-triggered paste event to check for attachments,
+ * we need to manually set the value and call the `onChangeText` handler.
+ * Synthetically-triggered paste events do not affect the document's contents.
+ * See https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event for more details.
+ */
+ const beforeCursorText = this.textInput.value.substring(0, this.selection.start);
+ const afterCursorText = this.textInput.value.substring(this.selection.end);
+ this.textInput.value = beforeCursorText + pastedText + afterCursorText;
+ this.updateNumberOfLines();
+ this.props.onChangeText(this.textInput.value);
+ const newCursorPosition = beforeCursorText.length + pastedText.length;
+ this.setState({selection: {start: newCursorPosition, end: newCursorPosition}});
});
}
}
@@ -247,12 +281,12 @@ class TextInputFocusable extends React.Component {
onChange={() => {
this.updateNumberOfLines();
}}
+ onSelectionChange={this.saveSelection}
numberOfLines={this.state.numberOfLines}
style={propStyles}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...propsWithoutStyles}
disabled={this.props.isDisabled}
- onSelectionChange={this.props.onSelectionChange}
/>
);
}
diff --git a/src/libs/Growl.js b/src/libs/Growl.js
new file mode 100644
index 000000000000..9e20dcbcc2a6
--- /dev/null
+++ b/src/libs/Growl.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import CONST from '../CONST';
+
+export const growlRef = React.createRef();
+
+/**
+ * Show the growl notification
+ *
+ * @param {String} bodyText
+ * @param {String} type
+ * @param {Number} [duration]
+*/
+function show(bodyText, type, duration = CONST.GROWL.DURATION) {
+ growlRef.current.show(bodyText, type, duration);
+}
+
+
+export default {show};
diff --git a/src/libs/GrowlNotification/GrowlNotificationContainer/index.native.js b/src/libs/GrowlNotification/GrowlNotificationContainer/index.native.js
deleted file mode 100644
index d6d5a0bc0e8b..000000000000
--- a/src/libs/GrowlNotification/GrowlNotificationContainer/index.native.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-import {Animated} from 'react-native';
-import styles from '../../../styles/styles';
-import propTypes from './GrowlNotificationContainerPropTypes';
-
-const GrowlNotificationContainer = ({children, translateY}) => (
-
- {children}
-
-);
-
-GrowlNotificationContainer.propTypes = propTypes;
-
-export default GrowlNotificationContainer;
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index 00ea71e257dd..1487e85057b8 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -29,8 +29,8 @@ import CreateMenu from '../../../components/CreateMenu';
import Picker from '../../../components/Picker';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import compose from '../../../libs/compose';
-import GrowlNotification from '../../../libs/GrowlNotification';
import Button from '../../../components/Button';
+import Growl from '../../../libs/Growl';
const propTypes = {
/* Onyx Props */
@@ -131,7 +131,6 @@ class ProfilePage extends Component {
this.setAutomaticTimezone = this.setAutomaticTimezone.bind(this);
this.getLogins = this.getLogins.bind(this);
this.createMenuItems = this.createMenuItems.bind(this);
- this.growlNotification = undefined;
}
componentDidUpdate(prevProps) {
@@ -211,7 +210,7 @@ class ProfilePage extends Component {
},
});
- this.growlNotification.show(this.props.translate('profilePage.growlMessageOnSave'), 'success', 3000);
+ Growl.show(this.props.translate('profilePage.growlMessageOnSave'), CONST.GROWL.SUCCESS, 3000);
}
/**
@@ -261,7 +260,6 @@ class ProfilePage extends Component {
return (
- this.growlNotification = el} />