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(
{content}
+ {attachments}