Show unread messages counter next to roster contacts

This commit is contained in:
JC Brand 2017-04-20 21:25:58 +02:00
parent ce16e1bcef
commit f3d29e016e
11 changed files with 101 additions and 43 deletions

View File

@ -2227,7 +2227,7 @@
z-index: 1;
opacity: 0; }
#conversejs #converse-roster .roster-contacts dd .open-chat.unread-msgs .avatar.avatar-online .pulse {
border: 0.7em solid #1A9707; }
border: 0.7em solid #2A9D8F; }
@-webkit-keyframes pulse {
0% {
-webkit-transform: scale(0);
@ -2304,7 +2304,7 @@
font-size: 11px;
margin-left: -3em;
font-weight: normal;
padding: 2px 4px;
padding: 0 4px;
text-shadow: none; }
#conversejs #converse-roster .roster-contacts dd .open-chat .contact-name {
padding: 0;
@ -2312,16 +2312,12 @@
max-width: 80%;
float: none;
height: 60px; }
#conversejs #converse-roster .roster-contacts dd .open-chat .contact-name.unread-msgs {
max-width: 70%; }
#conversejs #converse-roster .roster-contacts dd .open-chat .avatar {
float: left;
display: inline-block;
height: 60px; }
#conversejs #converse-roster .roster-contacts dd .open-chat .avatar .status-icon {
color: #2A9D8F; }
#conversejs #converse-roster .roster-contacts dd .open-chat .avatar .status-icon.icon-online {
color: #1A9707; }
#conversejs #converse-roster .roster-contacts dd:hover {
background-color: #DCF9F6; }
#conversejs #converse-roster .roster-contacts dd:hover .remove-xmpp-contact {
@ -2349,7 +2345,6 @@
background-color: #DCEAC5;
/* Make this difference */ }
#conversejs #converse-roster .roster-contacts dd a, #conversejs #converse-roster .roster-contacts dd span {
text-shadow: 0 1px 0 #FAFAFA;
display: inline-block;
overflow: hidden;
white-space: nowrap;

View File

@ -1,5 +1,11 @@
# Changelog
## 3.1.0 (Unreleased)
- Show unread messages next to roster contacts. [jcbrand]
- API change: the `message` event now returns a data object with `stanza` and
`chatbox` attributes, instead of just the stanza. [jcbrand]
## 3.0.2 (2017-04-23)
*Dependency updates*:

View File

@ -144,6 +144,19 @@ The user has logged out.
``_converse.on('logout', function () { ... });``
messageAdded
~~~~~~~~~~~~
Once a message has been added to a chat box. The passed in data object contains
a `chatbox` attribute, referring to the chat box receiving the message, as well
as a `message` attribute which refers to the Message model.
.. code-block:: javascript
_converse.on('messageAdded', function (data) {
// The message is at `data.message`
// The original chat box is at `data.chatbox`.
});
messageSend
~~~~~~~~~~~

View File

