Some UI improvements

- Render images as thumbnails
- Use the image.html template when rendering images from pasted URLs
- Update message and spoiler markup to render avatars
- Use the default avatar as fallback when user doesn't have one
- Instead of 'me' render own name or JID
This commit is contained in:
JC Brand 2018-04-27 12:06:56 +02:00
parent f913ee86f0
commit 3d42425083
22 changed files with 104 additions and 73 deletions

View File

@ -101,7 +101,6 @@ Brief description of converse.js's dependencies
Converse.js relies on the following dependencies:
* `JQuery <http://jquery.com/>`_ for DOM manipulation and `promises <http://api.jquery.com/promise/>`_.
* `moment.js <http://momentjs.com/>`_ provides a better API for handling dates and times.
* `Strophe.js <http://strophe.im/>`_ maintains the XMPP session, is used to
build XMPP stanzas, to send them, and to register handlers for received stanzas.

View File

@ -25,12 +25,6 @@
<div class="chat-head chat-head-chatbox row no-gutters">
<div class="col">
<div class="row no-gutters">
<div class="col-auto">
<img alt="User Avatar"
class="avatar"
height="50px" width="50px"
src="data:image/png;base64,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"/>
</div>
<div class="col chat-title" title="j@chat.example.org">Juliet Capulet
<p class="user-custom-message" title="On the balcony">On the balcony</p>
</div>

View File

@ -97,7 +97,6 @@
</div>
</div>
<div class="message chat-msg">
<canvas height="36" width="36" class="avatar"></canvas>
<div class="chat-msg-content">
@ -111,6 +110,24 @@
Or, if thou wilt not, be but sworn my love,
And I'll no longer be a Capulet.
</p>
<div class="chat-msg-media"></div>
</div>
</div>
<div class="message chat-msg">
<canvas height="36" width="36" class="avatar"></canvas>
<div class="chat-msg-content">
<span class="chat-msg-heading">
<span class="chat-msg-author">Juliet Capulet</span>
<span class="chat-msg-time text-muted">19:45</span>
</span>
<p class="chat-msg-text"></p>
<div class="chat-msg-media">
<a href="https://images.unsplash.com/photo-1496660067708-010ebdd7ce72?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=ea3514e6e00d8ce25c24d992b97929d9&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb"
target="_blank" rel="noopener">
<img class="chat-image img-thumbnail" src="https://images.unsplash.com/photo-1496660067708-010ebdd7ce72?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=ea3514e6e00d8ce25c24d992b97929d9&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb">
</a>
</div>
</div>
</div>
@ -121,13 +138,15 @@
<span class="chat-msg-author">Romeo Montague</span>
<span class="chat-msg-time text-muted">19:36</span>
</span>
<div class="spoiler-hint">By a name
<a class="badge badge-info toggle-spoiler" data-toggle-state="closed" href="#"><i class="fa fa-eye"></i>Show more</a>
<div>
<span class="spoiler-hint">By a name</span>
<a class="badge badge-info spoiler-toggle" data-toggle-state="closed" href="#"><i class="fa fa-eye"></i>Show more</a>
</div>
<div class="chat-msg-text spoiler collapsed">
I know not how to tell thee who I am: My name, dear saint, is hateful to
myself, Because it is an enemy to thee. Had I it written, I would tear the word.
</div>
<div class="chat-msg-media"></div>
</div>
</div>

View File

