ソースを参照

Show join/leave messages in chat rooms. Updates #365

JC Brand 8 年 前
コミット
0d48929bb3
5 ファイル変更173 行追加67 行削除
  1. 3 0
      docs/CHANGES.md
  2. 8 0
      docs/source/configuration.rst
  3. 80 10
      spec/chatroom.js
  4. 3 3
      spec/transcripts.js
  5. 79 54
      src/converse-muc.js

+ 3 - 0
docs/CHANGES.md

@@ -50,6 +50,9 @@
 - Bugfix. `TypeError: this.sendConfiguration(...).then is not a function` when
   an instant room is created. [jcbrand]
 - Ensure consistent behavior from `show_controlbox_by_default` [jcbrand]
+- #365 Show join/leave messages for chat rooms.
+  New configuration setting:
+  [muc_show_join_leave](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave)
 - #366 Show the chat room occupant's JID in the tooltip (if you're allowed to see it). [jcbrand]
 - #610, #785 Add presence priority handling [w3host, jcbrand]
 - #694 The `notification_option` wasn't being used consistently. [jcbrand]

+ 8 - 0
docs/source/configuration.rst

@@ -791,6 +791,14 @@ automatically be "john". If now john@differentdomain.com tries to join the
 room, his nickname will be "john-2", and if john@somethingelse.com joins, then
 his nickname will be "john-3", and so forth.
 
+muc_show_join_leave
+-------------------
+
+* Default; ``true``
+
+Determines whether Converse.js will show info messages inside a chat room
+whenever a user joins or leaves it.
+
 notify_all_room_messages
 ------------------------
 

+ 80 - 10
spec/chatroom.js

