file attachments work now
This commit is contained in:
+22
-4
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
+76
-7
@@ -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(<Message.Attachment
|
||||
filename={att.name} blobUrl={Drafty.getBlobUrl(att)}
|
||||
size={bytesToHumanSize(Drafty.getAttachmentSize(att))} key={i} />);
|
||||
}, this);
|
||||
content = React.createElement('span', null, Drafty.format(content, formatter));
|
||||
}
|
||||
|
||||
@@ -2331,6 +2354,7 @@
|
||||
<div>
|
||||
<div className={bubbleClass}>
|
||||
<p>{content}
|
||||
{attachments}
|
||||
<Message.ReceivedMarker response={this.props.response}
|
||||
timestamp={this.props.timestamp} received={this.props.received} />
|
||||
</p>
|
||||
@@ -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 (
|
||||
<div className="attachment">
|
||||
<div><i className="material-icons big gray">insert_drive_file</i></div>
|
||||
<div className="flex-column">
|
||||
<div>{filename} <span className="small">({this.props.size})</span></div>
|
||||
<div><a href={this.props.blobUrl} download={this.props.filename}>
|
||||
<i className="material-icons">file_download</i> save
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var MessagesView = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
@@ -2694,7 +2738,8 @@
|
||||
<MessagesView.Send
|
||||
topic={this.props.topic}
|
||||
disabled={this.state.readOnly}
|
||||
sendMessage={this.props.sendMessage} />
|
||||
sendMessage={this.props.sendMessage}
|
||||
onError={this.props.onError} />
|
||||
</div>
|
||||
);
|
||||
} 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 @@
|
||||
<div id="send-message-panel">
|
||||
{this.props.disabled ?
|
||||
<i className="material-icons disabled">photo</i> :
|
||||
<a href="#" onClick={function(e) {instance.attachImage.click();}}>
|
||||
<a href="#" onClick={function(e) {instance.attachImage.click();}} title="Add image">
|
||||
<i className="material-icons secondary">photo</i>
|
||||
</a>}
|
||||
{this.props.disabled ?
|
||||
<i className="material-icons disabled">attach_file</i> :
|
||||
<a href="#" onClick={function(e) {instance.attachFile.click();}}>
|
||||
<a href="#" onClick={function(e) {instance.attachFile.click();}} title="Attach file">
|
||||
<i className="material-icons secondary">attach_file</i>
|
||||
</a>}
|
||||
<textarea id="sendMessage" placeholder={prompt}
|
||||
@@ -2775,7 +2844,7 @@
|
||||
onChange={this.handleMessageTyping} onKeyPress={this.handleKeyPress} />
|
||||
{this.props.disabled ?
|
||||
<i className="material-icons disabled">send</i> :
|
||||
<a href="#" onClick={this.handleSend}><i className="material-icons">send</i></a>}
|
||||
<a href="#" onClick={this.handleSend} title="Send"><i className="material-icons">send</i></a>}
|
||||
<input type="file" ref={function(ref) {instance.attachFile = ref;}}
|
||||
onChange={this.handleAttachFile} style={{display: 'none'}} />
|
||||
<input type="file" ref={function(ref) {instance.attachImage = ref;}} accept="image/*"
|
||||
@@ -3088,7 +3157,7 @@
|
||||
var topic = Tinode.getTopic(this.state.topicSelected);
|
||||
var instance = this;
|
||||
// Check if the message is formatted.
|
||||
var dft = Drafty.parse(msg);
|
||||
var dft = Drafty.parse(msg) || msg;
|
||||
if (Drafty.isPlainText(dft)) {
|
||||
// The message is simple text. Sned if as text/plain.
|
||||
topic.publish(msg).catch(function(err) {
|
||||
|
||||
Reference in New Issue
Block a user