@ -52,7 +52,7 @@ function toggleSpoilerMessage (ev) {
}
window.initSpoilers = function () {
const spoilers = document.querySelectorAll('.toggle-spoiler');
const spoilers = document.querySelectorAll('.spoiler-toggle');
_.each(spoilers, (spoiler_el) => {
spoiler_el.addEventListener('click', toggleSpoilerMessage);
});

View File

@ -44,9 +44,9 @@
_converse.chatboxes.onMessage(msg);
var view = _converse.chatboxviews.get(sender_jid);
expect(_.includes(view.el.querySelector('.chat-msg-author').textContent, 'Max Frankfurter')).toBeTruthy();
expect(view.el.querySelector('.chat-msg-author').textContent).toBe('Max Frankfurter');
var message_content = view.el.querySelector('.chat-msg-content');
var message_content = view.el.querySelector('.chat-msg-text');
expect(message_content.textContent).toBe(spoiler);
var spoiler_hint_el = view.el.querySelector('.spoiler-hint');
@ -82,7 +82,7 @@
var view = _converse.chatboxviews.get(sender_jid);
expect(_.includes(view.el.querySelector('.chat-msg-author').textContent, 'Max Frankfurter')).toBeTruthy();
var message_content = view.el.querySelector('.chat-msg-content');
var message_content = view.el.querySelector('.chat-msg-text');
expect(message_content.textContent).toBe(spoiler);
var spoiler_hint_el = view.el.querySelector('.spoiler-hint');
@ -148,17 +148,17 @@
expect(body_el.textContent).toBe('This is the spoiler');
/* Test the HTML spoiler message */
expect(view.el.querySelector('.chat-msg-author').textContent.split(':')[1].trim().split(' ')[1]).toBe('me');
expect(view.el.querySelector('.chat-msg-author').textContent).toBe('dummy@localhost');
var spoiler_msg_el = view.el.querySelector('.chat-msg-content.spoiler');
var spoiler_msg_el = view.el.querySelector('.chat-msg-text.spoiler');
expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
spoiler_toggle = view.el.querySelector('.toggle-spoiler');
expect(spoiler_toggle.textContent).toBe('Show hidden message');
spoiler_toggle = view.el.querySelector('.spoiler-toggle');
expect(spoiler_toggle.textContent).toBe('Show more');
spoiler_toggle.click();
expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
expect(spoiler_toggle.textContent).toBe('Hide hidden message');
expect(spoiler_toggle.textContent).toBe('Show less');
spoiler_toggle.click();
expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
done();
@ -227,17 +227,17 @@
expect(body_el.textContent).toBe('This is the spoiler');
/* Test the HTML spoiler message */
expect(view.el.querySelector('.chat-msg-author').textContent.split(':')[1].trim().split(' ')[1]).toBe('me');
expect(view.el.querySelector('.chat-msg-author').textContent).toBe('dummy@localhost');
var spoiler_msg_el = view.el.querySelector('.chat-msg-content.spoiler');
var spoiler_msg_el = view.el.querySelector('.chat-msg-text.spoiler');
expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
spoiler_toggle = view.el.querySelector('.toggle-spoiler');
expect(spoiler_toggle.textContent).toBe('Show hidden message');
spoiler_toggle = view.el.querySelector('.spoiler-toggle');
expect(spoiler_toggle.textContent).toBe('Show more');
spoiler_toggle.click();
expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
expect(spoiler_toggle.textContent).toBe('Hide hidden message');
expect(spoiler_toggle.textContent).toBe('Show less');
spoiler_toggle.click();
expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
done();

View File

@ -193,7 +193,6 @@
'image': _converse.DEFAULT_IMAGE,
'image_type': _converse.DEFAULT_IMAGE_TYPE,
'num_unread': 0,
'show_avatar': true,
'type': 'chatbox',
'message_type': 'chat',
'url': ''

View File

@ -249,7 +249,7 @@
'click .toggle-compose-spoiler': 'toggleComposeSpoilerMessage',
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
'click .toggle-smiley': 'toggleEmojiMenu',
'click .toggle-spoiler': 'toggleSpoilerMessage',
'click .spoiler-toggle': 'toggleSpoilerMessage',
'click .upload-file': 'toggleFileUpload',
'keypress .chat-textarea': 'keyPressed',
'input .chat-textarea': 'inputChanged'
@ -373,7 +373,7 @@
this.el.querySelector('.chat-toolbar').insertAdjacentHTML('afterBegin', html);
}
}
});
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
},
insertHeading () {
@ -907,14 +907,16 @@
toggle_el.parentElement.parentElement.querySelector('.spoiler')
);
if (toggle_el.getAttribute("data-toggle-state") == "closed") {
toggle_el.textContent = __('Show less');
toggle_el.textContent = 'Show less';
icon_el.classList.remove("fa-eye");
icon_el.classList.add("fa-eye-slash");
toggle_el.insertAdjacentElement('afterBegin', icon_el);
toggle_el.setAttribute("data-toggle-state", "open");
} else {
toggle_el.textContent = __('Show more');
toggle_el.textContent = 'Show more';
icon_el.classList.remove("fa-eye-slash");
icon_el.classList.add("fa-eye");
toggle_el.insertAdjacentElement('afterBegin', icon_el);
toggle_el.setAttribute("data-toggle-state", "closed");
}
},

View File

@ -1509,10 +1509,12 @@
defaults () {
return {
"status": _converse.default_state,
"jid": _converse.bare_jid,
"nickname": _converse.nickname,
"vcard_updated": null
"status": _converse.default_state,
"vcard_updated": null,
'image': _converse.DEFAULT_IMAGE,
'image_type': _converse.DEFAULT_IMAGE_TYPE
}
},

View File

