Преглед изворни кода

Show unread messages counter next to roster contacts

JC Brand пре 8 година
родитељ
комит
f3d29e016e

+ 2 - 7
css/converse.css

@@ -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;

+ 6 - 0
docs/CHANGES.md

@@ -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*:

+ 13 - 0
docs/source/events.rst

@@ -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
 ~~~~~~~~~~~
 

+ 2 - 9
sass/_roster.scss

@@ -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;

+ 4 - 0
src/converse-chatview.js

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

+ 12 - 10
src/converse-core.js

@@ -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;
-                }
-                if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
-                    return true; // We already have this message stored.
+                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);
                 }
-                chatbox.createMessage(message, delay, original_stanza);
+                _converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});
                 return true;
             },
 

+ 0 - 3
src/converse-mam.js

@@ -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: {

+ 4 - 1
src/converse-muc.js

@@ -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;
                 }

+ 2 - 1
src/converse-notification.js

@@ -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;
                 }

+ 54 - 12
src/converse-rosterview.js

@@ -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);
         }
     });
 }));

+ 2 - 0
src/templates/roster_filter.html

@@ -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" {[ } ]}