diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js
index aa50d6db574b..e3123210277c 100644
--- a/src/components/AvatarWithImagePicker.js
+++ b/src/components/AvatarWithImagePicker.js
@@ -1,5 +1,5 @@
import _ from 'underscore';
-import React from 'react';
+import React, {useCallback, useState, useRef} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
@@ -12,10 +12,9 @@ import themeColors from '../styles/themes/default';
import AttachmentPicker from './AttachmentPicker';
import AvatarCropModal from './AvatarCropModal/AvatarCropModal';
import OfflineWithFeedback from './OfflineWithFeedback';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
+import useLocalize from '../hooks/useLocalize';
import variables from '../styles/variables';
import CONST from '../CONST';
-import SpinningIndicatorAnimation from '../styles/animation/SpinningIndicatorAnimation';
import Tooltip from './Tooltip';
import stylePropTypes from '../styles/stylePropTypes';
import * as FileUtils from '../libs/fileDownload/FileUtils';
@@ -51,8 +50,11 @@ const propTypes = {
left: PropTypes.number,
}).isRequired,
- /** Flag to see if image is being uploaded */
- isUploading: PropTypes.bool,
+ /** Where the popover should be positioned relative to the anchor points. */
+ anchorAlignment: PropTypes.shape({
+ horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
+ vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
+ }).isRequired,
/** Size of Indicator */
size: PropTypes.oneOf([CONST.AVATAR_SIZE.LARGE, CONST.AVATAR_SIZE.DEFAULT]),
@@ -78,8 +80,6 @@ const propTypes = {
/** The errors to display */
// eslint-disable-next-line react/forbid-prop-types
errors: PropTypes.object,
-
- ...withLocalizePropTypes,
};
const defaultProps = {
@@ -89,7 +89,6 @@ const defaultProps = {
style: [],
DefaultAvatar: () => {},
isUsingDefaultAvatar: false,
- isUploading: false,
size: CONST.AVATAR_SIZE.DEFAULT,
fallbackIcon: Expensicons.FallbackAvatar,
type: CONST.ICON_TYPE_AVATAR,
@@ -100,53 +99,46 @@ const defaultProps = {
errors: null,
};
-class AvatarWithImagePicker extends React.Component {
- constructor(props) {
- super(props);
- this.animation = new SpinningIndicatorAnimation();
- this.setError = this.setError.bind(this);
- this.isValidSize = this.isValidSize.bind(this);
- this.showAvatarCropModal = this.showAvatarCropModal.bind(this);
- this.hideAvatarCropModal = this.hideAvatarCropModal.bind(this);
- this.state = {
- isMenuVisible: false,
- validationError: null,
- phraseParam: {},
- isAvatarCropModalOpen: false,
- imageName: '',
- imageUri: '',
- imageType: '',
- };
- this.anchorRef = React.createRef();
- }
-
- componentDidMount() {
- if (!this.props.isUploading) {
- return;
- }
-
- this.animation.start();
- }
-
- componentDidUpdate(prevProps) {
- if (!prevProps.isUploading && this.props.isUploading) {
- this.animation.start();
- } else if (prevProps.isUploading && !this.props.isUploading) {
- this.animation.stop();
- }
- }
-
- componentWillUnmount() {
- this.animation.stop();
- }
+function AvatarWithImagePicker({
+ anchorPosition,
+ anchorAlignment,
+ DefaultAvatar,
+ editorMaskImage,
+ errors,
+ fallbackIcon,
+ isUsingDefaultAvatar,
+ pendingAction,
+ size,
+ source,
+ style,
+ type,
+ errorRowStyles,
+ onImageRemoved,
+ onImageSelected,
+ onErrorClose,
+}) {
+ const {translate} = useLocalize();
+
+ const [isMenuVisible, setIsMenuVisible] = useState(false);
+ const [validationError, setValidationError] = useState(null);
+ const [phraseParam, setPhraseParam] = useState({});
+ const [isAvatarCropModalOpen, setIsAvatarCropModalOpen] = useState(false);
+ const [imageName, setImageName] = useState('');
+ const [imageUri, setImageUri] = useState('');
+ const [imageType, setImageType] = useState('');
+
+ const additionalStyles = _.isArray(style) ? style : [style];
+
+ const anchorRef = useRef(null);
/**
* @param {String} error
- * @param {Object} phraseParam
+ * @param {Object} phrase
*/
- setError(error, phraseParam) {
- this.setState({validationError: error, phraseParam});
- }
+ const setError = (error, phrase) => {
+ setValidationError(error);
+ setPhraseParam(phrase);
+ };
/**
* Check if the attachment extension is allowed.
@@ -154,10 +146,10 @@ class AvatarWithImagePicker extends React.Component {
* @param {Object} image
* @returns {Boolean}
*/
- isValidExtension(image) {
+ const isValidExtension = (image) => {
const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(image, 'name', ''));
return _.contains(CONST.AVATAR_ALLOWED_EXTENSIONS, fileExtension.toLowerCase());
- }
+ };
/**
* Check if the attachment size is less than allowed size.
@@ -165,9 +157,7 @@ class AvatarWithImagePicker extends React.Component {
* @param {Object} image
* @returns {Boolean}
*/
- isValidSize(image) {
- return image && lodashGet(image, 'size', 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE;
- }
+ const isValidSize = (image) => image && lodashGet(image, 'size', 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE;
/**
* Check if the attachment resolution matches constraints.
@@ -175,34 +165,33 @@ class AvatarWithImagePicker extends React.Component {
* @param {Object} image
* @returns {Promise}
*/
- isValidResolution(image) {
- return getImageResolution(image).then(
+ const isValidResolution = (image) =>
+ getImageResolution(image).then(
(resolution) =>
resolution.height >= CONST.AVATAR_MIN_HEIGHT_PX &&
resolution.width >= CONST.AVATAR_MIN_WIDTH_PX &&
resolution.height <= CONST.AVATAR_MAX_HEIGHT_PX &&
resolution.width <= CONST.AVATAR_MAX_WIDTH_PX,
);
- }
/**
* Validates if an image has a valid resolution and opens an avatar crop modal
*
* @param {Object} image
*/
- showAvatarCropModal(image) {
- if (!this.isValidExtension(image)) {
- this.setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS});
+ const showAvatarCropModal = (image) => {
+ if (!isValidExtension(image)) {
+ setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS});
return;
}
- if (!this.isValidSize(image)) {
- this.setError('avatarWithImagePicker.sizeExceeded', {maxUploadSizeInMB: CONST.AVATAR_MAX_ATTACHMENT_SIZE / (1024 * 1024)});
+ if (!isValidSize(image)) {
+ setError('avatarWithImagePicker.sizeExceeded', {maxUploadSizeInMB: CONST.AVATAR_MAX_ATTACHMENT_SIZE / (1024 * 1024)});
return;
}
- this.isValidResolution(image).then((isValidResolution) => {
- if (!isValidResolution) {
- this.setError('avatarWithImagePicker.resolutionConstraints', {
+ isValidResolution(image).then((isValid) => {
+ if (!isValid) {
+ setError('avatarWithImagePicker.resolutionConstraints', {
minHeightInPx: CONST.AVATAR_MIN_HEIGHT_PX,
minWidthInPx: CONST.AVATAR_MIN_WIDTH_PX,
maxHeightInPx: CONST.AVATAR_MAX_HEIGHT_PX,
@@ -211,143 +200,143 @@ class AvatarWithImagePicker extends React.Component {
return;
}
- this.setState({
- isAvatarCropModalOpen: true,
- validationError: null,
- phraseParam: {},
- isMenuVisible: false,
- imageUri: image.uri,
- imageName: image.name,
- imageType: image.type,
- });
+ setIsAvatarCropModalOpen(true);
+ setValidationError(null);
+ setPhraseParam({});
+ setIsMenuVisible(false);
+ setImageUri(image.uri);
+ setImageName(image.name);
+ setImageType(image.type);
});
- }
-
- hideAvatarCropModal() {
- this.setState({isAvatarCropModalOpen: false});
- }
-
- render() {
- const DefaultAvatar = this.props.DefaultAvatar;
- const additionalStyles = _.isArray(this.props.style) ? this.props.style : [this.props.style];
-
- return (
-
-
-
-
- this.setState((prev) => ({isMenuVisible: !prev.isMenuVisible}))}
- accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
- accessibilityLabel={this.props.translate('avatarWithImagePicker.editImage')}
- disabled={this.state.isAvatarCropModalOpen}
- ref={this.anchorRef}
- >
-
- {this.props.source ? (
-
- ) : (
-
- )}
-
-
- {
+ setIsAvatarCropModalOpen(false);
+ }, []);
+
+ /**
+ * Create menu items list for avatar menu
+ *
+ * @param {Function} openPicker
+ * @returns {Array}
+ */
+ const createMenuItems = (openPicker) => {
+ const menuItems = [
+ {
+ icon: Expensicons.Upload,
+ text: translate('avatarWithImagePicker.uploadPhoto'),
+ onSelected: () => {
+ if (Browser.isSafari()) {
+ return;
+ }
+ openPicker({
+ onPicked: showAvatarCropModal,
+ });
+ },
+ },
+ ];
+
+ // If current avatar isn't a default avatar, allow Remove Photo option
+ if (!isUsingDefaultAvatar) {
+ menuItems.push({
+ icon: Expensicons.Trashcan,
+ text: translate('avatarWithImagePicker.removePhoto'),
+ onSelected: () => {
+ setError(null, {});
+ onImageRemoved();
+ },
+ });
+ }
+ return menuItems;
+ };
+
+ return (
+
+
+
+
+ setIsMenuVisible((prev) => !prev)}
+ accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
+ accessibilityLabel={translate('avatarWithImagePicker.editImage')}
+ disabled={isAvatarCropModalOpen}
+ ref={anchorRef}
+ >
+
+ {source ? (
+
-
-
-
-
-
- {({openPicker}) => {
- const menuItems = [
- {
- icon: Expensicons.Upload,
- text: this.props.translate('avatarWithImagePicker.uploadPhoto'),
- onSelected: () => {
- if (Browser.isSafari()) {
- return;
- }
- openPicker({
- onPicked: this.showAvatarCropModal,
- });
- },
- },
- ];
-
- // If current avatar isn't a default avatar, allow Remove Photo option
- if (!this.props.isUsingDefaultAvatar) {
- menuItems.push({
- icon: Expensicons.Trashcan,
- text: this.props.translate('avatarWithImagePicker.removePhoto'),
- onSelected: () => {
- this.setError(null, {});
- this.props.onImageRemoved();
- },
- });
- }
- return (
- this.setState({isMenuVisible: false})}
- onItemSelected={(item, index) => {
- this.setState({isMenuVisible: false});
- // In order for the file picker to open dynamically, the click
- // function must be called from within a event handler that was initiated
- // by the user on Safari.
- if (index === 0 && Browser.isSafari()) {
- openPicker({
- onPicked: this.showAvatarCropModal,
- });
- }
- }}
- menuItems={menuItems}
- anchorPosition={this.props.anchorPosition}
- withoutOverlay
- anchorRef={this.anchorRef}
- anchorAlignment={this.props.anchorAlignment}
+ ) : (
+
+ )}
+
+
+
- );
- }}
-
-
- {this.state.validationError && (
-
- )}
-
+
+
+
+
+
+ {({openPicker}) => (
+ setIsMenuVisible(false)}
+ onItemSelected={(item, index) => {
+ setIsMenuVisible(false);
+ // In order for the file picker to open dynamically, the click
+ // function must be called from within a event handler that was initiated
+ // by the user on Safari.
+ if (index === 0 && Browser.isSafari()) {
+ openPicker({onPicked: showAvatarCropModal});
+ }
+ }}
+ menuItems={createMenuItems(openPicker)}
+ anchorPosition={anchorPosition}
+ withoutOverlay
+ anchorRef={anchorRef}
+ anchorAlignment={anchorAlignment}
+ />
+ )}
+
- );
- }
+ {validationError && (
+
+ )}
+
+
+ );
}
AvatarWithImagePicker.propTypes = propTypes;
AvatarWithImagePicker.defaultProps = defaultProps;
+AvatarWithImagePicker.displayName = 'AvatarWithImagePicker';
-export default withLocalize(AvatarWithImagePicker);
+export default AvatarWithImagePicker;
diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js
index 7aff9093c4dd..12fde29dbcc1 100644
--- a/src/pages/workspace/WorkspaceSettingsPage.js
+++ b/src/pages/workspace/WorkspaceSettingsPage.js
@@ -99,7 +99,6 @@ function WorkspaceSettingsPage(props) {
enabledWhenOffline
>
(
diff --git a/src/styles/animation/SpinningIndicatorAnimation.js b/src/styles/animation/SpinningIndicatorAnimation.js
deleted file mode 100644
index 8e7fd0277221..000000000000
--- a/src/styles/animation/SpinningIndicatorAnimation.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import {Animated, Easing} from 'react-native';
-import useNativeDriver from '../../libs/useNativeDriver';
-
-class SpinningIndicatorAnimation {
- constructor() {
- this.rotate = new Animated.Value(0);
- this.scale = new Animated.Value(1);
- this.startRotation = this.startRotation.bind(this);
- this.start = this.start.bind(this);
- this.stop = this.stop.bind(this);
- this.getSyncingStyles = this.getSyncingStyles.bind(this);
- }
-
- /**
- * Rotation animation for indicator in a loop
- *
- * @memberof AvatarWithImagePicker
- */
- startRotation() {
- this.rotate.setValue(0);
- Animated.loop(
- Animated.timing(this.rotate, {
- toValue: 1,
- duration: 2000,
- easing: Easing.linear,
- isInteraction: false,
-
- // Animated.loop does not work with `useNativeDriver: true` on Web
- useNativeDriver,
- }),
- ).start();
- }
-
- /**
- * Start Animation for Indicator
- *
- * @memberof AvatarWithImagePicker
- */
- start() {
- this.startRotation();
- Animated.spring(this.scale, {
- toValue: 1.666,
- tension: 1,
- isInteraction: false,
- useNativeDriver: true,
- }).start();
- }
-
- /**
- * Stop Animation for Indicator
- *
- * @memberof AvatarWithImagePicker
- */
- stop() {
- Animated.spring(this.scale, {
- toValue: 1,
- tension: 1,
- isInteraction: false,
- useNativeDriver: true,
- }).start(() => {
- this.rotate.resetAnimation();
- this.scale.resetAnimation();
- this.rotate.setValue(0);
- });
- }
-
- /**
- * Get Indicator Styles while animating
- *
- * @returns {Object}
- */
- getSyncingStyles() {
- return {
- transform: [
- {
- rotate: this.rotate.interpolate({
- inputRange: [0, 1],
- outputRange: ['0deg', '-360deg'],
- }),
- },
- {
- scale: this.scale,
- },
- ],
- };
- }
-}
-
-export default SpinningIndicatorAnimation;