diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 393a0085176c..a481a4026659 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -1,4 +1,4 @@ -import React, {PureComponent} from 'react'; +import React, {useState, useCallback} from 'react'; import PropTypes from 'prop-types'; import {View, Animated, Keyboard} from 'react-native'; import Str from 'expensify-common/lib/str'; @@ -78,164 +78,153 @@ const defaultProps = { onModalHide: () => {}, }; -class AttachmentModal extends PureComponent { - constructor(props) { - super(props); - - this.state = { - isModalOpen: false, - shouldLoadAttachment: false, - isAttachmentInvalid: false, - isAuthTokenRequired: props.isAuthTokenRequired, - attachmentInvalidReasonTitle: null, - attachmentInvalidReason: null, - source: props.source, - modalType: CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE, - isConfirmButtonDisabled: false, - confirmButtonFadeAnimation: new Animated.Value(1), - file: props.originalFileName - ? { - name: props.originalFileName, - } - : undefined, - }; - - this.submitAndClose = this.submitAndClose.bind(this); - this.closeConfirmModal = this.closeConfirmModal.bind(this); - this.onNavigate = this.onNavigate.bind(this); - this.downloadAttachment = this.downloadAttachment.bind(this); - this.validateAndDisplayFileToUpload = this.validateAndDisplayFileToUpload.bind(this); - this.updateConfirmButtonVisibility = this.updateConfirmButtonVisibility.bind(this); - } +function AttachmentModal(props) { + const [isModalOpen, setIsModalOpen] = useState(false); + const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); + const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); + const [isAuthTokenRequired] = useState(props.isAuthTokenRequired); + const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(null); + const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); + const [source, setSource] = useState(props.source); + const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); + const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); + const [confirmButtonFadeAnimation] = useState(new Animated.Value(1)); + const [file, setFile] = useState( + props.originalFileName + ? { + name: props.originalFileName, + } + : undefined, + ); /** * Keeps the attachment source in sync with the attachment displayed currently in the carousel. * @param {{ source: String, isAuthTokenRequired: Boolean, file: { name: string } }} attachment */ - onNavigate(attachment) { - this.setState(attachment); - } + const onNavigate = useCallback((attachment) => { + setSource(attachment.source); + setFile(attachment.file); + }, []); /** * If our attachment is a PDF, return the unswipeable Modal type. * @param {String} sourceURL - * @param {Object} file + * @param {Object} _file * @returns {String} */ - getModalType(sourceURL, file) { - return sourceURL && (Str.isPDF(sourceURL) || (file && Str.isPDF(file.name || this.props.translate('attachmentView.unknownFilename')))) - ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE - : CONST.MODAL.MODAL_TYPE.CENTERED; - } - + const getModalType = useCallback( + (sourceURL, _file) => + sourceURL && (Str.isPDF(sourceURL) || (_file && Str.isPDF(_file.name || props.translate('attachmentView.unknownFilename')))) + ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE + : CONST.MODAL.MODAL_TYPE.CENTERED, + // eslint-disable-next-line react-hooks/exhaustive-deps + [props.translate], + ); /** * Download the currently viewed attachment. */ - downloadAttachment() { - let sourceURL = this.state.source; - if (this.state.isAuthTokenRequired) { + const downloadAttachment = useCallback(() => { + let sourceURL = source; + if (isAuthTokenRequired) { sourceURL = addEncryptedAuthTokenToURL(sourceURL); } - fileDownload(sourceURL, this.state.file.name); + fileDownload(sourceURL, file.name); // At ios, if the keyboard is open while opening the attachment, then after downloading // the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard. Keyboard.dismiss(); - } + }, [isAuthTokenRequired, source, file]); /** * Execute the onConfirm callback and close the modal. */ - submitAndClose() { + const submitAndClose = useCallback(() => { // If the modal has already been closed or the confirm button is disabled // do not submit. - if (!this.state.isModalOpen || this.state.isConfirmButtonDisabled) { + if (!isModalOpen || isConfirmButtonDisabled) { return; } - if (this.props.onConfirm) { - this.props.onConfirm(lodashExtend(this.state.file, {source: this.state.source})); + if (props.onConfirm) { + props.onConfirm(lodashExtend(file, {source})); } - this.setState({isModalOpen: false}); - } + setIsModalOpen(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isModalOpen, isConfirmButtonDisabled, props.onConfirm, file, source]); /** * Close the confirm modal. */ - closeConfirmModal() { - this.setState({isAttachmentInvalid: false}); - } - + const closeConfirmModal = useCallback(() => { + setIsAttachmentInvalid(false); + }, []); /** - * @param {Object} file + * @param {Object} _file * @returns {Boolean} */ - isValidFile(file) { - const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', '')); - if (_.contains(CONST.API_ATTACHMENT_VALIDATIONS.UNALLOWED_EXTENSIONS, fileExtension.toLowerCase())) { - const invalidReason = this.props.translate('attachmentPicker.notAllowedExtension'); - this.setState({ - isAttachmentInvalid: true, - attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.wrongFileType'), - attachmentInvalidReason: invalidReason, - }); - return false; - } - - if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { - this.setState({ - isAttachmentInvalid: true, - attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.attachmentTooLarge'), - attachmentInvalidReason: this.props.translate('attachmentPicker.sizeExceeded'), - }); - return false; - } - - if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { - this.setState({ - isAttachmentInvalid: true, - attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.attachmentTooSmall'), - attachmentInvalidReason: this.props.translate('attachmentPicker.sizeNotMet'), - }); - return false; - } - - return true; - } - + const isValidFile = useCallback( + (_file) => { + const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(_file, 'name', '')); + if (_.contains(CONST.API_ATTACHMENT_VALIDATIONS.UNALLOWED_EXTENSIONS, fileExtension.toLowerCase())) { + const invalidReason = props.translate('attachmentPicker.notAllowedExtension'); + + setIsAttachmentInvalid(true); + setAttachmentInvalidReasonTitle(props.translate('attachmentPicker.wrongFileType')); + setAttachmentInvalidReason(invalidReason); + return false; + } + + if (lodashGet(_file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { + setIsAttachmentInvalid(true); + setAttachmentInvalidReasonTitle(props.translate('attachmentPicker.attachmentTooLarge')); + setAttachmentInvalidReason(props.translate('attachmentPicker.sizeExceeded')); + return false; + } + + if (lodashGet(_file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { + setIsAttachmentInvalid(true); + setAttachmentInvalidReasonTitle(props.translate('attachmentPicker.attachmentTooSmall')); + setAttachmentInvalidReason(props.translate('attachmentPicker.sizeNotMet')); + return false; + } + + return true; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [props.translate], + ); /** - * @param {Object} file + * @param {Object} _file */ - validateAndDisplayFileToUpload(file) { - if (!file) { - return; - } - - if (!this.isValidFile(file)) { - return; - } - - if (file instanceof File) { - const source = URL.createObjectURL(file); - const modalType = this.getModalType(source, file); - this.setState({ - isModalOpen: true, - source, - file, - modalType, - }); - } else { - const modalType = this.getModalType(file.uri, file); - this.setState({ - isModalOpen: true, - source: file.uri, - file, - modalType, - }); - } - } + const validateAndDisplayFileToUpload = useCallback( + (_file) => { + if (!_file) { + return; + } + + if (!isValidFile(_file)) { + return; + } + + if (_file instanceof File) { + const inputSource = URL.createObjectURL(_file); + const inputModalType = getModalType(inputSource, _file); + setIsModalOpen(true); + setSource(inputSource); + setFile(_file); + setModalType(inputModalType); + } else { + const inputModalType = getModalType(_file.uri, _file); + setIsModalOpen(true); + setSource(_file.uri); + setFile(_file); + setModalType(inputModalType); + } + }, + [isValidFile, getModalType], + ); /** * In order to gracefully hide/show the confirm button when the keyboard @@ -245,110 +234,124 @@ class AttachmentModal extends PureComponent { * * @param {Boolean} shouldFadeOut If true, fade out confirm button. Otherwise fade in. */ - updateConfirmButtonVisibility(shouldFadeOut) { - this.setState({isConfirmButtonDisabled: shouldFadeOut}); - const toValue = shouldFadeOut ? 0 : 1; - - Animated.timing(this.state.confirmButtonFadeAnimation, { - toValue, - duration: 100, - useNativeDriver: true, - }).start(); - } - - render() { - const source = this.props.source || this.state.source; - return ( - <> - this.setState({isModalOpen: false})} - isVisible={this.state.isModalOpen} - backgroundColor={themeColors.componentBG} - onModalShow={() => { - this.props.onModalShow(); - this.setState({shouldLoadAttachment: true}); - }} - onModalHide={(e) => { - this.props.onModalHide(e); - this.setState({shouldLoadAttachment: false}); - }} - propagateSwipe - > - {this.props.isSmallScreenWidth && } - this.setState({isModalOpen: false})} - onCloseButtonPress={() => this.setState({isModalOpen: false})} - /> - - {!_.isEmpty(this.props.report) ? ( - { + setIsConfirmButtonDisabled(shouldFadeOut); + const toValue = shouldFadeOut ? 0 : 1; + + Animated.timing(confirmButtonFadeAnimation, { + toValue, + duration: 100, + useNativeDriver: true, + }).start(); + }, + [confirmButtonFadeAnimation], + ); + + /** + * close the modal + */ + const closeModal = useCallback(() => { + setIsModalOpen(false); + }, []); + + /** + * open the modal + */ + const openModal = useCallback(() => { + setIsModalOpen(true); + }, []); + + const sourceForAttachmentView = props.source || source; + return ( + <> + { + props.onModalShow(); + setShouldLoadAttachment(true); + }} + onModalHide={(e) => { + props.onModalHide(e); + setShouldLoadAttachment(false); + }} + propagateSwipe + > + {props.isSmallScreenWidth && } + downloadAttachment(source)} + shouldShowCloseButton={!props.isSmallScreenWidth} + shouldShowBackButton={props.isSmallScreenWidth} + onBackButtonPress={closeModal} + onCloseButtonPress={closeModal} + /> + + {!_.isEmpty(props.report) ? ( + + ) : ( + Boolean(sourceForAttachmentView) && + shouldLoadAttachment && ( + - ) : ( - Boolean(source) && - this.state.shouldLoadAttachment && ( - + {/* If we have an onConfirm method show a confirmation button */} + {Boolean(props.onConfirm) && ( + + {({safeAreaPaddingBottomStyle}) => ( + +