@ -74,7 +74,6 @@
_converse.HeadlinesBox = _converse.ChatBox.extend({
defaults: {
'type': 'headline',
'show_avatar': false,
'bookmarked': false,
'chat_state': undefined,
'num_unread': 0,

View File

@ -44,8 +44,8 @@
_converse.MessageView = Backbone.NativeView.extend({
initialize () {
const chatbox = this.model.collection.chatbox;
chatbox.on('change:fullname', (chatbox) => this.model.save('fullname', chatbox.get('fullname')));
this.chatbox = this.model.collection.chatbox;
this.chatbox.on('change:fullname', (chatbox) => this.model.save('fullname', chatbox.get('fullname')));
this.model.on('change:fullname', this.render, this);
this.model.on('change:progress', this.renderFileUploadProgresBar, this);
@ -61,7 +61,7 @@
return this.renderErrorMessage();
}
let template, username,
let template, username, image, image_type,
text = this.model.get('message');
// TODO: store proper username on the message itself
@ -71,9 +71,16 @@
username = arr[1];
text = arr[2];
} else {
const fullname = _converse.xmppstatus.get('fullname') || this.model.get('fullname');
username = this.model.get('sender') === 'me' && __('me') || fullname || this.model.get('from');
username = this.model.get('fullname') || this.model.get('from');
template = this.model.get('is_spoiler') ? tpl_spoiler_message : tpl_message;
if (this.model.get('sender') === 'me') {
image_type = _converse.xmppstatus.get('image_type');
image = _converse.xmppstatus.get('image');
} else {
image_type = this.chatbox.get('image_type');
image = this.chatbox.get('image');
}
}
const moment_time = moment(this.model.get('time'));
const msg = u.stringToElement(template(
@ -82,7 +89,9 @@
'time': moment_time.format(),
'username': username,
'extra_classes': this.getExtraMessageClasses(),
'label_show': __('Show more')
'label_show': __('Show more'),
'image_type': image_type,
'image': image
})
));
@ -97,7 +106,7 @@
)(url);
}
const msg_content = msg.querySelector('.chat-msg-content');
const msg_content = msg.querySelector('.chat-msg-text');
if (text !== url) {
text = xss.filterXSS(text, {'whiteList': {}});
msg_content.innerHTML = _.flow(
@ -106,7 +115,7 @@
_.partial(u.addEmoji, _converse, emojione, _)
)(text);
}
u.renderImageURLs(msg_content).then(() => {
u.renderImageURLs(_converse, msg_content).then(() => {
this.model.collection.trigger('rendered');
});
return this.replaceElement(msg);

View File

@ -1485,7 +1485,7 @@
* Chat rooms can be listed, joined and new rooms can be created.
*/
tagName: 'div',
className: 'controlbox-pane',
className: 'controlbox-section',
id: 'chatrooms',
events: {
'click a.chatbox-btn.fa-users': 'showAddRoomModal',

View File

@ -304,15 +304,11 @@
getOutgoingMessageAttributes (text, spoiler_hint) {
const is_spoiler = this.get('composing_spoiler');
return {
'from': _converse.connection.jid,
'fullname': this.get('nick'),
'is_spoiler': is_spoiler,
'message': text ? u.httpToGeoUri(emojione.shortnameToUnicode(text), _converse) : undefined,
'msgid': _converse.connection.getUniqueId(),
'sender': 'me',
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
'time': moment().format(),
'to': this.get('jid'),
'type': 'groupchat',
};
},

View File

@ -719,6 +719,7 @@
_converse.RosterView = Backbone.OrderedListView.extend({
tagName: 'div',
id: 'converse-roster',
className: 'controlbox-section',
ItemView: _converse.RosterGroupView,
listItems: 'model',

View File

@ -1,4 +1,4 @@
<div class="room-item">
<div class="list-item room-item">
<div class="available-chatroom d-flex flex-row {[ if (o.hidden) { ]} hidden {[ } ]}" data-room-jid="{{{o.jid}}}">
<a class="open-room w-100" data-room-jid="{{{o.jid}}}" title="{{{o.open_title}}}" href="#">{{{o.name}}}</a>
<a class="remove-bookmark fa fa-bookmark align-self-center {[ if (o.bookmarked) { ]} button-on {[ } ]}"

View File

@ -1,14 +1,6 @@
<div class="chat-head chat-head-chatbox row no-gutters">
<div class="col">
<div class="row no-gutters">
{[ if (o.show_avatar) { ]}
<div class="col-auto">
<img alt="User Avatar"
class="avatar"
height="{{{o.avatar_height}}}px" width="{{{o.avatar_width}}}px"
src="data:{{{o.image_type || o._converse.DEFAULT_IMAGE_TYPE}}};base64,{{{o.image || o._converse.DEFAULT_IMAGE}}}"/>
</div>
{[ } ]}
<div class="col chat-title" title="{{{o.jid}}}">
{[ if (o.url) { ]}
<a href="{{{o.url}}}" target="_blank" rel="noopener" class="user">

View File

@ -1 +1,4 @@
<img class="chat-image" src="{{{o.url}}}"/>
<a href="{{{o.url}}}"
target="_blank" rel="noopener">
<img class="chat-image img-thumbnail" src="{{{o.url}}}">
</a>

View File

@ -1,5 +1,11 @@
<div class="message chat-msg {{{o.extra_classes}}}" data-isodate="{{{o.time}}}" data-msgid="{{{o.msgid}}}">
<span class="chat-msg-author chat-msg-{{{o.sender}}}">{{{o.pretty_time}}} {{{o.username}}}:&nbsp;</span>
<span class="chat-msg-content"></span>
<div class="chat-msg-media"></div>
<img alt="User Avatar" class="avatar" height="36px" width="36px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
<div class="chat-msg-content">
<span class="chat-msg-heading">
<span class="chat-msg-author">{{{o.username}}}</span>
<span class="chat-msg-time tex{{{o.username}}}t-muted">{{{o.pretty_time}}}</span>
</span>
<p class="chat-msg-text">
<div class="chat-msg-media"></div>
</div>
</div>

View File

@ -1,6 +1,6 @@
<!-- <div id="chatrooms"> -->
<div class="d-flex">
<span class="w-100">{{{o.heading_chatrooms}}}</span>
<span class="w-100 controlbox-heading">{{{o.heading_chatrooms}}}</span>
<a class="chatbox-btn trigger-list-chatrooms-modal fa fa-list-ul" title="{{{o.title_list_rooms}}}" data-toggle="modal" data-target="#list-chatrooms-modal"></a>
<a class="chatbox-btn trigger-add-chatrooms-modal fa fa-users" title="{{{o.title_new_room}}}" data-toggle="modal" data-target="#add-chatrooms-modal"></a>
</div>

View File

@ -1,4 +1,4 @@
<div class="room-item">
<div class="list-item room-item">
<div class="available-chatroom d-flex flex-row {[ if (o.num_unread_general) { ]} unread-msgs {[ } ]}" data-room-jid="{{{o.jid}}}">
{[ if (o.num_unread) { ]}
<span class="msgs-indicator badge badge-info">{{{ o.num_unread }}}</span>

View File

@ -1,5 +1,5 @@
<div class="d-flex">
<span class="w-100">{{{o.heading_contacts}}}</span>
<span class="w-100 controlbox-heading">{{{o.heading_contacts}}}</span>
<a class="chatbox-btn add-contact fa fa-user-plus" title="{{{o.title_add_contact}}}"
data-toggle="modal" data-target="#add-contact-modal"></a>
</div>

View File

@ -1,6 +1,14 @@
<div class="message chat-msg {{{o.extra_classes}}}" data-isodate="{{{o.time}}}" data-msgid="{{{o.msgid}}}">
<span class="chat-msg-author chat-msg-{{{o.sender}}}">{{{o.pretty_time}}} {{{o.username}}}:&nbsp;</span>
<div class="spoiler-hint">{{{o.spoiler_hint}}}</div>
<a class="toggle-spoiler" data-toggle-state="closed" href="#"><i class="fa fa-eye"></li>{{{o.label_show}}}</a>
<div class="chat-msg-content spoiler collapsed"><!-- message gets added here via renderMessage --></div>
<img alt="User Avatar" class="avatar" height="36px" width="36px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
<div class="chat-msg-content">
<span class="chat-msg-heading">
<span class="chat-msg-author">{{{o.username}}}</span>
<span class="chat-msg-time text-muted">{{{o.pretty_time}}}</span>
</span>
<div>
<span class="spoiler-hint">{{{o.spoiler_hint}}}</span>
<a class="badge badge-info spoiler-toggle" data-toggle-state="closed" href="#"><i class="fa fa-eye"></i>{{{o.label_show}}}</a>
</div>
<div class="chat-msg-text spoiler collapsed"><!-- message gets added here via renderMessage --></div>
</div>
</div>

View File

@ -218,30 +218,32 @@
return text;
};
u.renderImageURLs = function (obj) {
u.renderImageURLs = function (_converse, obj) {
/* Returns a Promise which resolves once all images have been loaded.
*/
const { __ } = _converse;
const list = obj.textContent.match(URL_REGEX) || [];
return Promise.all(
_.map(list, (url) =>
new Promise((resolve, reject) =>
isImage(url).then(function (img) {
// XXX: need to create a new image, otherwise the event
// listener doesn't fire
const i = new Image();
i.className = 'chat-image';
i.src = img.src;
i.addEventListener('load', resolve);
// We also resolve for non-images, otherwise the
// Promise.all resolves prematurely.
i.addEventListener('error', resolve);
var anchors = sizzle(`a[href="${url}"]`, obj);
_.each(anchors, (a) => {
a.replaceChild(i, a.firstChild);
_.each(sizzle(`a[href="${url}"]`, obj), (a) => {
a.innerHTML = tpl_image({
'url': url,
'label_download': __('Download image file')
})
});
}).catch(resolve)
)
))
)
)
};
u.renderFileURL = function (_converse, url) {