diff --git a/src/ContentMessages.js b/src/ContentMessages.js index fd21977108..f2bbdfafe5 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -377,9 +377,9 @@ class ContentMessages { } } if (error) { - dis.dispatch({action: 'upload_failed', upload: upload}); + dis.dispatch({action: 'upload_failed', upload, error}); } else { - dis.dispatch({action: 'upload_finished', upload: upload}); + dis.dispatch({action: 'upload_finished', upload}); } }); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index dd57bd7636..934031e98d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -27,6 +27,7 @@ const React = require("react"); const ReactDOM = require("react-dom"); import PropTypes from 'prop-types'; import Promise from 'bluebird'; +import filesize from 'filesize'; const classNames = require("classnames"); import { _t } from '../../languageHandler'; @@ -101,6 +102,10 @@ module.exports = React.createClass({ roomLoading: true, peekLoading: false, shouldPeek: true, + + // Media limits for uploading. + mediaConfig: undefined, + // used to trigger a rerender in TimelinePanel once the members are loaded, // so RR are rendered again (now with the members available), ... membersLoaded: !llMembers, @@ -156,7 +161,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); - + this._fetchMediaConfig(); // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(true); @@ -164,6 +169,27 @@ module.exports = React.createClass({ WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate); }, + _fetchMediaConfig: function(invalidateCache: boolean = false) { + /// NOTE: Using global here so we don't make repeated requests for the + /// config every time we swap room. + if(global.mediaConfig !== undefined && !invalidateCache) { + this.setState({mediaConfig: global.mediaConfig}); + return; + } + console.log("[Media Config] Fetching"); + MatrixClientPeg.get().getMediaConfig().then((config) => { + console.log("[Media Config] Fetched config:", config); + return config; + }).catch(() => { + // Media repo can't or won't report limits, so provide an empty object (no limits). + console.log("[Media Config] Could not fetch config, so not limiting uploads."); + return {}; + }).then((config) => { + global.mediaConfig = config; + this.setState({mediaConfig: config}); + }); + }, + _onRoomViewStoreUpdate: function(initial) { if (this.unmounted) { return; @@ -499,6 +525,10 @@ module.exports = React.createClass({ break; case 'notifier_enabled': case 'upload_failed': + // 413: File was too big or upset the server in some way. + if(payload.error.http_status === 413) { + this._fetchMediaConfig(true); + } case 'upload_started': case 'upload_finished': this.forceUpdate(); @@ -931,6 +961,15 @@ module.exports = React.createClass({ this.setState({ draggingFile: false }); }, + isFileUploadAllowed(file) { + if (this.state.mediaConfig !== undefined && + this.state.mediaConfig["m.upload.size"] !== undefined && + file.size > this.state.mediaConfig["m.upload.size"]) { + return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.state.mediaConfig["m.upload.size"])}); + } + return true; + }, + uploadFile: async function(file) { dis.dispatch({action: 'focus_composer'}); @@ -1687,6 +1726,7 @@ module.exports = React.createClass({ callState={this.state.callState} disabled={this.props.disabled} showApps={this.state.showApps} + uploadAllowed={this.isFileUploadAllowed} />; } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 8df6a76836..3fa0f888df 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -139,7 +139,8 @@ export default class MessageComposer extends React.Component { } onUploadFileSelected(files) { - this.uploadFiles(files.target.files); + const tfiles = files.target.files; + this.uploadFiles(tfiles); } uploadFiles(files) { @@ -147,10 +148,21 @@ export default class MessageComposer extends React.Component { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const fileList = []; + const acceptedFiles = []; + const failedFiles = []; + for (let i=0; i - { files[i].name || _t('Attachment') } - ); + const fileAcceptedOrError = this.props.uploadAllowed(files[i]); + if (fileAcceptedOrError === true) { + acceptedFiles.push(
  • + { files[i].name || _t('Attachment') } +
  • ); + fileList.push(files[i]); + } else { + failedFiles.push(
  • + { files[i].name || _t('Attachment') }

    { _t('Reason') + ": " + fileAcceptedOrError}

    +
  • ); + } } const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); @@ -161,23 +173,47 @@ export default class MessageComposer extends React.Component { }

    ; } + const acceptedFilesPart = acceptedFiles.length === 0 ? null : ( +
    +

    { _t('Are you sure you want to upload the following files?') }

    +
      + { acceptedFiles } +
    +
    + ); + + const failedFilesPart = failedFiles.length === 0 ? null : ( +
    +

    { _t('The following files cannot be uploaded:') }

    +
      + { failedFiles } +
    +
    + ); + let buttonText; + if (acceptedFiles.length > 0 && failedFiles.length > 0) { + buttonText = "Upload selected" + } else if (failedFiles.length > 0) { + buttonText = "Close" + } + Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, { title: _t('Upload Files'), description: (
    -

    { _t('Are you sure you want to upload the following files?') }

    -
      - { fileList } -
    + { acceptedFilesPart } + { failedFilesPart } { replyToWarning }
    ), + hasCancelButton: acceptedFiles.length > 0, + button: buttonText, onFinished: (shouldUpload) => { if (shouldUpload) { // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file - if (files) { - for (let i=0; i