Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Check upload limits before trying to upload large files #1876

Merged
merged 11 commits into from
Dec 4, 2018
4 changes: 2 additions & 2 deletions src/ContentMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
});
}
Expand Down
42 changes: 41 additions & 1 deletion src/components/structures/RoomView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -156,14 +161,35 @@ 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);

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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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'});

Expand Down Expand Up @@ -1687,6 +1726,7 @@ module.exports = React.createClass({
callState={this.state.callState}
disabled={this.props.disabled}
showApps={this.state.showApps}
uploadAllowed={this.isFileUploadAllowed}
/>;
}

Expand Down
63 changes: 51 additions & 12 deletions src/components/views/rooms/MessageComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,30 @@ export default class MessageComposer extends React.Component {
}

onUploadFileSelected(files) {
this.uploadFiles(files.target.files);
const tfiles = files.target.files;
this.uploadFiles(tfiles);
}

uploadFiles(files) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const TintableSvg = sdk.getComponent("elements.TintableSvg");

const fileList = [];
const acceptedFiles = [];
const failedFiles = [];

for (let i=0; i<files.length; i++) {
fileList.push(<li key={i}>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> { files[i].name || _t('Attachment') }
</li>);
const fileAcceptedOrError = this.props.uploadAllowed(files[i]);
if (fileAcceptedOrError === true) {
acceptedFiles.push(<li key={i}>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> { files[i].name || _t('Attachment') }
</li>);
fileList.push(files[i]);
} else {
failedFiles.push(<li key={i}>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> { files[i].name || _t('Attachment') } <p>{ _t('Reason') + ": " + fileAcceptedOrError}</p>
</li>);
}
}

const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
Expand All @@ -161,23 +173,47 @@ export default class MessageComposer extends React.Component {
}</p>;
}

const acceptedFilesPart = acceptedFiles.length === 0 ? null : (
<div>
<p>{ _t('Are you sure you want to upload the following files?') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}>
{ acceptedFiles }
</ul>
</div>
);

const failedFilesPart = failedFiles.length === 0 ? null : (
<div>
<p>{ _t('The following files cannot be uploaded:') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}>
{ failedFiles }
</ul>
</div>
);
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: (
<div>
<p>{ _t('Are you sure you want to upload the following files?') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}>
{ fileList }
</ul>
{ acceptedFilesPart }
{ failedFilesPart }
{ replyToWarning }
</div>
),
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<files.length; i++) {
this.props.uploadFile(files[i]);
if (fileList) {
for (let i=0; i<fileList.length; i++) {
this.props.uploadFile(fileList[i]);
}
}
}
Expand Down Expand Up @@ -459,6 +495,9 @@ MessageComposer.propTypes = {
// callback when a file to upload is chosen
uploadFile: PropTypes.func.isRequired,

// function to test whether a file should be allowed to be uploaded.
uploadAllowed: PropTypes.func.isRequired,

// string representing the current room app drawer state
showApps: PropTypes.bool,
showApps: PropTypes.bool
};
9 changes: 8 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1351,5 +1351,12 @@
"Import": "Import",
"Failed to set direct chat tag": "Failed to set direct chat tag",
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room",
"File is too big. Maximum file size is %(fileSize)s": "File is too big. Maximum file size is %(fileSize)s",
"Reason": "Reason",
"The following files cannot be uploaded:": "The following files cannot be uploaded:",
"You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.",
"If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.",
"Incompatible local cache": "Incompatible local cache",
"Clear cache and resync": "Clear cache and resync"
}