Parcourir la source

MUC: Don't send XEP-0085 CSNs when we don't have voice

Includes some refactoring:

- Don't send an `active` chat state notification when entering a MUC
  I can't think of a good reason why this might be necessary or desired.
- Move `setChatState` form the view to the model
- Remove unused method `handleChatStateNotification`
- Don't store `role` and `affiliation` for the current user on the
  ChatRoom object, but instead on the ChatRoomOccupant object representing
  the user.
JC Brand il y a 6 ans
Parent
commit
ded9945ed9

+ 2 - 2
spec/chatbox.js

@@ -821,7 +821,7 @@
                         await test_utils.openChatBoxFor(_converse, contact_jid);
                         const view = _converse.chatboxviews.get(contact_jid);
                         spyOn(_converse.connection, 'send');
-                        spyOn(view, 'setChatState').and.callThrough();
+                        spyOn(view.model, 'setChatState').and.callThrough();
                         expect(view.model.get('chat_state')).toBe('active');
                         view.onKeyDown({
                             target: view.el.querySelector('textarea.chat-textarea'),
@@ -851,7 +851,7 @@
                             target: view.el.querySelector('textarea.chat-textarea'),
                             keyCode: 1
                         });
-                        expect(view.setChatState).toHaveBeenCalled();
+                        expect(view.model.setChatState).toHaveBeenCalled();
                         expect(view.model.get('chat_state')).toBe('composing');
 
                         view.onKeyDown({

+ 78 - 20
spec/muc.js

@@ -54,41 +54,41 @@
 
                 test_utils.createContacts(_converse, 'current');
                 await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group .group-toggle').length, 300);
-                await test_utils.openAndEnterChatRoom(_converse, 'chillout@montague.lit', 'romeo');
-                let jid = 'chillout@montague.lit';
-                let room = _converse.api.rooms.get(jid);
+                let muc_jid = 'chillout@montague.lit';
+                await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+                let room = _converse.api.rooms.get(muc_jid);
                 expect(room instanceof Object).toBeTruthy();
 
-                let chatroomview = _converse.chatboxviews.get(jid);
+                let chatroomview = _converse.chatboxviews.get(muc_jid);
                 expect(chatroomview.is_chatroom).toBeTruthy();
 
                 expect(u.isVisible(chatroomview.el)).toBeTruthy();
                 chatroomview.close();
 
                 // Test with mixed case
-                await test_utils.openAndEnterChatRoom(_converse, 'Leisure@montague.lit', 'romeo');
-                jid = 'Leisure@montague.lit';
-                room = _converse.api.rooms.get(jid);
+                muc_jid = 'Leisure@montague.lit';
+                await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+                room = _converse.api.rooms.get(muc_jid);
                 expect(room instanceof Object).toBeTruthy();
-                chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
+                chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase());
                 expect(u.isVisible(chatroomview.el)).toBeTruthy();
 
-                jid = 'leisure@montague.lit';
-                room = _converse.api.rooms.get(jid);
+                muc_jid = 'leisure@montague.lit';
+                room = _converse.api.rooms.get(muc_jid);
                 expect(room instanceof Object).toBeTruthy();
-                chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
+                chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase());
                 expect(u.isVisible(chatroomview.el)).toBeTruthy();
 
-                jid = 'leiSure@montague.lit';
-                room = _converse.api.rooms.get(jid);
+                muc_jid = 'leiSure@montague.lit';
+                room = _converse.api.rooms.get(muc_jid);
                 expect(room instanceof Object).toBeTruthy();
-                chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
+                chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase());
                 expect(u.isVisible(chatroomview.el)).toBeTruthy();
                 chatroomview.close();
 
                 // Non-existing room
-                jid = 'chillout2@montague.lit';
-                room = _converse.api.rooms.get(jid);
+                muc_jid = 'chillout2@montague.lit';
+                room = _converse.api.rooms.get(muc_jid);
                 expect(typeof room === 'undefined').toBeTruthy();
                 done();
             }));
@@ -331,7 +331,7 @@
                  *      </error>
                  *  </iq>
                  */
