file_upload.jsx 14.4 KB
Newer Older
1
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
=Corey Hulen's avatar
=Corey Hulen committed
2 3
// See License.txt for license information.

4 5 6 7 8
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';
9
import DelayedAction from 'utils/delayed_action.jsx';
10
import * as UserAgent from 'utils/user_agent.jsx';
11
import * as Utils from 'utils/utils.jsx';
=Corey Hulen's avatar
=Corey Hulen committed
12

13
import {intlShape, injectIntl, defineMessages} from 'react-intl';
14

15 16
import {uploadFile} from 'actions/file_actions.jsx';

17 18 19 20 21 22 23 24 25 26 27 28
const holders = defineMessages({
    limited: {
        id: 'file_upload.limited',
        defaultMessage: 'Uploads limited to {count} 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}'
29 30 31 32
    },
    pasted: {
        id: 'file_upload.pasted',
        defaultMessage: 'Image Pasted at '
33 34 35
    }
});

36 37
import React from 'react';

38 39
const OverlayTimeout = 500;

40
class FileUpload extends React.Component {
=Corey Hulen's avatar
=Corey Hulen committed
41 42 43
    constructor(props) {
        super(props);

44
        this.uploadFiles = this.uploadFiles.bind(this);
=Corey Hulen's avatar
=Corey Hulen committed
45 46
        this.handleChange = this.handleChange.bind(this);
        this.handleDrop = this.handleDrop.bind(this);
47
        this.registerDragEvents = this.registerDragEvents.bind(this);
48
        this.cancelUpload = this.cancelUpload.bind(this);
David Lu's avatar
David Lu committed
49 50
        this.pasteUpload = this.pasteUpload.bind(this);
        this.keyUpload = this.keyUpload.bind(this);
51
        this.handleMaxUploadReached = this.handleMaxUploadReached.bind(this);
52
        this.emojiClick = this.emojiClick.bind(this);
=Corey Hulen's avatar
=Corey Hulen committed
53 54 55 56 57 58

        this.state = {
            requests: {}
        };
    }

=Corey Hulen's avatar
=Corey Hulen committed
59
    fileUploadSuccess(channelId, data) {
60
        this.props.onFileUpload(data.file_infos, data.client_ids, channelId);
=Corey Hulen's avatar
=Corey Hulen committed
61

62
        const requests = Object.assign({}, this.state.requests);
63
        for (var j = 0; j < data.client_ids.length; j++) {
64
            Reflect.deleteProperty(requests, data.client_ids[j]);
=Corey Hulen's avatar
=Corey Hulen committed
65
        }
66
        this.setState({requests});
=Corey Hulen's avatar
=Corey Hulen committed
67 68
    }

69 70
    fileUploadFail(clientId, channelId, err) {
        this.props.onUploadError(err, clientId, channelId);
=Corey Hulen's avatar
=Corey Hulen committed
71 72
    }

73 74 75
    uploadFiles(files) {
        // clear any existing errors
        this.props.onUploadError(null);
=Corey Hulen's avatar
=Corey Hulen committed
76

77
        const channelId = this.props.channelId || ChannelStore.getCurrentId();
78

79 80
        const uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId);
        let numUploads = 0;
=Corey Hulen's avatar
=Corey Hulen committed
81

82
        // keep track of how many files have been too large
83
        const tooLargeFiles = [];
84

85
        for (let i = 0; i < files.length && numUploads < uploadsRemaining; i++) {
86
            if (files[i].size > global.mm_config.MaxFileSize) {
87
                tooLargeFiles.push(files[i]);
=Corey Hulen's avatar
=Corey Hulen committed
88 89 90
                continue;
            }

91
            // generate a unique id that can be used by other components to refer back to this upload
92
            const clientId = Utils.generateId();
93

94 95 96 97 98 99 100 101
            const request = uploadFile(
                    files[i],
                    files[i].name,
                    channelId,
                    clientId,
                    this.fileUploadSuccess.bind(this, channelId),
                    this.fileUploadFail.bind(this, clientId)
                );
102

103
            const requests = this.state.requests;
104
            requests[clientId] = request;
105
            this.setState({requests});
106

107
            this.props.onUploadStart([clientId], channelId);
108 109

            numUploads += 1;
=Corey Hulen's avatar
=Corey Hulen committed
110 111
        }

112
        const {formatMessage} = this.props.intl;
113
        if (files.length > uploadsRemaining) {
114
            this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES}));
115 116 117
        } else if (tooLargeFiles.length > 1) {
            var tooLargeFilenames = tooLargeFiles.map((file) => file.name).join(', ');

118
            this.props.onUploadError(formatMessage(holders.filesAbove, {max: (global.mm_config.MaxFileSize / 1048576), filenames: tooLargeFilenames}));
119
        } else if (tooLargeFiles.length > 0) {
120
            this.props.onUploadError(formatMessage(holders.fileAbove, {max: (global.mm_config.MaxFileSize / 1048576), filename: tooLargeFiles[0].name}));
121 122 123
        }
    }

