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;