-                var result_stanza = $iq({
+                const result_stanza = $iq({
                     'type': 'error',
                     'id': stanza.getAttribute('id'),
                     'from': view.model.get('jid'),
@@ -339,10 +339,13 @@
                 }).c('error', {'type': 'cancel'})
                     .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
                 _converse.connection._dataRecv(test_utils.createRequest(result_stanza));
+
                 const input = await test_utils.waitUntil(() => view.el.querySelector('input[name="nick"]'));
+                expect(view.model.get('connection_status')).toBe(converse.ROOMSTATUS.NICKNAME_REQUIRED);
                 input.value = 'nicky';
                 view.el.querySelector('input[type=submit]').click();
                 expect(view.model.join).toHaveBeenCalled();
+                await test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
 
                 // The user has just entered the room (because join was called)
                 // and receives their own presence from the server.
@@ -369,10 +372,11 @@
                 }).up()
                 .c('status').attrs({code:'110'}).up()
                 .c('status').attrs({code:'201'}).nodeTree;
-
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
 
-                await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
+                await test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.ENTERED);
+                await test_utils.returnMemberLists(_converse, muc_jid);
+                // await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
 
                 const info_texts = Array.from(view.el.querySelectorAll('.chat-content .chat-info')).map(e => e.textContent);
                 expect(info_texts[0]).toBe('A new groupchat has been created');