124 125 126
    handleChange(e) {
        if (e.target.files.length > 0) {
            this.uploadFiles(e.target.files);
127

128 129
            Utils.clearFileInput(e.target);
        }
130 131

        this.props.onFileUploadChange();
=Corey Hulen's avatar
=Corey Hulen committed
132 133 134
    }

    handleDrop(e) {
135 136 137 138 139
        if (global.window.mm_config.EnableFileAttachments === 'false') {
            this.props.onUploadError(Utils.localizeMessage('file_upload.disabled', 'File attachments are disabled.'));
            return;
        }

140 141 142
        this.props.onUploadError(null);

        var files = e.originalEvent.dataTransfer.files;
143 144

        if (typeof files !== 'string' && files.length) {
145
            this.uploadFiles(files);
146
        }
=Corey Hulen's avatar
=Corey Hulen committed
147 148 149
    }

    componentDidMount() {
150
        if (this.props.postType === 'post') {
151
            this.registerDragEvents('.row.main', '.center-file-overlay');
152
        } else if (this.props.postType === 'comment') {
153
            this.registerDragEvents('.post-right__container', '.right-file-overlay');
154 155
        }

156 157 158
        document.addEventListener('paste', this.pasteUpload);
        document.addEventListener('keydown', this.keyUpload);
    }
159

160 161 162 163 164 165 166 167 168 169 170
    registerDragEvents(containerSelector, overlaySelector) {
        const self = this;

        const overlay = $(overlaySelector);

        const dragTimeout = new DelayedAction(() => {
            if (!overlay.hasClass('hidden')) {
                overlay.addClass('hidden');
            }
        });

171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
        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);
201
                }
202 203 204 205 206
            };
        } else {
            dragsterActions = {
                drop(dragsterEvent, e) {
                    self.handleDrop(e);
207
                }
208 209
            };
        }
210

211
        $(containerSelector).dragster(dragsterActions);
212 213

        this.props.onFileUploadChange();
214 215
    }

216 217 218 219 220 221 222
    componentWillUnmount() {
        let target;
        if (this.props.postType === 'post') {
            target = $('.row.main');
        } else {
            target = $('.post-right__container');
        }
=Corey Hulen's avatar
=Corey Hulen committed
223

224 225
        document.removeEventListener('paste', this.pasteUpload);
        document.removeEventListener('keydown', this.keyUpload);
=Corey Hulen's avatar
=Corey Hulen committed
226

227 228 229
        // 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');
    }
230

231 232 233
    emojiClick() {
        this.props.onEmojiClick();
    }
234

235 236
    pasteUpload(e) {
        const {formatMessage} = this.props.intl;
=Corey Hulen's avatar
=Corey Hulen committed
237

238
        if (!e.clipboardData || !e.clipboardData.items) {
239 240
            return;
        }
=Corey Hulen's avatar
=Corey Hulen committed
241

242 243
        const textarea = ReactDOM.findDOMNode(this.props.getTarget());
        if (!textarea || !textarea.contains(e.target)) {
244 245
            return;
        }
246

247
        this.props.onUploadError(null);
=Corey Hulen's avatar
=Corey Hulen committed
248

249 250 251
        const items = [];
        for (let i = 0; i < e.clipboardData.items.length; i++) {
            const item = e.clipboardData.items[i];
252

253 254 255
            if (item.type.indexOf('image') === -1) {
                continue;
            }
256

257 258
            if (Constants.IMAGE_TYPES.indexOf(item.type.split('/')[1].toLowerCase()) === -1) {
                continue;
=Corey Hulen's avatar
=Corey Hulen committed
259
            }
260

261 262
            items.push(item);
        }
263

264 265
        // This looks redundant, but must be done this way due to
        // setState being an asynchronous call
266 267 268 269 270 271
        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;
            }

272 273 274
            var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - this.props.getFileCount(ChannelStore.getCurrentId()), items.length);

            if (items.length > numToUpload) {
275
                this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES}));
276
            }
=Corey Hulen's avatar
=Corey Hulen committed
277

278
            const channelId = this.props.channelId || ChannelStore.getCurrentId();
279

280 281
            for (var i = 0; i < items.length && i < numToUpload; i++) {
                var file = items[i].getAsFile();
282

283
                var ext = items[i].type.split('/')[1].toLowerCase();
284

285 286
                // generate a unique id that can be used by other components to refer back to this file upload
                var clientId = Utils.generateId();
287

288 289 290 291 292 293 294 295 296 297 298 299 300
                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());
                }
301

302
                const name = formatMessage(holders.pasted) + d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + hour + '-' + min + '.' + ext;
303

304 305
                const request = uploadFile(
                    file,
306 307 308 309 310 311
                    name,
                    channelId,
                    clientId,
                    this.fileUploadSuccess.bind(this, channelId),
                    this.fileUploadFail.bind(this, clientId)
                );
