From 3e8fd9d6e61ff22fb9831fd582b926e8ea3a159a Mon Sep 17 00:00:00 2001 From: or-else Date: Sun, 24 Sep 2017 18:21:14 -0700 Subject: [PATCH] file attachments work now --- css/base.css | 26 ++++++++++++--- drafty.js | 92 +++++++++++++++++++++++++++++++++++++++++++++------- index.html | 83 +++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 179 insertions(+), 22 deletions(-) diff --git a/css/base.css b/css/base.css index 3e36d88d..0355c4ca 100644 --- a/css/base.css +++ b/css/base.css @@ -258,6 +258,10 @@ ul.menu > li.separator:hover { color: #D32F2F; /* red 700 */ } +.gray { + color: #757575; +} + .hr { display: block; width: 100%; @@ -518,13 +522,13 @@ table.permission-editor td.checkbox { } .alert-box.error { - display: block; + display: flex; background-color: #ffcdd2; border-bottom: 1px solid #ffbfc8; color: #b71c1c; } .alert-box.warning { - display: block; + display: flex; background-color: #fff9c4; border-bottom: 1px solid #fff59d; color: #607d8b; @@ -539,7 +543,7 @@ table.permission-editor td.checkbox { color: #757575; } .alert-box .icon { - display: inline-block; + display: block; vertical-align: middle; margin-right: 0.5rem; } @@ -912,7 +916,7 @@ table.permission-editor td.checkbox { } #send-message-panel .material-icons { - min-width: 1.5rem; + min-width: 1.75rem; } #send-message-panel .material-icons.secondary, .material-icons.disabled { @@ -1103,6 +1107,17 @@ table.permission-editor td.checkbox { color:#777; } +/* Attachment styles */ +.attachment { + display: flex; +} +.attachment .material-icons { + vertical-align: middle; +} +.attachment:last-of-type { + margin-bottom: -1rem; +} + /* END of talk bubbles */ /* END of topic/mesages view */ @@ -1438,6 +1453,9 @@ ul.tabbar li.active > a:after { .material-icons.small { font-size: 1rem; } +.material-icons.big { + font-size: 2.4rem; +} #self-avatar.material-icons, .avatar-box .material-icons { font-size: 2.4rem; diff --git a/drafty.js b/drafty.js index fb70ec4c..b3fb8c8b 100644 --- a/drafty.js +++ b/drafty.js @@ -376,10 +376,16 @@ Sample JSON representation of the text above: } return { + /** - * Parse text into structured representation. + * Parse plain text into structured representation. */ parse: function(content) { + // Make sure we are parsing strings only. + if (typeof content != "string") { + return null; + } + // Split text into lines. It makes further processing easier. var lines = content.split(/\r?\n/); @@ -491,11 +497,16 @@ Sample JSON representation of the text above: * @param {string} fname file name suggestion for downloading the image. */ insertImage: function(content, at, mime, base64bits, width, height, fname) { - content = content || {txt: ""}; + content = content || {txt: " "}; content.ent = content.ent || []; content.fmt = content.fmt || []; - var entity = { + content.fmt.push({ + at: at, + len: 1, + key: content.ent.length + }); + content.ent.push({ tp: "IM", data: { mime: mime, @@ -504,15 +515,30 @@ Sample JSON representation of the text above: height: height, name: fname } - }; - var range = { - at: at, + }); + + return content; + }, + + attachFile: function(content, mime, base64bits, fname) { + content = content || {txt: ""}; + content.ent = content.ent || []; + content.fmt = content.fmt || []; + + content.fmt.push({ + at: -1, len: 0, key: content.ent.length - }; + }); - content.fmt.push(range); - content.ent.push(entity); + content.ent.push({ + tp: "EX", + data: { + mime: mime, + val: base64bits, + name: fname + } + }); return content; }, @@ -577,6 +603,8 @@ Sample JSON representation of the text above: format: function(content, formatter, context) { var {txt, fmt, ent} = content; + txt = txt || ""; + if (!fmt) { return [txt]; } @@ -625,13 +653,56 @@ Sample JSON representation of the text above: /** * Returns true if content has no markup and no entities. * - * @param {drafty} content - content to check for presence of markup. + * @param {Drafty} content - content to check for presence of markup. * @returns true is content is plain text, false otherwise. */ isPlainText: function(content, noLineBreaks) { return !(content.fmt || content.ent); }, + /** + * Check if the drafty content has attachments. + * + * @param {Drafty} content - content to check for attachments. + * @returns true if there are attachments. + */ + hasAttachment: function(content) { + if (content.ent && content.ent.length > 0) { + for (var i in content.ent) { + if (content.ent[i].tp == "EX") { + return true; + } + } + } + return false; + }, + + /** + * Enumerate attachments + * + * @param {Drafty} content - drafty object to process for attachments. + * @param {function} callback - callback to call for each attachment. + * @param {Object} content - value of "this" for callback. + */ + attachments: function(content, callback, context) { + if (content.ent && content.ent.length > 0) { + for (var i in content.ent) { + if (content.ent[i].tp == "EX") { + callback.call(context, content.ent[i].data, i); + } + } + } + }, + + // Get blob URL for a given entity + getBlobUrl: function(entity) { + return base64toObjectUrl(entity.val, entity.mime); + }, + + getAttachmentSize: function(entity) { + // Value is base64 encoded. The actual object size is smaller than the encoded length. + return entity.val ? (entity.val.length * 0.75) | 0 : 0; + }, /** * Get HTML tag for a given two-letter style name * @param {string} style - two-letter style, like ST or LN @@ -664,7 +735,6 @@ Sample JSON representation of the text above: getContentType: function() { return "text/x-drafty"; } - }; }); diff --git a/index.html b/index.html index 08bf7a19..03c4ff9e 100644 --- a/index.html +++ b/index.html @@ -53,6 +53,10 @@ // Number of chat messages to fetch in one call. var MESSAGES_PAGE = 24; + // Maximum allowed attachment size = 128K. Increade this limit to a + // greater value in production. Also increase max_message_size in server config. + var MAX_ATTACHMENT_SIZE = 1 << 17; + // Short representation of time in the past. function shortDateFormat(then) { var locale = window.navigator.userLanguage || window.navigator.language; @@ -144,6 +148,19 @@ return undefined; } + // Convert a number of bytes to human-readable format. + function bytesToHumanSize(bytes) { + if(!bytes || bytes == 0) { + return '0 Bytes'; + } + + var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + var bucket = Math.floor(Math.log2(bytes) / 10) | 0; + var count = bytes / Math.pow(1024, bucket); + var round = bucket > 0 ? (count < 10 ? 2 : (count < 100 ? 1 : 0)) : 0; + return count.toFixed(round) + ' ' + sizes[bucket]; + } + /* BEGIN Context Menu: popup/dropdown menu */ // Function is used by context menu to set permissions. @@ -2314,7 +2331,13 @@ (this.props.sequence === "single" || this.props.sequence === "last")); var content = this.props.content; + var attachments = []; if (this.props.mimeType === Drafty.getContentType()) { + Drafty.attachments(content, function(att, i) { + attachments.push(); + }, this); content = React.createElement('span', null, Drafty.format(content, formatter)); } @@ -2331,6 +2354,7 @@

{content} + {attachments}

@@ -2368,6 +2392,26 @@ } }); + Message.Attachment = React.createClass({ + render: function() { + var filename = this.props.filename || "file_attachment"; + if (filename.length > 36) { + filename = filename.substring(0, 16) + "..." + filename.substring(-16); + } + return ( +
+
insert_drive_file
+
+
{filename} ({this.props.size})
+ +
+
+ ); + } + }); + var MessagesView = React.createClass({ getInitialState: function() { return { @@ -2694,7 +2738,8 @@ + sendMessage={this.props.sendMessage} + onError={this.props.onError} />
); } else { @@ -2717,12 +2762,36 @@ keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1 }; }, - handleAttachImage: function() { + handleAttachImage: function(e) { + if (e.target.files && e.target.files.length > 0) { + var file = e.target.files[0]; + if (file.size > MAX_ATTACHMENT_SIZE) { + this.props.onError("The image size " + bytesToHumanSize(file.size) + + " exceeds the " + bytesToHumanSize(MAX_ATTACHMENT_SIZE) + " limit.", "err"); + return; + } + } }, - handleAttachFile: function() { + handleAttachFile: function(e) { + if (e.target.files && e.target.files.length > 0) { + var file = e.target.files[0]; + if (file.size > MAX_ATTACHMENT_SIZE) { + this.props.onError("The file size " + bytesToHumanSize(file.size) + + " exceeds the " + bytesToHumanSize(MAX_ATTACHMENT_SIZE) + " attachment limit.", "err"); + return; + } + + var reader = new FileReader(); + var instance = this; + reader.addEventListener("load", function () { + instance.props.sendMessage(Drafty.attachFile(null, file.type, reader.result.split(',')[1], file.name)); + }, false); + reader.readAsDataURL(file); + } }, + handleSend: function() { var message = this.state.message.trim(); if (message) { @@ -2762,12 +2831,12 @@
{this.props.disabled ? photo : - + photo } {this.props.disabled ? attach_file : - + attach_file }