@ -140,7 +140,7 @@
}
&.avatar-online {
.pulse {
border: 0.7em solid $online-color;
border: 0.7em solid $link-color;
}
}
@include keyframes(pulse) {
@ -176,7 +176,7 @@
font-size: 11px;
margin-left: -3em;
font-weight: normal;
padding: 2px 4px;
padding: 0 4px;
text-shadow: none;
}
@ -186,9 +186,6 @@
max-width: 80%;
float: none;
height: $roster-item-height;
&.unread-msgs {
max-width: 70%;
}
}
.avatar {
@ -198,9 +195,6 @@
.status-icon {
color: $link-color;
&.icon-online {
color: $online-color;
}
}
}
}
@ -242,7 +236,6 @@
/* Make this difference */
}
a, span {
text-shadow: 0 1px 0 $link-shadow-color;
display: inline-block;
overflow: hidden;
white-space: nowrap;

View File

@ -471,6 +471,10 @@
} else {
this.handleTextMessage(message);
}
_converse.emit('messageAdded', {
'message': message,
'chatbox': this.model
});
},
createMessageStanza: function (message) {

View File

@ -183,9 +183,11 @@
Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
Strophe.addNamespace('DELAY', 'urn:xmpp:delay');
Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
Strophe.addNamespace('MAM', 'urn:xmpp:mam:0');
Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
Strophe.addNamespace('XFORM', 'jabber:x:data');
// Instance level constants
@ -786,7 +788,8 @@
'groups': [],
'image_type': DEFAULT_IMAGE_TYPE,
'image': DEFAULT_IMAGE,
'status': ''
'status': '',
'num_unread': 0
}, attributes));
this.on('destroy', function () { this.removeFromRoster(); }.bind(this));
@ -1476,7 +1479,7 @@
*/
var original_stanza = message,
contact_jid, forwarded, delay, from_bare_jid,
from_resource, is_me, msgid,
from_resource, is_me, msgid, messages,
chatbox, resource,
from_jid = message.getAttribute('from'),
to_jid = message.getAttribute('to'),
@ -1517,7 +1520,6 @@
from_bare_jid = Strophe.getBareJidFromJid(from_jid);
from_resource = Strophe.getResourceFromJid(from_jid);
is_me = from_bare_jid === _converse.bare_jid;
msgid = message.getAttribute('id');
if (is_me) {
// I am the sender, so this must be a forwarded message...
contact_jid = Strophe.getBareJidFromJid(to_jid);
@ -1526,16 +1528,16 @@
contact_jid = from_bare_jid;
resource = from_resource;
}
_converse.emit('message', original_stanza);
// Get chat box, but only create a new one when the message has a body.
chatbox = this.getChatBox(contact_jid, !_.isNull(message.querySelector('body')));
if (!chatbox) {
return true;
msgid = message.getAttribute('id');
messages = msgid && chatbox.messages.findWhere({msgid: msgid}) || [];
if (chatbox && _.isEmpty(messages)) {
// Only create the message when we're sure it's not a
// duplicate
chatbox.createMessage(message, delay, original_stanza);
}
if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
return true; // We already have this message stored.
}
chatbox.createMessage(message, delay, original_stanza);
_converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});
return true;
},

View File

