refactoring upload progress

This commit is contained in:
or-else
2018-08-07 20:55:55 +03:00
parent df86411cb7
commit 4cd97e7ff8
+78 -118
View File
@@ -240,6 +240,7 @@ function fileToBase64(file, onSuccess, onError) {
} }
// File pasted from the clipboard. It's either an inline image or a file attachment. // File pasted from the clipboard. It's either an inline image or a file attachment.
// FIXME: handle large files out of band.
function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) { function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
var items = (event.clipboardData || event.originalEvent.clipboardData || {}).items; var items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
for (var i in items) { for (var i in items) {
@@ -963,14 +964,10 @@ class ChipInput extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
// FIXME: these two methods should be static.
this.indexChips = this.indexChips.bind(this);
this.sortChips = this.sortChips.bind(this);
this.state = { this.state = {
placeholder: props.chips ? "" : props.prompt, placeholder: props.chips ? "" : props.prompt,
sortedChips: this.sortChips(props.chips, props.required), sortedChips: ChipInput.sortChips(props.chips, props.required),
chipIndex: this.indexChips(props.chips), chipIndex: ChipInput.indexChips(props.chips),
input: '', input: '',
focused: false focused: false
}; };
@@ -985,8 +982,8 @@ class ChipInput extends React.Component {
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.setState({ this.setState({
sortedChips: this.sortChips(nextProps.chips, nextProps.required), sortedChips: ChipInput.sortChips(nextProps.chips, nextProps.required),
chipIndex: this.indexChips(nextProps.chips) chipIndex: ChipInput.indexChips(nextProps.chips)
}); });
if (nextProps.chips.length > this.props.chips.length) { if (nextProps.chips.length > this.props.chips.length) {
// Chip added: clear input. // Chip added: clear input.
@@ -995,8 +992,7 @@ class ChipInput extends React.Component {
} }
// Map chip index to user name // Map chip index to user name
// FIXME: convert to static method. static indexChips(chips) {
indexChips(chips) {
var index = {}; var index = {};
var count = 0; var count = 0;
chips.map(function(item) { chips.map(function(item) {
@@ -1007,8 +1003,7 @@ class ChipInput extends React.Component {
} }
// Have non-removable chips appear before all other chips. // Have non-removable chips appear before all other chips.
// FIXME: convert to static method. static sortChips(chips, keep) {
sortChips(chips, keep) {
var required = []; var required = [];
var normal = []; var normal = [];
chips.map(function(item) { chips.map(function(item) {
@@ -2043,7 +2038,7 @@ class UnreadBadge extends React.PureComponent {
}; };
/* Contact's labels: [you], [muted], [blocked], etc */ /* Contact's labels: [you], [muted], [blocked], etc */
// FIXME: this class is unused // FIXME: this class is unused.
class ContactBadges_UNUSED_REMOVE extends React.Component { class ContactBadges_UNUSED_REMOVE extends React.Component {
render() { render() {
var badges = null; var badges = null;
@@ -2065,7 +2060,6 @@ class ContactsView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
// FIXME: make static
this.prepareContactList = this.prepareContactList.bind(this); this.prepareContactList = this.prepareContactList.bind(this);
this.state = { this.state = {
@@ -2153,7 +2147,6 @@ class ContactsView extends React.Component {
this.resetContactList(); this.resetContactList();
} }
// FIXME: make this method static.
prepareContactList() { prepareContactList() {
var instance = this; var instance = this;
var contacts = []; var contacts = [];
@@ -2283,10 +2276,10 @@ class GroupManager extends React.Component {
this.state = { this.state = {
members: props.members, members: props.members,
index: this.indexMembers(props.members), index: GroupManager.indexMembers(props.members),
contactFilter: '', contactFilter: '',
noContactsMessage: 'You have no contacts :-(', noContactsMessage: 'You have no contacts :-(',
selectedContacts: this.selectedContacts(props.members) selectedContacts: GroupManager.selectedContacts(props.members)
}; };
this.handleContactSelected = this.handleContactSelected.bind(this); this.handleContactSelected = this.handleContactSelected.bind(this);
@@ -2296,8 +2289,7 @@ class GroupManager extends React.Component {
this.handleCancel = this.handleCancel.bind(this); this.handleCancel = this.handleCancel.bind(this);
} }
// FIXME: make method static static indexMembers(members) {
indexMembers(members) {
var index = {}; var index = {};
members.map(function(m) { members.map(function(m) {
index[m.user] = {delta: 0, present: true}; // Delta: 0 unchanged, +1 added, -1 removed index[m.user] = {delta: 0, present: true}; // Delta: 0 unchanged, +1 added, -1 removed
@@ -2305,8 +2297,7 @@ class GroupManager extends React.Component {
return index; return index;
} }
// FIXME: make method static static selectedContacts(members) {
selectedContacts(members) {
var sel = []; var sel = [];
members.map(function(m) { members.map(function(m) {
sel.push(m.user); sel.push(m.user);
@@ -2365,8 +2356,7 @@ class GroupManager extends React.Component {
this.setState({contactFilter: val, noContactsMessage: msg}); this.setState({contactFilter: val, noContactsMessage: msg});
} }
// FIXME: make static static doContactFiltering(filter, values) {
doContactFiltering(filter, values) {
if (filter) { if (filter) {
for (var i=0; i<values.length; i++) { for (var i=0; i<values.length; i++) {
if (values[i].indexOf(filter) >= 0) { if (values[i].indexOf(filter) >= 0) {
@@ -2424,7 +2414,7 @@ class GroupManager extends React.Component {
contacts={this.props.contacts} contacts={this.props.contacts}
topicSelected={this.state.selectedContacts} topicSelected={this.state.selectedContacts}
filter={this.state.contactFilter} filter={this.state.contactFilter}
filterFunc={this.doContactFiltering} filterFunc={GroupManager.doContactFiltering}
emptyListMessage={this.state.noContactsMessage} emptyListMessage={this.state.noContactsMessage}
showOnline={false} showOnline={false}
showUnread={false} showUnread={false}
@@ -3384,12 +3374,16 @@ class ChatMessage extends React.Component {
var content = this.props.content; var content = this.props.content;
var attachments = []; var attachments = [];
if (this.props.mimeType === Drafty.getContentType()) { if (this.props.mimeType == Drafty.getContentType()) {
Drafty.attachments(content, function(att, i) { Drafty.attachments(content, function(att, i) {
attachments.push(<AttachmentProgress attachments.push(<Attachment
filename={att.name} downloadUrl={Drafty.getDownloadUrl(att)} downloadUrl={Drafty.getDownloadUrl(att)}
filename={att.name} uploader={Drafty.isUploading(att)}
mimetype={att.mime} size={Drafty.getEntitySize(att)} mimetype={att.mime} size={Drafty.getEntitySize(att)}
onError={this.props.onError} key={i} />); progress={this.props.progress}
onCancelUpload={this.props.onCancelUpload}
onError={this.props.onError}
key={i} />);
}, this); }, this);
content = React.createElement('span', null, Drafty.format(content, formatter, this)); content = React.createElement('span', null, Drafty.format(content, formatter, this));
} }
@@ -3408,8 +3402,10 @@ class ChatMessage extends React.Component {
<div className={bubbleClass}> <div className={bubbleClass}>
<p>{content} <p>{content}
{attachments} {attachments}
<ReceivedMarker response={this.props.response} <ReceivedMarker
timestamp={this.props.timestamp} received={this.props.received} /> response={this.props.response}
timestamp={this.props.timestamp}
received={this.props.received} />
</p> </p>
<span className="menuTrigger"> <span className="menuTrigger">
<a href="javascript:;" onClick={this.handleContextClick}> <a href="javascript:;" onClick={this.handleContextClick}>
@@ -3430,7 +3426,7 @@ class MessageUploader extends React.PureComponent {
var bubbleClass = "bubble tip"; var bubbleClass = "bubble tip";
var attachments = [ var attachments = [
<AttachmentProgress <Attachment
uploader={true} filename={this.props.filename} size={this.props.size} uploader={true} filename={this.props.filename} size={this.props.size}
progress={this.props.progress} progress={this.props.progress}
onCancelUpload={this.props.onCancelUpload} onCancelUpload={this.props.onCancelUpload}
@@ -3442,8 +3438,10 @@ class MessageUploader extends React.PureComponent {
<div> <div>
<div className="bubble tip"> <div className="bubble tip">
<p>{attachments} <p>{attachments}
<ReceivedMarker response={this.props.response} <ReceivedMarker
timestamp={this.props.timestamp} received={this.props.received} /> response={this.props.response}
timestamp={this.props.timestamp}
received={this.props.received} />
</p> </p>
</div> </div>
</div> </div>
@@ -3479,7 +3477,7 @@ class ReceivedMarker extends React.PureComponent {
} }
}; };
class AttachmentProgress extends React.Component { class Attachment extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -3517,25 +3515,24 @@ class AttachmentProgress extends React.Component {
} }
render() { render() {
var filename = this.props.filename || "file_attachment"; let filename = this.props.filename || "file_attachment";
if (filename.length > 36) { if (filename.length > 36) {
filename = filename.substr(0, 16) + "..." + filename.substr(-16); filename = filename.substr(0, 16) + "..." + filename.substr(-16);
} }
var size = this.props.size > 0 ? let size = this.props.size > 0 ?
<span className="small gray">({bytesToHumanSize(this.props.size)})</span> : <span className="small gray">({bytesToHumanSize(this.props.size)})</span> :
null; null;
// Detect if the download URL is relative or absolute. // Detect if the download URL is relative or absolute.
// If the URL is relative use LargeFileHelper to attach authentication // If the URL is relative use LargeFileHelper to attach authentication
// credentials to the request. // credentials to the request.
var url, helperFunc; let url, helperFunc;
if (!this.props.uploader && !this.state.downloader && if (!this.props.uploader && !this.state.downloader &&
!(/^(?:(?:[a-z]+:)?\/\/)/i.test(this.props.downloadUrl))) { !(/^(?:(?:[a-z]+:)?\/\/)/i.test(this.props.downloadUrl))) {
// Relative URL. Use download helper. // Relative URL. Use download helper.
url = "javascript:;"; url = "javascript:;";
var instance = this; helperFunc = (e) => {
helperFunc = function(e) { this.downloadFile(this.props.downloadUrl, this.props.filename, this.props.mimetype);
instance.downloadFile(instance.props.downloadUrl, instance.props.filename, instance.props.mimetype);
}; };
} else { } else {
url = this.props.downloadUrl; url = this.props.downloadUrl;
@@ -3591,8 +3588,6 @@ class MessagesView extends React.Component {
this.handleContextClick = this.handleContextClick.bind(this); this.handleContextClick = this.handleContextClick.bind(this);
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this); this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
this.handleBackNavigation = this.handleBackNavigation.bind(this); this.handleBackNavigation = this.handleBackNavigation.bind(this);
this.handleUploadProgress = this.handleUploadProgress.bind(this);
this.handleCancelUpload = this.handleCancelUpload.bind(this);
} }
// Scroll last message into view on component update e.g. on message received. // Scroll last message into view on component update e.g. on message received.
@@ -3865,17 +3860,6 @@ class MessagesView extends React.Component {
this.props.onHideMessagesView(); this.props.onHideMessagesView();
} }
handleUploadProgress(progress) {
this.setState({uploadProgress: progress});
}
handleCancelUpload() {
if (this.state.uploadFile && this.state.uploadFile.uploader) {
this.state.uploadFile.uploader.cancel();
}
this.setState({uploadFile: null});
}
render() { render() {
var component = null; var component = null;
if (this.state.topic) { if (this.state.topic) {
@@ -3999,7 +3983,6 @@ class MessagesView extends React.Component {
topic={this.props.topic} topic={this.props.topic}
disabled={this.state.readOnly} disabled={this.state.readOnly}
sendMessage={this.props.sendMessage} sendMessage={this.props.sendMessage}
onUploadProgress={this.handleUploadProgress}
onError={this.props.onError} /> onError={this.props.onError} />
{this.state.imagePreview ? {this.state.imagePreview ?
<ImagePreview content={this.state.imagePreview} <ImagePreview content={this.state.imagePreview}
@@ -4048,52 +4031,51 @@ class SendMessage extends React.PureComponent {
if (this.props.disabled) { if (this.props.disabled) {
return; return;
} }
var instance = this; // FIXME: handle large files too.
if (filePasted(e, if (filePasted(e,
function(bits, mime, width, height, fname) { (bits, mime, width, height, fname) => {
instance.props.sendMessage(Drafty.insertImage(null, this.props.sendMessage(Drafty.insertImage(null,
0, mime, bits, width, height, fname)); 0, mime, bits, width, height, fname));
}, },
function(mime, bits, fname) { (mime, bits, fname) => {
instance.props.sendMessage(Drafty.attachFile(null, mime, bits, fname)); this.props.sendMessage(Drafty.attachFile(null, mime, bits, fname));
}, },
instance.props.onError)) { this.props.onError)) {
// If file was pasted, don't paste base64 daata into input field. // If a file was pasted, don't paste base64 data into input field.
e.preventDefault(); e.preventDefault();
} }
} }
handleAttachImage(e) { handleAttachImage(e) {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
var instance = this; let file = e.target.files[0];
var file = e.target.files[0];
// Check if the uploaded file is indeed an image and if it isn't too large. // Check if the uploaded file is indeed an image and if it isn't too large.
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) { if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
// Convert image for size or format. // Convert image for size or format.
imageFileScaledToBase64(file, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, false, imageFileScaledToBase64(file, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, false,
// Success // Success
function(bits, mime, width, height, fname) { (bits, mime, width, height, fname) => {
instance.props.sendMessage(Drafty.insertImage(null, this.props.sendMessage(Drafty.insertImage(null,
0, mime, bits, width, height, fname)); 0, mime, bits, width, height, fname));
}, },
// Failure // Failure
function(err) { (err) => {
instance.props.onError(err, "err"); this.props.onError(err, "err");
}); });
} else { } else {
console.log("File size is OK", file.size, MAX_INBAND_ATTACHMENT_SIZE);
// Image can be uploaded as is. No conversion is needed. // Image can be uploaded as is. No conversion is needed.
imageFileToBase64(file, imageFileToBase64(file,
// Success // Success
function(bits, mime, width, height, fname) { (bits, mime, width, height, fname) => {
instance.props.sendMessage(Drafty.insertImage(null, this.props.sendMessage(Drafty.insertImage(null,
0, mime, bits, width, height, fname)); 0, mime, bits, width, height, fname));
}, },
// Failure // Failure
function(err) { (err) => {
instance.props.onError(err, "err"); this.props.onError(err, "err");
}); }
);
} }
} }
// Clear the value so the same file can be uploaded again. // Clear the value so the same file can be uploaded again.
@@ -4102,32 +4084,31 @@ class SendMessage extends React.PureComponent {
handleAttachFile(e) { handleAttachFile(e) {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
var instance = this;
var file = e.target.files[0]; var file = e.target.files[0];
if (file.size > MAX_EXTERN_ATTACHMENT_SIZE) { if (file.size > MAX_EXTERN_ATTACHMENT_SIZE) {
// Too large. // Too large.
this.props.onError("The file size " + bytesToHumanSize(file.size) + this.props.onError("The file size " + bytesToHumanSize(file.size) +
" exceeds the " + bytesToHumanSize(MAX_EXTERN_ATTACHMENT_SIZE) + " limit.", "err"); " exceeds the " + bytesToHumanSize(MAX_EXTERN_ATTACHMENT_SIZE) + " limit.", "err");
} else if (file.size > MAX_INBAND_ATTACHMENT_SIZE) { } else if (file.size > MAX_INBAND_ATTACHMENT_SIZE) {
// To large to send inband - uploading out of band and sending as a link. // Too large to send inband - uploading out of band and sending as a link.
var uploader = Tinode.getLargeFileHelper(); let uploader = Tinode.getLargeFileHelper();
if (!uploader) { if (!uploader) {
this.props.onError("Cannot initiate file upload."); this.props.onError("Cannot initiate file upload.");
return; return;
} }
this.props.sendMessage( // Format data and initiate upload.
Drafty.attachFile(null, file.type, null, file.name, ctrl.params.url, file.size), let uploadCompletionPromise = uploader.upload(file);
uploader.upload(file, function(progress) { let msg = Drafty.attachFile(null, file.type, null, file.name, file.size, uploadCompletionPromise);
instance.props.onUploadProgress(progress); // Pass data and the uploader to the AppView.
}) this.props.sendMessage(msg, uploadCompletionPromise, uploader);
);
} else { } else {
// Small enough to send inband. // Small enough to send inband.
fileToBase64(file, fileToBase64(file,
function(mime, bits, fname) { (mime, bits, fname) => {
instance.props.sendMessage(Drafty.attachFile(null, mime, bits, fname)); this.props.sendMessage(Drafty.attachFile(null, mime, bits, fname));
}, },
this.props.onError); this.props.onError
);
} }
} }
// Clear the value so the same file can be uploaded again. // Clear the value so the same file can be uploaded again.
@@ -4380,7 +4361,7 @@ class AppView extends React.Component {
Tinode.enableLogging(true, true); Tinode.enableLogging(true, true);
Tinode.onConnect = this.handleConnected; Tinode.onConnect = this.handleConnected;
Tinode.onDisconnect = this.handleDisconnect; Tinode.onDisconnect = this.handleDisconnect;
this.tnSetup(this.state.serverAddress, this.state.transport); AppView.tnSetup(this.state.serverAddress, this.state.transport);
var token; var token;
if (localStorage.getObject("keep-logged-in")) { if (localStorage.getObject("keep-logged-in")) {
token = localStorage.getObject("auth-token"); token = localStorage.getObject("auth-token");
@@ -4412,8 +4393,7 @@ class AppView extends React.Component {
} }
// Setup transport (usually websocket) and server address. This will terminate connection with the server. // Setup transport (usually websocket) and server address. This will terminate connection with the server.
// FIXME: make static static tnSetup(serverAddress, transport) {
tnSetup(serverAddress, transport) {
Tinode.setup(APP_NAME, serverAddress, API_KEY, transport); Tinode.setup(APP_NAME, serverAddress, API_KEY, transport);
} }
@@ -4713,35 +4693,15 @@ class AppView extends React.Component {
window.location.hash = setUrlTopic(window.location.hash, null); window.location.hash = setUrlTopic(window.location.hash, null);
} }
// User clicked on a message send button. // User is sending a message, either plain text or a drafty object with attachments.
handleSendMessage(msg, promise) { handleSendMessage(msg, promise, uploader) {
var topic = Tinode.getTopic(this.state.topicSelected); let topic = Tinode.getTopic(this.state.topicSelected);
var instance = this; msg = topic.createMessage(Drafty.parse(msg) || msg);
// Check if the message is formatted.
var dft = Drafty.parse(msg) || msg; topic.publishDraft(msg, promise)
if (Drafty.isPlainText(dft)) { .catch((err) => {
// The message is unformatted text. Send if as text/plain. this.handleError(err.message, "err");
topic.publishDraft(topic.createMessage(msg), promise)
.catch(function(err) {
instance.handleError(err.message, "err");
}); });
} else {
// The message is a Drafty object.
var attachments = [];
Drafty.attachments(dft, function(data) {
if (data.ref) {
attachments.push(data.ref);
}
});
if (attachments.length < 1) {
attachments = null;
}
// Formatted message. Send it as a Drafty object.
topic.publishDraft(topic.createMessage(dft, undefined, Drafty.getContentType(), attachments), promise)
.catch(function(err) {
instance.handleError(err.message, "err");
});
}
} }
// User chose a Sign Up menu item. // User chose a Sign Up menu item.
@@ -4799,7 +4759,7 @@ class AppView extends React.Component {
transport = transport || this.state.transport; transport = transport || this.state.transport;
serverAddress = serverAddress || this.state.serverAddress; serverAddress = serverAddress || this.state.serverAddress;
this.setState({serverAddress: serverAddress, transport: transport, sidePanelSelected: 'login'}); this.setState({serverAddress: serverAddress, transport: transport, sidePanelSelected: 'login'});
this.tnSetup(serverAddress, transport); AppView.tnSetup(serverAddress, transport);
} }
// User clicked Cancel button in Setting or Sign Up panel. // User clicked Cancel button in Setting or Sign Up panel.
@@ -4889,7 +4849,7 @@ class AppView extends React.Component {
handleLogout() { handleLogout() {
localStorage.removeItem("auth-token"); localStorage.removeItem("auth-token");
this.setState(this.getInitialState()); this.setState(this.getInitialState());
this.tnSetup(this.state.serverAddress, this.state.transport); AppView.tnSetup(this.state.serverAddress, this.state.transport);
window.location.hash = ""; window.location.hash = "";
} }