312

313 314 315
                const requests = this.state.requests;
                requests[clientId] = request;
                this.setState({requests});
316

317
                this.props.onUploadStart([clientId], channelId);
318
            }
319

320 321 322 323
            if (numToUpload > 0) {
                this.props.onFileUploadChange();
            }
        }
324
    }
325

326
    keyUpload(e) {
327
        if (Utils.cmdOrCtrlPressed(e) && e.keyCode === Constants.KeyCodes.U) {
328
            e.preventDefault();
329 330 331 332 333 334

            if (global.window.mm_config.EnableFileAttachments === 'false') {
                this.props.onUploadError(Utils.localizeMessage('file_upload.disabled', 'File attachments are disabled.'));
                return;
            }

Christopher Speller's avatar
Christopher Speller committed
335 336
            if ((this.props.postType === 'post' && document.activeElement.id === 'post_textbox') ||
                (this.props.postType === 'comment' && document.activeElement.id === 'reply_textbox')) {
337 338
                $(this.refs.fileInput).focus().trigger('click');
            }
339
        }
340 341
    }

=Corey Hulen's avatar
=Corey Hulen committed
342
    cancelUpload(clientId) {
343
        const requests = Object.assign({}, this.state.requests);
344
        const request = requests[clientId];
345 346 347 348

        if (request) {
            request.abort();

349
            Reflect.deleteProperty(requests, clientId);
350
            this.setState({requests});
351
        }
=Corey Hulen's avatar
=Corey Hulen committed
352 353
    }

354 355 356 357 358 359 360 361 362 363
    handleMaxUploadReached(e) {
        e.preventDefault();

        const {formatMessage} = this.props.intl;

        this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES}));

        return false;
    }

=Corey Hulen's avatar
=Corey Hulen committed
364
    render() {
365
        let multiple = true;
366
        if (UserAgent.isMobileApp()) {
367 368 369 370 371
            // iOS WebViews don't upload videos properly in multiple mode
            multiple = false;
        }

        let accept = '';
372
        if (UserAgent.isIosChrome()) {
373 374 375 376
            // iOS Chrome can't upload videos at all
            accept = 'image/*';
        }

377 378 379
        const channelId = this.props.channelId || ChannelStore.getCurrentId();

        const uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId);
380 381 382 383 384 385 386 387 388 389

        let emojiSpan;
        if (this.props.emojiEnabled) {
            emojiSpan = (
                <span
                    className={'fa fa-smile-o icon--emoji-picker emoji-' + this.props.navBarName}
                    onClick={this.emojiClick}
                />
            );
        }
390

391 392 393
        let fileDiv;
        if (global.window.mm_config.EnableFileAttachments === 'true') {
            fileDiv = (
394 395
                <div className='icon--attachment'>
                    <span
396
                        className='icon'
397 398 399 400 401 402 403 404 405 406 407
                        dangerouslySetInnerHTML={{__html: Constants.ATTACHMENT_ICON_SVG}}
                    />
                    <input
                        ref='fileInput'
                        type='file'
                        onChange={this.handleChange}
                        onClick={uploadsRemaining > 0 ? this.props.onClick : this.handleMaxUploadReached}
                        multiple={multiple}
                        accept={accept}
                    />
                </div>
408 409 410 411 412 413 414 415 416
            );
        }

        return (
            <span
                ref='input'
                className={'btn btn-file' + (uploadsRemaining <= 0 ? ' btn-file__disabled' : '')}
            >
                {fileDiv}
417
                {emojiSpan}
418
            </span>
=Corey Hulen's avatar
=Corey Hulen committed
419 420
        );
    }
=Corey Hulen's avatar
=Corey Hulen committed
421 422 423
}

FileUpload.propTypes = {
424
    intl: intlShape.isRequired,
=Corey Hulen's avatar
=Corey Hulen committed
425 426
    onUploadError: React.PropTypes.func,
    getFileCount: React.PropTypes.func,
427
    getTarget: React.PropTypes.func.isRequired,
hmhealey's avatar
hmhealey committed
428
    onClick: React.PropTypes.func,
=Corey Hulen's avatar
=Corey Hulen committed
429 430
    onFileUpload: React.PropTypes.func,
    onUploadStart: React.PropTypes.func,
431
    onFileUploadChange: React.PropTypes.func,
432
    onTextDrop: React.PropTypes.func,
=Corey Hulen's avatar
=Corey Hulen committed
433
    channelId: React.PropTypes.string,
434 435 436 437
    postType: React.PropTypes.string,
    onEmojiClick: React.PropTypes.func,
    navBarName: React.PropTypes.string,
    emojiEnabled: React.PropTypes.bool
=Corey Hulen's avatar
=Corey Hulen committed
438
};
439

440
export default injectIntl(FileUpload, {withRef: true});