diff --git a/src/CONST.js b/src/CONST.js
index f8fbc930c9a3..33fcd8f58dad 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -12,8 +12,17 @@ const CONST = {
ANDROID_PACKAGE_NAME,
ANIMATED_TRANSITION: 300,
- // 50 megabytes in bytes
- API_MAX_ATTACHMENT_SIZE: 52428800,
+ API_ATTACHMENT_VALIDATIONS: {
+ // Same as the PHP layer allows
+ ALLOWED_EXTENSIONS: ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'html', 'txt', 'rtf', 'doc', 'docx', 'htm', 'tiff', 'tif', 'xml'],
+
+ // 50 megabytes in bytes
+ MAX_SIZE: 52428800,
+
+ // An arbitrary size, but the same minimum as in the PHP layer
+ MIN_SIZE: 240,
+ },
+
AVATAR_MAX_ATTACHMENT_SIZE: 6291456,
NEW_EXPENSIFY_URL: ACTIVE_EXPENSIFY_URL,
APP_DOWNLOAD_LINKS: {
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 4bb62c44d0cc..b67e88f79ee3 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
import {View} from 'react-native';
import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
-import _ from 'lodash';
+import lodashExtend from 'lodash/extend';
+import _ from 'underscore';
import CONST from '../CONST';
import Modal from './Modal';
import AttachmentView from './AttachmentView';
@@ -71,7 +72,9 @@ class AttachmentModal extends PureComponent {
this.state = {
isModalOpen: false,
- isConfirmModalOpen: false,
+ isAttachmentInvalid: false,
+ attachmentInvalidReasonTitle: null,
+ attachmentInvalidReason: null,
file: null,
sourceURL: props.sourceURL,
modalType: CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE,
@@ -79,7 +82,7 @@ class AttachmentModal extends PureComponent {
this.submitAndClose = this.submitAndClose.bind(this);
this.closeConfirmModal = this.closeConfirmModal.bind(this);
- this.isValidSize = this.isValidSize.bind(this);
+ this.validateAndDisplayFileToUpload = this.validateAndDisplayFileToUpload.bind(this);
}
/**
@@ -89,22 +92,30 @@ class AttachmentModal extends PureComponent {
* @returns {String}
*/
getModalType(sourceUrl, file) {
- const modalType = (sourceUrl
- && (Str.isPDF(sourceUrl) || (file && Str.isPDF(file.name || this.props.translate('attachmentView.unknownFilename')))))
+ 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;
- return modalType;
}
/**
* Returns the filename split into fileName and fileExtension
+ *
+ * @param {String} fullFileName
* @returns {Object}
*/
- splitExtensionFromFileName() {
- const fullFileName = this.props.originalFileName ? this.props.originalFileName.trim() : lodashGet(this.state, 'file.name', '').trim();
- const splittedFileName = fullFileName.split('.');
- const fileExtension = splittedFileName.pop();
- const fileName = splittedFileName.join('.');
+ splitExtensionFromFileName(fullFileName) {
+ const fileName = fullFileName.trim();
+ const splitFileName = fileName.split('.');
+ const fileExtension = splitFileName.pop();
return {fileName, fileExtension};
}
@@ -118,7 +129,7 @@ class AttachmentModal extends PureComponent {
}
if (this.props.onConfirm) {
- this.props.onConfirm(_.extend(this.state.file, {source: this.state.sourceURL}));
+ this.props.onConfirm(lodashExtend(this.state.file, {source: this.state.sourceURL}));
}
this.setState({isModalOpen: false});
@@ -128,16 +139,70 @@ class AttachmentModal extends PureComponent {
* Close the confirm modal.
*/
closeConfirmModal() {
- this.setState({isConfirmModalOpen: false});
+ this.setState({isAttachmentInvalid: false});
}
/**
- * Check if the attachment size is less than the API size limit.
* @param {Object} file
* @returns {Boolean}
*/
- isValidSize(file) {
- return !file || lodashGet(file, 'size', 0) < CONST.API_MAX_ATTACHMENT_SIZE;
+ isValidFile(file) {
+ 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;
+ }
+
+ const {fileExtension} = this.splitExtensionFromFileName(lodashGet(file, 'name', ''));
+ if (!_.contains(CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_EXTENSIONS, fileExtension)) {
+ const invalidReason = `${this.props.translate('attachmentPicker.notAllowedExtension')} ${CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_EXTENSIONS.join(', ')}`;
+ this.setState({
+ isAttachmentInvalid: true,
+ attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.wrongFileType'),
+ attachmentInvalidReason: invalidReason,
+ });
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @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, sourceURL: source, file, modalType,
+ });
+ } else {
+ const modalType = this.getModalType(file.uri, file);
+ this.setState({
+ isModalOpen: true, sourceURL: file.uri, file, modalType,
+ });
+ }
}
render() {
@@ -149,7 +214,7 @@ class AttachmentModal extends PureComponent {
? [styles.imageModalImageCenterContainer]
: [styles.imageModalImageCenterContainer, styles.p5];
- const {fileName, fileExtension} = this.splitExtensionFromFileName();
+ const {fileName, fileExtension} = this.splitExtensionFromFileName(this.props.originalFileName || lodashGet(this.state, 'file.name', ''));
return (
<>
@@ -197,34 +262,19 @@ class AttachmentModal extends PureComponent {
/>
)}
+
+
{this.props.children({
- displayFileInModal: ({file}) => {
- if (!this.isValidSize(file)) {
- this.setState({isConfirmModalOpen: true});
- return;
- }
- if (file instanceof File) {
- const source = URL.createObjectURL(file);
- const modalType = this.getModalType(source, file);
- this.setState({
- isModalOpen: true, sourceURL: source, file, modalType,
- });
- } else {
- const modalType = this.getModalType(file.uri, file);
- this.setState({
- isModalOpen: true, sourceURL: file.uri, file, modalType,
- });
- }
- },
+ displayFileInModal: this.validateAndDisplayFileToUpload,
show: () => {
this.setState({isModalOpen: true});
},
diff --git a/src/components/ConfirmModal.js b/src/components/ConfirmModal.js
index 5c89135bef7b..bf7d1e2fffda 100755
--- a/src/components/ConfirmModal.js
+++ b/src/components/ConfirmModal.js
@@ -7,7 +7,7 @@ import ConfirmContent from './ConfirmContent';
const propTypes = {
/** Title of the modal */
- title: PropTypes.string.isRequired,
+ title: PropTypes.string,
/** A callback to call when the form has been submitted */
onConfirm: PropTypes.func.isRequired,
@@ -54,6 +54,7 @@ const defaultProps = {
onCancel: () => {},
shouldShowCancelButton: true,
shouldSetModalVisibility: true,
+ title: '',
onModalHide: () => {},
};
diff --git a/src/languages/en.js b/src/languages/en.js
index fa86c37a450d..4cb0a8029e92 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -119,6 +119,10 @@ export default {
chooseDocument: 'Choose document',
attachmentTooLarge: 'Attachment too large',
sizeExceeded: 'Attachment size is larger than 50 MB limit.',
+ attachmentTooSmall: 'Attachment too small',
+ sizeNotMet: 'Attachment size must be greater than 240 bytes',
+ wrongFileType: 'Attachment is the wrong type',
+ notAllowedExtension: 'Attachments must be one of the following types: ',
},
composer: {
noExtentionFoundForMimeType: 'No extension found for mime type',
diff --git a/src/languages/es.js b/src/languages/es.js
index 934df4f03e1c..661c1793c6b1 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -119,6 +119,10 @@ export default {
chooseDocument: 'Elegir documento',
attachmentTooLarge: 'Archivo adjunto demasiado grande',
sizeExceeded: 'El archivo adjunto supera el límite de 50 MB.',
+ attachmentTooSmall: 'Archivo adjunto demasiado pequeño',
+ sizeNotMet: 'El archivo adjunto debe ser mas grande que 240 bytes',
+ wrongFileType: 'El tipo del archivo adjunto es incorrecto',
+ notAllowedExtension: 'Los archivos adjuntos deben ser de uno de los siguientes tipos: ',
},
composer: {
noExtentionFoundForMimeType: 'No se encontró una extension para este tipo de contenido',
diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js
index 2565c4575997..4f22eb497350 100644
--- a/src/libs/actions/User.js
+++ b/src/libs/actions/User.js
@@ -241,19 +241,19 @@ function validateLogin(accountID, validateCode) {
* Checks the blockedFromConcierge object to see if it has an expiresAt key,
* and if so whether the expiresAt date of a user's ban is before right now
*
- * @param {Object} blockedFromConcierge
+ * @param {Object} blockedFromConciergeNVP
* @returns {Boolean}
*/
-function isBlockedFromConcierge(blockedFromConcierge) {
- if (_.isEmpty(blockedFromConcierge)) {
+function isBlockedFromConcierge(blockedFromConciergeNVP) {
+ if (_.isEmpty(blockedFromConciergeNVP)) {
return false;
}
- if (!blockedFromConcierge.expiresAt) {
+ if (!blockedFromConciergeNVP.expiresAt) {
return false;
}
- return moment().isBefore(moment(blockedFromConcierge.expiresAt), 'day');
+ return moment().isBefore(moment(blockedFromConciergeNVP.expiresAt), 'day');
}
/**
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index fd45bd61626b..6e58ae1f5715 100755
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -124,12 +124,14 @@ class ReportActionCompose extends React.Component {
this.setIsFullComposerAvailable = this.setIsFullComposerAvailable.bind(this);
this.focus = this.focus.bind(this);
this.addEmojiToTextBox = this.addEmojiToTextBox.bind(this);
- this.comment = props.comment;
- this.shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();
this.onSelectionChange = this.onSelectionChange.bind(this);
this.setTextInputRef = this.setTextInputRef.bind(this);
this.getInputPlaceholder = this.getInputPlaceholder.bind(this);
this.getIOUOptions = this.getIOUOptions.bind(this);
+ this.addAttachment = this.addAttachment.bind(this);
+
+ this.comment = props.comment;
+ this.shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();
this.state = {
isFocused: this.shouldFocusInputOnScreenFocus,
@@ -447,6 +449,15 @@ class ReportActionCompose extends React.Component {
return trimmedComment;
}
+ /**
+ * @param {Object} file
+ */
+ addAttachment(file) {
+ const comment = this.prepareCommentAndResetComposer();
+ Report.addAttachment(this.props.reportID, file, comment);
+ this.setTextInputShouldClear(false);
+ }
+
/**
* Add a new comment to this chat
*
@@ -500,11 +511,7 @@ class ReportActionCompose extends React.Component {
>
{
- const comment = this.prepareCommentAndResetComposer();
- Report.addAttachment(this.props.reportID, file, comment);
- this.setTextInputShouldClear(false);
- }}
+ onConfirm={this.addAttachment}
>
{({displayFileInModal}) => (
<>
@@ -572,9 +579,7 @@ class ReportActionCompose extends React.Component {
text: this.props.translate('reportActionCompose.addAttachment'),
onSelected: () => {
openPicker({
- onPicked: (file) => {
- displayFileInModal({file});
- },
+ onPicked: displayFileInModal,
});
},
},
@@ -616,7 +621,7 @@ class ReportActionCompose extends React.Component {
return;
}
- displayFileInModal({file});
+ displayFileInModal(file);
this.setState({isDraggingOver: false});
}}
style={[styles.textInputCompose, this.props.isComposerFullSize ? styles.textInputFullCompose : styles.flex4]}
@@ -624,7 +629,7 @@ class ReportActionCompose extends React.Component {
maxLines={this.state.maxLines}
onFocus={() => this.setIsFocused(true)}
onBlur={() => this.setIsFocused(false)}
- onPasteFile={file => displayFileInModal({file})}
+ onPasteFile={displayFileInModal}
shouldClear={this.state.textInputShouldClear}
onClear={() => this.setTextInputShouldClear(false)}
isDisabled={isComposeDisabled || isBlockedFromConcierge}
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 10e5b58de3cb..f36440dcabaf 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -28,6 +28,8 @@ import {withNetwork, withReportActionsDrafts} from '../../../components/OnyxProv
import RenameAction from '../../../components/ReportActionItem/RenameAction';
import InlineSystemMessage from '../../../components/InlineSystemMessage';
import styles from '../../../styles/styles';
+import * as User from '../../../libs/actions/User';
+import * as ReportUtils from '../../../libs/ReportUtils';
const propTypes = {
/** The ID of the report this action is on. */
@@ -139,16 +141,20 @@ class ReportActionItem extends Component {
);
} else {
children = !this.props.draftMessage
- ?
- : (
+ ? (
+
+ ) : (
this.textInput = el}
- report={this.props.report}
- blockedFromConcierge={this.props.blockedFromConcierge}
+ action={this.props.action}
+ draftMessage={this.props.draftMessage}
+ reportID={this.props.reportID}
+ index={this.props.index}
+ ref={el => this.textInput = el}
+ report={this.props.report}
+ shouldDisableEmojiPicker={
+ (ReportUtils.chatIncludesConcierge(this.props.report) && User.isBlockedFromConcierge(this.props.blockedFromConcierge))
+ || ReportUtils.isArchivedRoom(this.props.report)
+ }
/>
);
}
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js
index c2b270c9085a..3e64cfaaf09c 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.js
+++ b/src/pages/home/report/ReportActionItemMessageEdit.js
@@ -16,10 +16,8 @@ import Button from '../../../components/Button';
import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager';
import compose from '../../../libs/compose';
import EmojiPickerButton from '../../../components/EmojiPicker/EmojiPickerButton';
-import * as ReportUtils from '../../../libs/ReportUtils';
import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
import VirtualKeyboard from '../../../libs/VirtualKeyboard';
-import * as User from '../../../libs/actions/User';
const propTypes = {
/** All the data of the action */
@@ -43,11 +41,8 @@ const propTypes = {
participants: PropTypes.arrayOf(PropTypes.string),
}),
- // The NVP describing a user's block status
- blockedFromConcierge: PropTypes.shape({
- // The date that the user will be unblocked
- expiresAt: PropTypes.string,
- }),
+ // Whether or not the emoji picker is disabled
+ shouldDisableEmojiPicker: PropTypes.bool,
/** Window Dimensions Props */
...windowDimensionsPropTypes,
@@ -59,7 +54,7 @@ const propTypes = {
const defaultProps = {
forwardedRef: () => {},
report: {},
- blockedFromConcierge: {},
+ shouldDisableEmojiPicker: false,
};
class ReportActionItemMessageEdit extends React.Component {
@@ -190,10 +185,6 @@ class ReportActionItemMessageEdit extends React.Component {
}
render() {
- const shouldDisableEmojiPicker = (ReportUtils.chatIncludesConcierge(this.props.report)
- && User.isBlockedFromConcierge(this.props.blockedFromConcierge))
- || ReportUtils.isArchivedRoom(this.props.report);
-
return (
@@ -225,7 +216,7 @@ class ReportActionItemMessageEdit extends React.Component {
/>
InteractionManager.runAfterInteractions(() => this.textInput.focus())}
onEmojiSelected={this.addEmojiToTextBox}
/>