@ -27,9 +27,6 @@
// XEP-0313 Message Archive Management
var MAM_ATTRIBUTES = ['with', 'start', 'end'];
Strophe.addNamespace('MAM', 'urn:xmpp:mam:0');
Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
converse.plugins.add('converse-mam', {
overrides: {

View File

@ -1952,7 +1952,10 @@
this.model.createMessage(message, delay, original_stanza);
if (sender !== this.model.get('nick')) {
// We only emit an event if it's not our own message
_converse.emit('message', original_stanza);
_converse.emit(
'message',
{'stanza': original_stanza, 'chatbox': this.model}
);
}
return true;
}

View File

@ -231,10 +231,11 @@
}
};
_converse.handleMessageNotification = function (message) {
_converse.handleMessageNotification = function (data) {
/* Event handler for the on('message') event. Will call methods
* to play sounds and show HTML5 notifications.
*/
var message = data.stanza;
if (!_converse.shouldNotifyOfMessage(message)) {
return false;
}

View File

@ -30,6 +30,7 @@
Strophe = converse.env.Strophe,
$iq = converse.env.$iq,
b64_sha1 = converse.env.b64_sha1,
sizzle = converse.env.sizzle,
_ = converse.env._;
@ -156,6 +157,7 @@
label_groups: LABEL_GROUPS,
label_state: __('State'),
label_any: __('Any'),
label_unread_messages: __('Unread'),
label_online: __('Online'),
label_chatty: __('Chatty'),
label_busy: __('Busy'),
@ -279,6 +281,8 @@
_converse.on('rosterGroupsFetched', this.positionFetchedGroups, this);
_converse.on('rosterContactsFetched', this.update, this);
this.createRosterFilter();
},
render: function () {
@ -622,20 +626,28 @@
));
} else if (subscription === 'both' || subscription === 'to') {
this.el.classList.add('current-xmpp-contact');
this.$el.removeClass(_.without(['both', 'to'], subscription)[0]).addClass(subscription);
this.$el.html(tpl_roster_item(
_.extend(item.toJSON(), {
'desc_status': STATUSES[chat_status||'offline'],
'desc_chat': __('Click to chat with this contact'),
'desc_remove': __('Click to remove this contact'),
'title_fullname': __('Name'),
'allow_contact_removal': _converse.allow_contact_removal
})
));
this.el.classList.remove(_.without(['both', 'to'], subscription)[0])
this.el.classList.add(subscription);
this.renderRosterItem(item);
}
return this;
},
renderRosterItem: function (item) {
var chat_status = item.get('chat_status');
this.$el.html(tpl_roster_item(
_.extend(item.toJSON(), {
'desc_status': STATUSES[chat_status||'offline'],
'desc_chat': __('Click to chat with this contact'),
'desc_remove': __('Click to remove this contact'),
'title_fullname': __('Name'),
'allow_contact_removal': _converse.allow_contact_removal,
'num_unread': item.get('num_unread') || 0
})
));
return this;
},
isGroupCollapsed: function () {
/* Check whether the group in which this contact appears is
* collapsed.
@ -677,6 +689,7 @@
openChat: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
this.model.save({'num_unread': 0});
return _converse.chatboxviews.showChat(this.model.attributes);
},
@ -829,6 +842,8 @@
return utils.contains.not('chat_status', q)(contact) && !contact.get('requesting');
}
);
} else if (q === 'unread_messages') {
matches = this.model.contacts.filter({'num_unread': 0});
} else {
matches = this.model.contacts.filter(
utils.contains.not('chat_status', q)
@ -918,6 +933,32 @@
/* -------- Event Handlers ----------- */
var onMessageReceived = function (data) {
/* Given a newly received message, update the unread counter on
* the relevant roster contact (TODO: or chat room).
*/
var chatbox = data.chatbox;
if (_.isUndefined(chatbox)) {
return;
}
if (_.isNull(data.stanza.querySelector('body'))) {
return; // The message has no text
}
var new_message = !(sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', data.stanza).length);
var hidden_or_minimized_chatbox = chatbox.get('hidden') || chatbox.get('minimized');
if (hidden_or_minimized_chatbox && new_message) {
if (chatbox.get('type') === 'chatroom') {
// TODO
} else {
var contact = _.head(_converse.roster.where({'jid': chatbox.get('jid')}));
if (!_.isUndefined(contact)) {
contact.save({'num_unread': contact.get('num_unread') + 1});
}
}
}
};
var initRoster = function () {
/* Create an instance of RosterView once the RosterGroups
* collection has been created (in converse-core.js)
@ -927,8 +968,9 @@
});
_converse.rosterview.render();
};
_converse.on('rosterInitialized', initRoster);
_converse.on('rosterReadyAfterReconnection', initRoster);
_converse.api.listen.on('rosterInitialized', initRoster);
_converse.api.listen.on('rosterReadyAfterReconnection', initRoster);
_converse.api.listen.on('message', onMessageReceived);
}
});
}));

View File

@ -4,6 +4,8 @@
{[ if (filter_type === 'state') { ]} style="display: none" {[ } ]} >
<select class="state-type" {[ if (filter_type !== 'state') { ]} style="display: none" {[ } ]} >
<option value="">{{label_any}}</option>
<option {[ if (chat_state === 'unread_messages') { ]} selected="selected" {[ } ]}
value="unread_messages">{{label_unread_messages}}</option>
<option {[ if (chat_state === 'online') { ]} selected="selected" {[ } ]}
value="online">{{label_online}}</option>
<option {[ if (chat_state === 'chat') { ]} selected="selected" {[ } ]}