// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import $ from 'jquery'; import 'jquery-dragster/jquery.dragster.js'; import ReactDOM from 'react-dom'; import Constants from 'utils/constants.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; import * as UserAgent from 'utils/user_agent.jsx'; import * as Utils from 'utils/utils.jsx'; import {intlShape, injectIntl, defineMessages} from 'react-intl'; import {uploadFile} from 'actions/file_actions.jsx'; const holders = defineMessages({ limited: { id: 'file_upload.limited', defaultMessage: 'Uploads limited to {count, number} files maximum. Please use additional posts for more files.' }, filesAbove: { id: 'file_upload.filesAbove', defaultMessage: 'Files above {max}MB could not be uploaded: {filenames}' }, fileAbove: { id: 'file_upload.fileAbove', defaultMessage: 'File above {max}MB could not be uploaded: {filename}' }, pasted: { id: 'file_upload.pasted', defaultMessage: 'Image Pasted at ' } }); import PropTypes from 'prop-types'; import React from 'react'; const OverlayTimeout = 500; class FileUpload extends React.Component { constructor(props) { super(props); this.uploadFiles = this.uploadFiles.bind(this); this.handleChange = this.handleChange.bind(this); this.handleDrop = this.handleDrop.bind(this); this.registerDragEvents = this.registerDragEvents.bind(this); this.cancelUpload = this.cancelUpload.bind(this); this.pasteUpload = this.pasteUpload.bind(this); this.keyUpload = this.keyUpload.bind(this); this.handleMaxUploadReached = this.handleMaxUploadReached.bind(this); this.state = { requests: {} }; } fileUploadSuccess(channelId, data) { this.props.onFileUpload(data.file_infos, data.client_ids, channelId); const requests = Object.assign({}, this.state.requests); for (var j = 0; j < data.client_ids.length; j++) { Reflect.deleteProperty(requests, data.client_ids[j]); } this.setState({requests}); } fileUploadFail(clientId, channelId, err) { this.props.onUploadError(err, clientId, channelId); } uploadFiles(files) { // clear any existing errors this.props.onUploadError(null); const channelId = this.props.channelId || ChannelStore.getCurrentId(); const uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId); let numUploads = 0; // keep track of how many files have been too large const tooLargeFiles = []; for (let i = 0; i < files.length && numUploads < uploadsRemaining; i++) { if (files[i].size > global.mm_config.MaxFileSize) { tooLargeFiles.push(files[i]); continue; } // generate a unique id that can be used by other components to refer back to this upload const clientId = Utils.generateId(); const request = uploadFile( files[i], files[i].name, channelId, clientId, this.fileUploadSuccess.bind(this, channelId), this.fileUploadFail.bind(this, clientId, channelId) ); const requests = this.state.requests; requests[clientId] = request; this.setState({requests}); this.props.onUploadStart([clientId], channelId); numUploads += 1; } const {formatMessage} = this.props.intl; if (files.length > uploadsRemaining) { this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES})); } else if (tooLargeFiles.length > 1) { var tooLargeFilenames = tooLargeFiles.map((file) => file.name).join(', '); this.props.onUploadError(formatMessage(holders.filesAbove, {max: (global.mm_config.MaxFileSize / 1048576), filenames: tooLargeFilenames})); } else if (tooLargeFiles.length > 0) { this.props.onUploadError(formatMessage(holders.fileAbove, {max: (global.mm_config.MaxFileSize / 1048576), filename: tooLargeFiles[0].name})); } } handleChange(e) { if (e.target.files.length > 0) { this.uploadFiles(e.target.files); Utils.clearFileInput(e.target); } this.props.onFileUploadChange(); } handleDrop(e) { if (global.window.mm_config.EnableFileAttachments === 'false') { this.props.onUploadError(Utils.localizeMessage('file_upload.disabled', 'File attachments are disabled.')); return; } this.props.onUploadError(null); var files = e.originalEvent.dataTransfer.files; if (typeof files !== 'string' && files.length) { this.uploadFiles(files); } } componentDidMount() { if (this.props.postType === 'post') { this.registerDragEvents('.row.main', '.center-file-overlay'); } else if (this.props.postType === 'comment') { this.registerDragEvents('.post-right__container', '.right-file-overlay'); } document.addEventListener('paste', this.pasteUpload); document.addEventListener('keydown', this.keyUpload); } registerDragEvents(containerSelector, overlaySelector) { const self = this; const overlay = $(overlaySelector); const dragTimeout = new DelayedAction(() => { if (!overlay.hasClass('hidden')) { overlay.addClass('hidden'); } }); let dragsterActions = {}; if (global.window.mm_config.EnableFileAttachments === 'true') { dragsterActions = { enter(dragsterEvent, e) { var files = e.originalEvent.dataTransfer; if (Utils.isFileTransfer(files)) { $(overlaySelector).removeClass('hidden'); } }, leave(dragsterEvent, e) { var files = e.originalEvent.dataTransfer; if (Utils.isFileTransfer(files) && !overlay.hasClass('hidden')) { overlay.addClass('hidden'); } dragTimeout.cancel(); }, over() { dragTimeout.fireAfter(OverlayTimeout); }, drop(dragsterEvent, e) { if (!overlay.hasClass('hidden')) { overlay.addClass('hidden'); } dragTimeout.cancel(); self.handleDrop(e); } }; } else { dragsterActions = { drop(dragsterEvent, e) { self.handleDrop(e); } }; } $(containerSelector).dragster(dragsterActions); this.props.onFileUploadChange(); } componentWillUnmount() { let target; if (this.props.postType === 'post') { target = $('.row.main'); } else { target = $('.post-right__container'); } document.removeEventListener('paste', this.pasteUpload); document.removeEventListener('keydown', this.keyUpload); // jquery-dragster doesn't provide a function to unregister itself so do it manually target.off('dragenter dragleave dragover drop dragster:enter dragster:leave dragster:over dragster:drop'); } pasteUpload(e) { const {formatMessage} = this.props.intl; if (!e.clipboardData || !e.clipboardData.items) { return; } const textarea = ReactDOM.findDOMNode(this.props.getTarget()); if (!textarea || !textarea.contains(e.target)) { return; } this.props.onUploadError(null); const items = []; for (let i = 0; i < e.clipboardData.items.length; i++) { const item = e.clipboardData.items[i]; if (item.type.indexOf('image') === -1) { continue; } if (Constants.IMAGE_TYPES.indexOf(item.type.split('/')[1].toLowerCase()) === -1) { continue; } items.push(item); } // This looks redundant, but must be done this way due to // setState being an asynchronous call if (items && items.length > 0) { if (global.window.mm_config.EnableFileAttachments === 'false') { this.props.onUploadError(Utils.localizeMessage('file_upload.disabled', 'File attachments are disabled.')); return; } var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - this.props.getFileCount(ChannelStore.getCurrentId()), items.length); if (items.length > numToUpload) { this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES})); } const channelId = this.props.channelId || ChannelStore.getCurrentId(); for (var i = 0; i < items.length && i < numToUpload; i++) { var file = items[i].getAsFile(); var ext = items[i].type.split('/')[1].toLowerCase(); // generate a unique id that can be used by other components to refer back to this file upload var clientId = Utils.generateId(); var d = new Date(); var hour; if (d.getHours() < 10) { hour = '0' + d.getHours(); } else { hour = String(d.getHours()); } var min; if (d.getMinutes() < 10) { min = '0' + d.getMinutes(); } else { min = String(d.getMinutes()); } const name = formatMessage(holders.pasted) + d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + hour + '-' + min + '.' + ext; const request = uploadFile( file, name, channelId, clientId, this.fileUploadSuccess.bind(this, channelId), this.fileUploadFail.bind(this, clientId) ); const requests = this.state.requests; requests[clientId] = request; this.setState({requests}); this.props.onUploadStart([clientId], channelId); } if (numToUpload > 0) { this.props.onFileUploadChange(); } } } keyUpload(e) { if (Utils.cmdOrCtrlPressed(e) && e.keyCode === Constants.KeyCodes.U) { e.preventDefault(); if (global.window.mm_config.EnableFileAttachments === 'false') { this.props.onUploadError(Utils.localizeMessage('file_upload.disabled', 'File attachments are disabled.')); return; } if ((this.props.postType === 'post' && document.activeElement.id === 'post_textbox') || (this.props.postType === 'comment' && document.activeElement.id === 'reply_textbox')) { $(this.refs.fileInput).focus().trigger('click'); } } } cancelUpload(clientId) { const requests = Object.assign({}, this.state.requests); const request = requests[clientId]; if (request) { request.abort(); Reflect.deleteProperty(requests, clientId); this.setState({requests}); } } handleMaxUploadReached(e) { e.preventDefault(); const {formatMessage} = this.props.intl; this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES})); return false; } render() { let multiple = true; if (UserAgent.isMobileApp()) { // iOS WebViews don't upload videos properly in multiple mode multiple = false; } let accept = ''; if (UserAgent.isIosChrome()) { // iOS Chrome can't upload videos at all accept = 'image/*'; } const channelId = this.props.channelId || ChannelStore.getCurrentId(); const uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId); let fileDiv; if (global.window.mm_config.EnableFileAttachments === 'true') { fileDiv = (