@@ -345,6 +345,76 @@
 
         describe("A Chat Room", function () {
 
+            it("shows join/leave messages when users enter or exit a room", mock.initConverse(function (_converse) {
+                test_utils.openChatRoom(_converse, "coven", 'chat.shakespeare.lit', 'some1');
+                var view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
+                var $chat_content = view.$el.find('.chat-content');
+
+                /* We don't show join/leave messages for existing occupants. We
+                 * know about them because we receive their presences before we
+                 * receive our own.
+                 */
+                presence = $pres({
+                        to: 'dummy@localhost/_converse.js-29092160',
+                        from: 'coven@chat.shakespeare.lit/oldguy'
+                    }).c('x', {xmlns: Strophe.NS.MUC_USER})
+                    .c('item', {
+                        'affiliation': 'none',
+                        'jid': 'oldguy@localhost/_converse.js-290929789',
+                        'role': 'participant'
+                    });
+                _converse.connection._dataRecv(test_utils.createRequest(presence));
+                expect($chat_content.find('div.chat-info').length).toBe(0);
+
+                /* <presence to="dummy@localhost/_converse.js-29092160"
+                 *           from="coven@chat.shakespeare.lit/some1">
+                 *      <x xmlns="http://jabber.org/protocol/muc#user">
+                 *          <item affiliation="owner" jid="dummy@localhost/_converse.js-29092160" role="moderator"/>
+                 *          <status code="110"/>
+                 *      </x>
+                 *  </presence></body>
+                 */
+                var presence = $pres({
+                        to: 'dummy@localhost/_converse.js-29092160',
+                        from: 'coven@chat.shakespeare.lit/some1'
+                    }).c('x', {xmlns: Strophe.NS.MUC_USER})
+                    .c('item', {
+                        'affiliation': 'owner',
+                        'jid': 'dummy@localhost/_converse.js-29092160',
+                        'role': 'moderator'
+                    }).up()
+                    .c('status', {code: '110'});
+                _converse.connection._dataRecv(test_utils.createRequest(presence));
+                expect($chat_content.find('div.chat-info:first').html()).toBe("some1 has joined the room");
+
+                presence = $pres({
+                        to: 'dummy@localhost/_converse.js-29092160',
+                        from: 'coven@chat.shakespeare.lit/newguy'
+                    }).c('x', {xmlns: Strophe.NS.MUC_USER})
+                    .c('item', {
+                        'affiliation': 'none',
+                        'jid': 'newguy@localhost/_converse.js-290929789',
+                        'role': 'participant'
+                    });
+                _converse.connection._dataRecv(test_utils.createRequest(presence));
+                expect($chat_content.find('div.chat-info').length).toBe(2);
+                expect($chat_content.find('div.chat-info:last').html()).toBe("newguy has joined the room");
+
+                presence = $pres({
+                        to: 'dummy@localhost/_converse.js-29092160',
+                        type: 'unavailable',
+                        from: 'coven@chat.shakespeare.lit/newguy'
+                    }).c('x', {xmlns: Strophe.NS.MUC_USER})
+                    .c('item', {
+                        'affiliation': 'none',
+                        'jid': 'newguy@localhost/_converse.js-290929789',
+                        'role': 'participant'
+                    });
+                _converse.connection._dataRecv(test_utils.createRequest(presence));
+                expect($chat_content.find('div.chat-info').length).toBe(3);
+                expect($chat_content.find('div.chat-info:last').html()).toBe("newguy has left the room");
+            }));
+
             it("shows its description in the chat heading",  mock.initConverse(function (_converse) {
                 var sent_IQ, IQ_id;
                 var sendIQ = _converse.connection.sendIQ;
@@ -1036,8 +1106,7 @@
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 var view = _converse.chatboxviews.get('jdev@conference.jabber.org');
                 var $chat_content = view.$el.find('.chat-content');
-                expect($chat_content.find('.chat-info').length).toBe(1);
-                expect($chat_content.find('.chat-info').text()).toBe('Topic set by ralphm to: '+text);
+                expect($chat_content.find('.chat-info:last').text()).toBe('Topic set by ralphm to: '+text);
             }));
 
             it("escapes the subject before rendering it, to avoid JS-injection attacks", mock.initConverse(function (_converse) {
@@ -1047,8 +1116,7 @@
                 var view = _converse.chatboxviews.get('jdev@conference.jabber.org');
                 view.setChatRoomSubject('ralphm', subject);
                 var $chat_content = view.$el.find('.chat-content');
-                expect($chat_content.find('.chat-info').length).toBe(1);
-                expect($chat_content.find('.chat-info').text()).toBe('Topic set by ralphm to: '+subject);
+                expect($chat_content.find('.chat-info:last').text()).toBe('Topic set by ralphm to: '+subject);
             }));
 
             it("informs users if their nicknames has been changed.", mock.initConverse(function (_converse) {
@@ -1114,8 +1182,9 @@
                 expect($occupants.children().length).toBe(1);
                 expect($occupants.children().first(0).text()).toBe("oldnick");
 
-                expect($chat_content.find('div.chat-info').length).toBe(1);
-                expect($chat_content.find('div.chat-info').html()).toBe(__(_converse.muc.new_nickname_messages["210"], "oldnick"));
+                expect($chat_content.find('div.chat-info').length).toBe(2);
+                expect($chat_content.find('div.chat-info:first').html()).toBe("oldnick has joined the room");
+                expect($chat_content.find('div.chat-info:last').html()).toBe(__(_converse.muc.new_nickname_messages["210"], "oldnick"));
 
                 presence = $pres().attrs({
                         from:'lounge@localhost/oldnick',
@@ -1134,7 +1203,7 @@
                     .c('status').attrs({code:'110'}).nodeTree;
 
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect($chat_content.find('div.chat-info').length).toBe(2);
+                expect($chat_content.find('div.chat-info').length).toBe(3);
                 expect($chat_content.find('div.chat-info').last().html()).toBe(__(_converse.muc.new_nickname_messages["303"], "newnick"));
 
                 $occupants = view.$('.occupant-list');
@@ -1154,8 +1223,9 @@
                     .c('status').attrs({code:'110'}).nodeTree;
 
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect($chat_content.find('div.chat-info').length).toBe(2);
-                expect($chat_content.find('div.chat-info').last().html()).toBe(__(_converse.muc.new_nickname_messages["303"], "newnick"));
+                expect($chat_content.find('div.chat-info').length).toBe(4);
+                expect($chat_content.find('div.chat-info').get(2).textContent).toBe(__(_converse.muc.new_nickname_messages["303"], "newnick"));
+                expect($chat_content.find('div.chat-info').last().html()).toBe("newnick has joined the room");
                 $occupants = view.$('.occupant-list');
                 expect($occupants.children().length).toBe(1);
                 expect($occupants.children().first(0).text()).toBe("newnick");
@@ -1400,7 +1470,7 @@
 
         describe("Each chat room can take special commands", function () {
 
-            it("to set the room subject", mock.initConverse(function (_converse) {
+            it("to set the room topic", mock.initConverse(function (_converse) {
                 var sent_stanza;
                 test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy');
                 var view = _converse.chatboxviews.get('lounge@localhost');

+ 3 - 3
spec/transcripts.js

@@ -50,10 +50,10 @@
 
         it("can be used to replay conversations", mock.initConverse(function (_converse) {
             /*
-            test_utils.openChatRoom("discuss", 'conference.conversejs.org', 'jc');
-            test_utils.openChatRoom("dummy", 'rooms.localhost', 'jc');
-            test_utils.openChatRoom("prosody", 'conference.prosody.im', 'jc');
+            test_utils.openChatRoom(_converse, "dummy", 'rooms.localhost', 'jc');
+            test_utils.openChatRoom(_converse, "prosody", 'conference.prosody.im', 'jc');
             */
+            test_utils.openChatRoom(_converse, "discuss", 'conference.conversejs.org', 'ee');
             spyOn(_converse, 'areDesktopNotificationsEnabled').andReturn(true);
             _.each(transcripts, function (transcript) {
                 var text = transcript();

+ 79 - 54
src/converse-muc.js

@@ -73,6 +73,13 @@
     Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
     Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
 
+    var ROOMSTATUS = {
+        CONNECTED: 0,
+        CONNECTING: 1,
+        DISCONNECTED: 2,
+        ENTERED: 3
+    };
+
     converse.plugins.add('converse-muc', {
         /* Optional dependencies are other plugins which might be
          * overridden or relied upon, if they exist, otherwise they're ignored.
@@ -274,6 +281,7 @@
                 muc_history_max_stanzas: undefined,
                 muc_instant_rooms: true,
                 muc_nickname_from_jid: false,
+                muc_show_join_leave: true,
                 visible_toolbar_buttons: {
                     'toggle_occupants': true
                 },
@@ -287,7 +295,7 @@
                 return _converse.chatboxviews.showChat(
                     _.extend({
                         'affiliation': null,
-                        'connection_status': Strophe.Status.DISCONNECTED,
+                        'connection_status': ROOMSTATUS.DISCONNECTED,
                         'description': '',
                         'features_fetched': false,
                         'hidden': false,
@@ -349,9 +357,9 @@
                     // Which for some reason doesn't work.
                     // So working around that fact here:
                     this.$el.find('.chat-content').on('scroll', this.markScrolled.bind(this));
-                    
+
                     this.registerHandlers();
-                    if (this.model.get('connection_status') !==  Strophe.Status.CONNECTED) {
+                    if (this.model.get('connection_status') !==  ROOMSTATUS.ENTERED) {
                         this.getRoomFeatures().always(function () {
                             that.join();
                             that.fetchMessages();
@@ -443,7 +451,7 @@
                 },
 
                 afterConnected: function () {
-                    if (this.model.get('connection_status') ===  Strophe.Status.CONNECTED) {
+                    if (this.model.get('connection_status') ===  ROOMSTATUS.ENTERED) {
                         this.setChatState(_converse.ACTIVE);
                         this.scrollDown();
                         this.focus();
@@ -665,7 +673,7 @@
                         members,
                         _.partial(this.sendAffiliationIQ, this.model.get('jid'), affiliation)
                     );
-                    return $.when.apply($, promises); 
+                    return $.when.apply($, promises);
                 },
 
                 setAffiliations: function (members, onSuccess, onError) {
@@ -807,7 +815,7 @@
                      * as taken from the 'chat_state' attribute of the chat box.
                      * See XEP-0085 Chat State Notifications.
                      */
-                    if (this.model.get('connection_status') !==  Strophe.Status.CONNECTED) {
+                    if (this.model.get('connection_status') !==  ROOMSTATUS.ENTERED) {
                         return;
                     }
                     var chat_state = this.model.get('chat_state');
@@ -1102,7 +1110,7 @@
                     if (!nick) {
                         return this.checkForReservedNick();
                     }
-                    if (this.model.get('connection_status') ===  Strophe.Status.CONNECTED) {
+                    if (this.model.get('connection_status') === ROOMSTATUS.ENTERED) {
                         // We have restored a chat room from session storage,
                         // so we don't send out a presence stanza again.
                         return this;
@@ -1115,13 +1123,13 @@
                     if (password) {
                         stanza.cnode(Strophe.xmlElement("password", [], password));
                     }
-                    this.model.save('connection_status', Strophe.Status.CONNECTING);
+                    this.model.save('connection_status', ROOMSTATUS.CONNECTING);
                     _converse.connection.send(stanza);
                     return this;
                 },
 
                 cleanup: function () {
-                    this.model.save('connection_status', Strophe.Status.DISCONNECTED);
+                    this.model.save('connection_status', ROOMSTATUS.DISCONNECTED);
                     this.removeHandlers();
                     _converse.ChatBoxView.prototype.close.apply(this, arguments);
                 },
@@ -1137,7 +1145,7 @@
                     this.occupantsview.model.reset();
                     this.occupantsview.model.browserStorage._clear();
                     if (!_converse.connection.connected ||
-                            this.model.get('connection_status') === Strophe.Status.DISCONNECTED) {
+                            this.model.get('connection_status') === ROOMSTATUS.DISCONNECTED) {
                         // Don't send out a stanza if we're not connected.
                         this.cleanup();
                         return;
@@ -1568,26 +1576,23 @@
                      *                     current user.
                      *  (XMLElement) stanza: The original stanza received.
                      */
-                    var code = stat.getAttribute('code'),
-                        from_nick;
-                    if (is_self && code === "210") {
-                        from_nick = Strophe.unescapeNode(Strophe.getResourceFromJid(stanza.getAttribute('from')));
-                        return __(_converse.muc.new_nickname_messages[code], from_nick);
-                    } else if (is_self && code === "303") {
-                        return __(
-                            _converse.muc.new_nickname_messages[code],
-                            stanza.querySelector('x item').getAttribute('nick')
-                        );
-                    } else if (!is_self && (code in _converse.muc.action_info_messages)) {
-                        from_nick = Strophe.unescapeNode(Strophe.getResourceFromJid(stanza.getAttribute('from')));
-                        return __(_converse.muc.action_info_messages[code], from_nick);
-                    } else if (code in _converse.muc.info_messages) {
+                    var code = stat.getAttribute('code'), nick;
+                    if (code === '110') { return; }
+                    if (code in _converse.muc.info_messages) {
                         return _converse.muc.info_messages[code];
-                    } else if (code !== '110') {
-                        if (stat.textContent) {
-                            // Sometimes the status contains human readable text and not a code.
-                            return stat.textContent;
+                    }
+                    if (!is_self) {
+                        if (code in _converse.muc.action_info_messages) {
+                            nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
+                            return __(_converse.muc.action_info_messages[code], nick);
+                        }
+                    } else if (code in _converse.muc.new_nickname_messages) {
+                        if (is_self && code === "210") {
+                            nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
+                        } else if (is_self && code === "303") {
+                            nick = stanza.querySelector('x item').getAttribute('nick');
                         }
+                        return __(_converse.muc.new_nickname_messages[code], nick);
                     }
                     return;
                 },
@@ -1622,9 +1627,11 @@
                     // 1. Get notification messages based on the <status> elements.
                     var statuses = x.querySelectorAll('status');
                     var mapper = _.partial(this.getMessageFromStatus, _, stanza, is_self);
-                    var notification = {
-                        'messages': _.reject(_.map(statuses, mapper), _.isUndefined),
-                    };
+                    var notification = {};
+                    var messages = _.reject(_.map(statuses, mapper), _.isUndefined);
+                    if (messages.length) {
+                        notification.messages = messages;
+                    }
                     // 2. Get disconnection messages based on the <status> elements
                     var codes = _.invokeMap(statuses, Element.prototype.getAttribute, 'code');
                     var disconnection_codes = _.intersection(codes, _.keys(_converse.muc.disconnect_messages));
@@ -1666,7 +1673,7 @@
                         if (notification.reason) {
                             this.showDisconnectMessage(__(___('The reason given is: <em>"%1$s"</em>.'), notification.reason));
                         }
-                        this.model.save('connection_status', Strophe.Status.DISCONNECTED);
+                        this.model.save('connection_status', ROOMSTATUS.DISCONNECTED);
                         return;
                     }
                     _.each(notification.messages, function (message) {
@@ -1680,6 +1687,25 @@
                     }
                 },
 
+                getJoinLeaveMessages: function (stanza) {
+                    /* Parse the given stanza and return notification messages
+                     * for join/leave events.
+                     */
+                    // XXX: some mangling required to make the returned
+                    // result look like the structure returned by
+                    // parseXUserElement. Not nice...
+                    var nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
+                    if (stanza.getAttribute('type') === 'unavailable') {
+                        var stat = stanza.querySelector('status');
+                        if (!_.isNull(stat) && stat.textContent) {
+                            return [{'messages': [__(nick+' has left the room. "'+stat.textContent+'"')]}];
+                        } else {
+                            return [{'messages': [__(nick+' has left the room')]}];
+                        }
+                    }
+                    return [{'messages': [__(nick+' has joined the room')]}];
+                },
+
                 showStatusMessages: function (stanza) {
                     /* Check for status codes and communicate their purpose to the user.
                      * See: http://xmpp.org/registrar/mucstatus.html
@@ -1688,12 +1714,17 @@
                      *  (XMLElement) stanza: The message or presence stanza
                      *      containing the status codes.
                      */
-                    var is_self = stanza.querySelectorAll("status[code='110']").length;
                     var elements = sizzle('x[xmlns="'+Strophe.NS.MUC_USER+'"]', stanza);
-                    var notifications = _.map(
-                        elements,
-                        _.partial(this.parseXUserElement.bind(this), _, stanza, is_self)
-                    );
+                    var is_self = stanza.querySelectorAll("status[code='110']").length;
+                    var iteratee = _.partial(this.parseXUserElement.bind(this), _, stanza, is_self);
+                    var notifications = _.reject(_.map(elements, iteratee), _.isEmpty);
+                    if (_.isEmpty(notifications) &&
+                            _converse.muc_show_join_leave &&
+                            stanza.nodeName === 'presence' &&
+                            this.model.get('connection_status') === ROOMSTATUS.ENTERED
+                        ) {
+                        notifications = this.getJoinLeaveMessages(stanza);
+                    }
                     _.each(notifications, this.displayNotificationsforUser.bind(this));
                     return stanza;
                 },
@@ -1767,11 +1798,10 @@
                      *  (XMLElement) pres: The stanza
                      */
                     if (pres.getAttribute('type') === 'error') {
-                        this.model.save('connection_status', Strophe.Status.DISCONNECTED);
+                        this.model.save('connection_status', ROOMSTATUS.DISCONNECTED);
                         this.showErrorMessage(pres);
                         return true;
                     }
-                    var show_status_messages = true;
                     var is_self = pres.querySelector("status[code='110']");
                     var locked_room = pres.querySelector("status[code='201']");
                     if (is_self) {
@@ -1784,15 +1814,14 @@
                             } else {
                                 this.configureChatRoom();
                                 if (!this.model.get('auto_configure')) {
-                                    // We don't show status messages if the
-                                    // configuration form is being shown.
-                                    show_status_messages = false;
+                                    return;
                                 }
                             }
                         }
+                        this.model.save('connection_status', ROOMSTATUS.ENTERED);
                     }
                     if (!locked_room && !this.model.get('features_fetched') &&
-                            this.model.get('connection_status') !== Strophe.Status.CONNECTED) {
+                            this.model.get('connection_status') !== ROOMSTATUS.CONNECTED) {
                         // The features for this room weren't fetched yet, perhaps
                         // because it's a new room without locking (in which
                         // case Prosody doesn't send a 201 status).
@@ -1800,12 +1829,11 @@
                         // so a good time to fetch the features.
                         this.getRoomFeatures();
                     }
-                    if (show_status_messages) {
-                        this.hideSpinner().showStatusMessages(pres);
-                    }
+                    this.hideSpinner().showStatusMessages(pres);
                     this.occupantsview.updateOccupantsOnPresence(pres);
-                    if (this.model.get('role') !== 'none') {
-                        this.model.save('connection_status', Strophe.Status.CONNECTED);
+                    if (this.model.get('role') !== 'none' &&
+                            this.model.get('connection_status') === ROOMSTATUS.CONNECTING) {
+                        this.model.save('connection_status', ROOMSTATUS.CONNECTED);
                     }
                     return true;
                 },
@@ -2425,10 +2453,7 @@
                         'box_id': b64_sha1(room_jid),
                         'password': $x.attr('password')
                     });
-                    if (!_.includes(
-                                [Strophe.Status.CONNECTING, Strophe.Status.CONNECTED],
-                                chatroom.get('connection_status'))
-                            ) {
+                    if (chatroom.get('connection_status') === ROOMSTATUS.DISCONNECTED) {
                         _converse.chatboxviews.get(room_jid).join();
                     }
                 }
@@ -2549,7 +2574,7 @@
                  */
                 _converse.chatboxviews.each(function (view) {
                     if (view.model.get('type') === 'chatroom') {
-                        view.model.save('connection_status', Strophe.Status.DISCONNECTED);
+                        view.model.save('connection_status', ROOMSTATUS.DISCONNECTED);
                         view.join();
                     }
                 });
@@ -2563,7 +2588,7 @@
                  */
                 _converse.chatboxes.each(function (model) {
                     if (model.get('type') === 'chatroom') {
-                        model.save('connection_status', Strophe.Status.DISCONNECTED);
+                        model.save('connection_status', ROOMSTATUS.DISCONNECTED);
                     }
                 });
             };