@@ -389,6 +393,7 @@
                     `<iq id="${iq.getAttribute('id')}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
                         `<query xmlns="http://jabber.org/protocol/muc#owner"><x type="submit" xmlns="jabber:x:data"/>`+
                     `</query></iq>`);
+
                 done();
             }));
         });
@@ -1302,6 +1307,7 @@
                     .c('status', {code: '110'});
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(view.model.saveAffiliationAndRole).toHaveBeenCalled();
+                debugger;
                 expect(u.isVisible(view.el.querySelector('.toggle-chatbox-button'))).toBeTruthy();
                 await test_utils.waitUntil(() => !_.isNull(view.el.querySelector('.configure-chatroom-button')))
                 expect(u.isVisible(view.el.querySelector('.configure-chatroom-button'))).toBeTruthy();
@@ -4719,7 +4725,59 @@
             }));
         });
 
-        describe("A Chat Status Notification", function () {
+        describe("A XEP-0085 Chat Status Notification", function () {
+
+            it("is is not sent out to a MUC if the user is a visitor in a moderated room",
+                mock.initConverse(
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    async function (done, _converse) {
+
+                spyOn(_converse.ChatRoom.prototype, 'sendChatState').and.callThrough();
+
+                const muc_jid = 'lounge@montague.lit';
+                const features = [
+                    'http://jabber.org/protocol/muc',
+                    'jabber:iq:register',
+                    'muc_passwordprotected',
+                    'muc_hidden',
+                    'muc_temporary',
+                    'muc_membersonly',
+                    'muc_moderated',
+                    'muc_anonymous'
+                ]
+                await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
+
+                const view = _converse.api.chatviews.get(muc_jid);
+                view.model.setChatState(_converse.ACTIVE);
+
+                expect(view.model.sendChatState).toHaveBeenCalled();
+                const last_stanza = _converse.connection.sent_stanzas.pop();
+                expect(Strophe.serialize(last_stanza)).toBe(
+                    `<message to="lounge@montague.lit" type="groupchat" xmlns="jabber:client">`+
+                        `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
+                        `<no-store xmlns="urn:xmpp:hints"/>`+
+                        `<no-permanent-store xmlns="urn:xmpp:hints"/>`+
+                    `</message>`);
+
+                // Romeo loses his voice
+                const presence = $pres({
+                        to: 'romeo@montague.lit/orchard',
+                        from: `${muc_jid}/some1`
+                    }).c('x', {xmlns: Strophe.NS.MUC_USER})
+                    .c('item', {'affiliation': 'none', 'role': 'visitor'}).up()
+                    .c('status', {code: '110'});
+                _converse.connection._dataRecv(test_utils.createRequest(presence));
+
+                const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid});
+                expect(occupant.get('role')).toBe('visitor');
+
+                spyOn(_converse.connection, 'send');
+                view.model.setChatState(_converse.INACTIVE);
+                expect(view.model.sendChatState.calls.count()).toBe(2);
+                expect(_converse.connection.send).not.toHaveBeenCalled();
+                done();
+            }));
+
 
             describe("A composing notification", function () {
 

+ 6 - 39
src/converse-chatview.js

@@ -877,39 +877,6 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
-            /**
-             * Mutator for setting the chat state of this chat session.
-             * Handles clearing of any chat state notification timeouts and
-             * setting new ones if necessary.
-             * Timeouts are set when the  state being set is COMPOSING or PAUSED.
-             * After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
-             * See XEP-0085 Chat State Notifications.
-             * @private
-             * @method _converse.ChatBoxView#setChatState
-             * @param { string } state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
-             */
-            setChatState (state, options) {
-                if (!_.isUndefined(this.chat_state_timeout)) {
-                    window.clearTimeout(this.chat_state_timeout);
-                    delete this.chat_state_timeout;
-                }
-                if (state === _converse.COMPOSING) {
-                    this.chat_state_timeout = window.setTimeout(
-                        this.setChatState.bind(this),
-                        _converse.TIMEOUTS.PAUSED,
-                        _converse.PAUSED
-                    );
-                } else if (state === _converse.PAUSED) {
-                    this.chat_state_timeout = window.setTimeout(
-                        this.setChatState.bind(this),
-                        _converse.TIMEOUTS.INACTIVE,
-                        _converse.INACTIVE
-                    );
-                }
-                this.model.set('chat_state', state, options);
-                return this;
-            },
-
             async onFormSubmitted (ev) {
                 ev.preventDefault();
                 const textarea = this.el.querySelector('.chat-textarea');
@@ -955,7 +922,7 @@ converse.plugins.add('converse-chatview', {
                 textarea.focus();
                 // Suppress events, otherwise superfluous CSN gets set
                 // immediately after the message, causing rate-limiting issues.
-                this.setChatState(_converse.ACTIVE, {'silent': true});
+                this.model.setChatState(_converse.ACTIVE, {'silent': true});
             },
 
             updateCharCounter (chars) {
@@ -1038,7 +1005,7 @@ converse.plugins.add('converse-chatview', {
                 if (this.model.get('chat_state') !== _converse.COMPOSING) {
                     // Set chat state to composing if keyCode is not a forward-slash
                     // (which would imply an internal command and not a message).
-                    this.setChatState(_converse.COMPOSING);
+                    this.model.setChatState(_converse.COMPOSING);
                 }
             },
 
@@ -1283,7 +1250,7 @@ converse.plugins.add('converse-chatview', {
                 if (_converse.connection.connected) {
                     // Immediately sending the chat state, because the
                     // model is going to be destroyed afterwards.
-                    this.setChatState(_converse.INACTIVE);
+                    this.model.setChatState(_converse.INACTIVE);
                     this.model.sendChatState();
                 }
                 this.model.close();
@@ -1336,7 +1303,7 @@ converse.plugins.add('converse-chatview', {
 
             afterShown () {
                 this.model.clearUnreadMsgCounter();
-                this.setChatState(_converse.ACTIVE);
+                this.model.setChatState(_converse.ACTIVE);
                 this.scrollDown();
                 if (_converse.auto_focus) {
                     this.focus();
@@ -1425,13 +1392,13 @@ converse.plugins.add('converse-chatview', {
             onWindowStateChanged (state) {
                 if (state === 'visible') {
                     if (!this.model.isHidden()) {
-                        this.setChatState(_converse.ACTIVE);
+                        this.model.setChatState(_converse.ACTIVE);
                         if (this.model.get('num_unread', 0)) {
                             this.model.clearUnreadMsgCounter();
                         }
                     }
                 } else if (state === 'hidden') {
-                    this.setChatState(_converse.INACTIVE, {'silent': true});
+                    this.model.setChatState(_converse.INACTIVE, {'silent': true});
                     this.model.sendChatState();
                 }
             }

+ 2 - 2
src/converse-minimize.js

@@ -206,7 +206,7 @@ converse.plugins.add('converse-minimize', {
                 if (!this.model.isScrolledUp()) {
                     this.model.clearUnreadMsgCounter();
                 }
-                this.setChatState(_converse.INACTIVE);
+                this.model.setChatState(_converse.INACTIVE);
                 this.show();
                 /**
                  * Triggered when a previously minimized chat gets maximized
@@ -235,7 +235,7 @@ converse.plugins.add('converse-minimize', {
                 } else {
                     this.model.set({'scroll': this.content.scrollTop});
                 }
-                this.setChatState(_converse.INACTIVE);
+                this.model.setChatState(_converse.INACTIVE);
                 this.hide();
                 /**
                  * Triggered when a previously maximized chat gets Minimized

+ 9 - 25
src/converse-muc-views.js

@@ -532,7 +532,7 @@ converse.plugins.add('converse-muc-views', {
 
             renderBottomPanel () {
                 const container = this.el.querySelector('.bottom-panel');
-                if (this.model.features.get('moderated') && this.model.get('role') === 'visitor') {
+                if (this.model.features.get('moderated') && this.model.getOwnOccupant().get('role') === 'visitor') {
                     container.innerHTML = tpl_chatroom_bottom_panel({'__': __});
                 } else {
                     if (!container.firstElementChild || !container.querySelector('.sendXMPPMessage')) {
@@ -708,8 +708,6 @@ converse.plugins.add('converse-muc-views', {
                     this.showSpinner();
                 } else if (conn_status === converse.ROOMSTATUS.ENTERED) {
                     this.hideSpinner();
-                    this.setChatState(_converse.ACTIVE);
-                    this.scrollDown();
                     if (_converse.auto_focus) {
                         this.focus();
                     }
@@ -725,7 +723,7 @@ converse.plugins.add('converse-muc-views', {
                     _converse.ChatBoxView.prototype.getToolbarOptions.apply(this, arguments),
                     {
                       'label_hide_occupants': __('Hide the list of participants'),
-                      'show_occupants_toggle': this.is_chatroom && _converse.visible_toolbar_buttons.toggle_occupants
+                      'show_occupants_toggle': _converse.visible_toolbar_buttons.toggle_occupants
                     }
                 );
             },
@@ -789,24 +787,6 @@ converse.plugins.add('converse-muc-views', {
                 this.insertIntoTextArea(ev.target.textContent);
             },
 
-            handleChatStateNotification (message) {
-                /* Override the method on the ChatBoxView base class to
-                 * ignore <gone/> notifications in groupchats.
-                 *
-                 * As laid out in the business rules in XEP-0085
-                 * https://xmpp.org/extensions/xep-0085.html#bizrules-groupchat
-                 */
-                if (message.get('fullname') === this.model.get('nick')) {
-                    // Don't know about other servers, but OpenFire sends
-                    // back to you your own chat state notifications.
-                    // We ignore them here...
-                    return;
-                }
-                if (message.get('chat_state') !== _converse.GONE) {
-                    _converse.ChatBoxView.prototype.handleChatStateNotification.apply(this, arguments);
-                }
-            },
-
             verifyRoles (roles, occupant, show_error=true) {
                 if (!Array.isArray(roles)) {
                     throw new TypeError('roles must be an Array');
@@ -1462,7 +1442,8 @@ converse.plugins.add('converse-muc-views', {
                     this.renderNicknameForm();
                 } else if (this.model.get('connection_status') == converse.ROOMSTATUS.PASSWORD_REQUIRED) {
                     this.renderPasswordForm();
-                } else {
+                } else if (this.model.get('connection_status') == converse.ROOMSTATUS.ENTERED) {
+                    this.hideChatRoomContents();
                     u.showElement(this.el.querySelector('.chat-area'));
                     u.showElement(this.el.querySelector('.occupants'));
                     this.scrollDown();
@@ -1726,10 +1707,13 @@ converse.plugins.add('converse-muc-views', {
             async initialize () {
                 OrderedListView.prototype.initialize.apply(this, arguments);
 
+                this.model.on(
+                    'change:affiliation',
+                    o => (o.get('jid') === _converse.bare_jid) && this.renderInviteWidget()
+                );
                 this.chatroomview = this.model.chatroomview;
                 this.chatroomview.model.features.on('change', this.renderRoomFeatures, this);
                 this.chatroomview.model.features.on('change:open', this.renderInviteWidget, this);
-                this.chatroomview.model.on('change:affiliation', this.renderInviteWidget, this);
                 this.chatroomview.model.on('change:hidden_occupants', this.setVisibility, this);
                 this.render();
                 await this.model.fetched;
@@ -1843,7 +1827,7 @@ converse.plugins.add('converse-muc-views', {
             shouldInviteWidgetBeShown () {
                 return _converse.allow_muc_invitations &&
                     (this.chatroomview.model.features.get('open') ||
-                        this.chatroomview.model.get('affiliation') === "owner"
+                        this.chatroomview.model.getOwnOccupant().get('affiliation') === "owner"
                     );
             },
 

+ 39 - 6
src/headless/converse-chatboxes.js

@@ -401,6 +401,39 @@ converse.plugins.add('converse-chatboxes', {
                 }
             },
 
+            /**
+             * Mutator for setting the chat state of this chat session.
+             * Handles clearing of any chat state notification timeouts and
+             * setting new ones if necessary.
+             * Timeouts are set when the  state being set is COMPOSING or PAUSED.
+             * After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
+             * See XEP-0085 Chat State Notifications.
+             * @private
+             * @method _converse.ChatBox#setChatState
+             * @param { string } state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
+             */
+            setChatState (state, options) {
+                if (!_.isUndefined(this.chat_state_timeout)) {
+                    window.clearTimeout(this.chat_state_timeout);
+                    delete this.chat_state_timeout;
+                }
+                if (state === _converse.COMPOSING) {
+                    this.chat_state_timeout = window.setTimeout(
+                        this.setChatState.bind(this),
+                        _converse.TIMEOUTS.PAUSED,
+                        _converse.PAUSED
+                    );
+                } else if (state === _converse.PAUSED) {
+                    this.chat_state_timeout = window.setTimeout(
+                        this.setChatState.bind(this),
+                        _converse.TIMEOUTS.INACTIVE,
+                        _converse.INACTIVE
+                    );
+                }
+                this.set('chat_state', state, options);
+                return this;
+            },
+
             /**
              * @private
              * @method _converse.ChatBox#shouldShowErrorMessage
@@ -675,10 +708,8 @@ converse.plugins.add('converse-chatboxes', {
              *
              * @method _converse.ChatBox#sendMessage
              * @memberOf _converse.ChatBox
-             *
              * @param {String} text - The chat message text
              * @param {String} spoiler_hint - An optional hint, if the message being sent is a spoiler
-             *
              * @example
              * const chat = _converse.api.chats.get('buddy1@example.com');
              * chat.sendMessage('hello world');
@@ -705,11 +736,13 @@ converse.plugins.add('converse-chatboxes', {
                 return true;
             },
 
+            /**
+             * Sends a message with the current XEP-0085 chat state of the user
+             * as taken from the `chat_state` attribute of the {@link _converse.ChatBox}.
+             * @private
+             * @method _converse.ChatBox#sendChatState
+             */
             sendChatState () {
-                /* Sends a message with the status of the user in this chat session
-                 * as taken from the 'chat_state' attribute of the chat box.
-                 * See XEP-0085 Chat State Notifications.
-                 */
                 if (_converse.send_chat_state_notifications && this.get('chat_state')) {
                     _converse.api.send(
                         $msg({

+ 2 - 2
src/headless/converse-core.js

@@ -735,7 +735,7 @@ _converse.initialize = async function (settings, callback) {
     this.generateResource = () => `/converse.js-${Math.floor(Math.random()*139749528).toString()}`;
 
     /**
-     * Send out a Chat Status Notification (XEP-0352)
+     * Send out a Client State Indication (XEP-0352)
      * @private
      * @method sendCSI
      * @memberOf _converse
@@ -1223,7 +1223,7 @@ _converse.initialize = async function (settings, callback) {
         if (credentials) {
             this.autoLogin(credentials);
         } else if (this.auto_login) {
-            if (this.credentials_url && _converse.authentication === 'login') {
+            if (this.credentials_url && _converse.authentication === _converse.LOGIN) {
                 const data = await getLoginCredentials();
                 this.autoLogin(data);
             } else if (!this.jid) {

+ 50 - 19
src/headless/converse-muc.js

@@ -354,7 +354,6 @@ converse.plugins.add('converse-muc', {
                     // generally unread messages (which *includes* mentions!).
                     'num_unread_general': 0,
 
-                    'affiliation': null,
                     'bookmarked': false,
                     'chat_state': undefined,
                     'connection_status': converse.ROOMSTATUS.DISCONNECTED,
@@ -377,10 +376,10 @@ converse.plugins.add('converse-muc', {
                 }
                 this.set('box_id', `box-${btoa(this.get('jid'))}`);
 
+                this.initFeatures(); // sendChatState depends on this.features
                 this.on('change:chat_state', this.sendChatState, this);
                 this.on('change:connection_status', this.onConnectionStatusChanged, this);
 
-                this.initFeatures();
                 this.initOccupants();
                 this.registerHandlers();
                 this.initMessages();
@@ -707,12 +706,16 @@ converse.plugins.add('converse-muc', {
             },
 
             /**
-             * Sends a message with the status of the user in this chat session
-             * as taken from the 'chat_state' attribute of the chat box.
-             * See XEP-0085 Chat State Notifications.
+             * Sends a message with the current XEP-0085 chat state of the user
+             * as taken from the `chat_state` attribute of the {@link _converse.ChatRoom}.
+             * @private
+             * @method _converse.ChatRoom#sendChatState
              */
             sendChatState () {
-                if (!_converse.send_chat_state_notifications || this.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
+                if (!_converse.send_chat_state_notifications ||
+                        !this.get('chat_state') ||
+                        this.get('connection_status') !== converse.ROOMSTATUS.ENTERED ||
+                        this.features.get('moderated') && this.getOwnOccupant().get('role') === 'visitor') {
                     return;
                 }
                 const chat_state = this.get('chat_state');
@@ -970,8 +973,30 @@ converse.plugins.add('converse-muc', {
                 return _converse.api.sendIQ(iq).then(callback).catch(errback);
             },
 
+
             /**
-             * Parse the presence stanza for the current user's affiliation.
+             * Get the {@link _converse.ChatRoomOccupant} instance which
+             * represents the current user.
+             * @private
+             * @method _converse.ChatRoom#getOwnOccupant
+             * @returns { _converse.ChatRoomOccupant }
+             */
+            getOwnOccupant () {
+                const occupant = this.occupants.findWhere({'jid': _converse.bare_jid});
+                if (occupant) {
+                    return occupant;
+                }
+                const attributes = {
+                    'jid': _converse.bare_jid,
+                    'resource': Strophe.getResourceFromJid(_converse.resource)
+                };
+                return this.occupants.create(attributes);
+            },
+
+            /**
+             * Parse the presence stanza for the current user's affiliation and
+             * role and save them on the relevant {@link _converse.ChatRoomOccupant}
+             * instance.
              * @private
              * @method _converse.ChatRoom#saveAffiliationAndRole
              * @param { XMLElement } pres - A <presence> stanza.
@@ -982,11 +1007,15 @@ converse.plugins.add('converse-muc', {
                 if (is_self && !_.isNil(item)) {
                     const affiliation = item.getAttribute('affiliation');
                     const role = item.getAttribute('role');
+                    const changes = {};
                     if (affiliation) {
-                        this.save({'affiliation': affiliation});
+                        changes['affiliation'] = affiliation;
                     }
                     if (role) {
-                        this.save({'role': role});
+                        changes['role'] = role;
+                    }
+                    if (!_.isEmpty(changes)) {
+                        this.getOwnOccupant().save(changes);
                     }
                 }
             },
@@ -1549,7 +1578,6 @@ converse.plugins.add('converse-muc', {
                     return;
                 }
                 const codes = sizzle('status', x).map(s => s.getAttribute('code'));
-
                 codes.forEach(code => {
                     let message;
                     if (code === '110' || (code === '100' && !is_self)) {
@@ -1579,7 +1607,6 @@ converse.plugins.add('converse-muc', {
                         this.save('nick', nick);
                         message = __(_converse.muc.new_nickname_messages[code], nick);
                     }
-
                     if (message) {
                         this.messages.create({'type': 'info', message});
                     }
@@ -1689,14 +1716,15 @@ converse.plugins.add('converse-muc', {
                 if (stanza.getAttribute('type') === 'error') {
                     return this.onErrorPresence(stanza);
                 }
+                this.createInfoMessages(stanza);
                 if (stanza.querySelector("status[code='110']")) {
                     this.onOwnPresence(stanza);
-                }
-                this.createInfoMessages(stanza);
-                this.updateOccupantsOnPresence(stanza);
-
-                if (this.get('role') !== 'none' && this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
-                    this.save('connection_status', converse.ROOMSTATUS.CONNECTED);
+                    if (this.getOwnOccupant().get('role') !== 'none' &&
+                            this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
+                        this.save('connection_status', converse.ROOMSTATUS.CONNECTED);
+                    }
+                } else {
+                    this.updateOccupantsOnPresence(stanza);
                 }
             },
 
@@ -1716,6 +1744,10 @@ converse.plugins.add('converse-muc', {
              * @param { XMLElement } pres - The stanza
              */
             onOwnPresence (stanza) {
+                if (stanza.getAttribute('type') !== 'unavailable') {
+                    this.save('connection_status', converse.ROOMSTATUS.ENTERED);
+                }
+                this.updateOccupantsOnPresence(stanza);
                 this.saveAffiliationAndRole(stanza);
 
                 if (stanza.getAttribute('type') === 'unavailable') {
@@ -1746,13 +1778,12 @@ converse.plugins.add('converse-muc', {
                         // (in which case Prosody doesn't send a 201 status),
                         // otherwise the features would have been fetched in
                         // the "initialize" method already.
-                        if (this.get('affiliation') === 'owner' && this.get('auto_configure')) {
+                        if (this.getOwnOccupant().get('affiliation') === 'owner' && this.get('auto_configure')) {
                             this.autoConfigureChatRoom().then(() => this.refreshRoomFeatures());
                         } else {
                             this.getRoomFeatures();
                         }
                     }
-                    this.save('connection_status', converse.ROOMSTATUS.ENTERED);
                 }
             },
 

+ 44 - 34
tests/utils.js

@@ -121,17 +121,17 @@
     };
 
     utils.getRoomFeatures = async function (_converse, room, server, features=[]) {
-        const room_jid = `${room}@${server}`.toLowerCase();
+        const muc_jid = `${room}@${server}`.toLowerCase();
         const stanzas = _converse.connection.IQ_stanzas;
         const index = stanzas.length-1;
         const stanza = await utils.waitUntil(() => _.filter(
             stanzas.slice(index),
             iq => iq.querySelector(
-                `iq[to="${room_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+                `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
             )).pop());
 
         const features_stanza = $iq({
-            'from': room_jid,
+            'from': muc_jid,
             'id': stanza.getAttribute('id'),
             'to': 'romeo@montague.lit/desktop',
             'type': 'result'
@@ -164,24 +164,18 @@
         _converse.connection._dataRecv(utils.createRequest(features_stanza));
     };
 
-    utils.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[], members=[]) {
-        const room = Strophe.getNodeFromJid(muc_jid);
-        const server = Strophe.getDomainFromJid(muc_jid);
-        const room_jid = `${room}@${server}`.toLowerCase();
-        const stanzas = _converse.connection.IQ_stanzas;
-        await _converse.api.rooms.open(room_jid);
-        await utils.getRoomFeatures(_converse, room, server, features);
 
+    utils.waitForReservedNick = async function (_converse, muc_jid, nick) {
+        const view = _converse.chatboxviews.get(muc_jid);
+        const stanzas = _converse.connection.IQ_stanzas;
         const iq = await utils.waitUntil(() => _.filter(
             stanzas,
-            s => sizzle(`iq[to="${room_jid}"] query[node="x-roomuser-item"]`, s).length
+            s => sizzle(`iq[to="${muc_jid.toLowerCase()}"] query[node="x-roomuser-item"]`, s).length
         ).pop());
-
         // We remove the stanza, otherwise we might get stale stanzas returned in our filter above.
         stanzas.splice(stanzas.indexOf(iq), 1)
 
         // The XMPP server returns the reserved nick for this user.
-        const view = _converse.chatboxviews.get(room_jid);
         const IQ_id = iq.getAttribute('id');
         const stanza = $iq({
             'type': 'result',
@@ -191,29 +185,15 @@
         }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info', 'node': 'x-roomuser-item'})
             .c('identity', {'category': 'conference', 'name': nick, 'type': 'text'});
         _converse.connection._dataRecv(utils.createRequest(stanza));
-        await utils.waitUntil(() => view.model.get('nick'));
+        return utils.waitUntil(() => view.model.get('nick'));
+    };
 
-        // The user has just entered the room (because join was called)
-        // and receives their own presence from the server.
-        // See example 24: https://xmpp.org/extensions/xep-0045.html#enter-pres
-        const presence = $pres({
-                to: _converse.connection.jid,
-                from: `${room_jid}/${nick}`,
-                id: u.getUniqueId()
-        }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
-            .c('item').attrs({
-                affiliation: 'owner',
-                jid: _converse.bare_jid,
-                role: 'moderator'
-            }).up()
-            .c('status').attrs({code:'110'});
-        _converse.connection._dataRecv(utils.createRequest(presence));
-        await utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.ENTERED));
 
-        // Now we return the (empty) member lists
+    utils.returnMemberLists = async function (_converse, muc_jid, members=[]) {
+        const stanzas = _converse.connection.IQ_stanzas;
         const member_IQ = await utils.waitUntil(() => _.filter(
             stanzas,
-            s => sizzle(`iq[to="${room_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="member"]`, s).length
+            s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="member"]`, s).length
         ).pop());
         const member_list_stanza = $iq({
                 'from': 'coven@chat.shakespeare.lit',
@@ -233,7 +213,7 @@
 
         const admin_IQ = await utils.waitUntil(() => _.filter(
             stanzas,
-            s => sizzle(`iq[to="${room_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="admin"]`, s).length
+            s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="admin"]`, s).length
         ).pop());
         const admin_list_stanza = $iq({
                 'from': 'coven@chat.shakespeare.lit',
@@ -245,7 +225,7 @@
 
         const owner_IQ = await utils.waitUntil(() => _.filter(
             stanzas,
-            s => sizzle(`iq[to="${room_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="owner"]`, s).length
+            s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="owner"]`, s).length
         ).pop());
         const owner_list_stanza = $iq({
                 'from': 'coven@chat.shakespeare.lit',
@@ -256,6 +236,36 @@
         _converse.connection._dataRecv(utils.createRequest(owner_list_stanza));
     };
 
+
+    utils.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[], members=[]) {
+        muc_jid = muc_jid.toLowerCase();
+        const room = Strophe.getNodeFromJid(muc_jid);
+        const server = Strophe.getDomainFromJid(muc_jid);
+        await _converse.api.rooms.open(muc_jid);
+        await utils.getRoomFeatures(_converse, room, server, features);
+        await utils.waitForReservedNick(_converse, muc_jid, nick);
+
+        // The user has just entered the room (because join was called)
+        // and receives their own presence from the server.
+        // See example 24: https://xmpp.org/extensions/xep-0045.html#enter-pres
+        const presence = $pres({
+                to: _converse.connection.jid,
+                from: `${muc_jid}/${nick}`,
+                id: u.getUniqueId()
+        }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
+            .c('item').attrs({
+                affiliation: 'owner',
+                jid: _converse.bare_jid,
+                role: 'moderator'
+            }).up()
+            .c('status').attrs({code:'110'});
+        _converse.connection._dataRecv(utils.createRequest(presence));
+
+        const view = _converse.chatboxviews.get(muc_jid);
+        await utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.ENTERED));
+        await utils.returnMemberLists(_converse, muc_jid, members);
+    };
+
     utils.clearBrowserStorage = function () {
         window.localStorage.clear();
         window.sessionStorage.clear();