瀏覽代碼

converse-muc: Create `info` and `error` messages on the model

instead of on the view.
JC Brand 6 年之前
父節點
當前提交
bbe2a62295
共有 7 個文件被更改,包括 459 次插入384 次删除
  1. 2 1
      spec/messages.js
  2. 148 36
      spec/muc.js
  3. 12 0
      src/converse-message-view.js
  4. 12 0
      src/converse-minimize.js
  5. 45 312
      src/converse-muc-views.js
  6. 236 33
      src/headless/converse-muc.js
  7. 4 2
      tests/utils.js

+ 2 - 1
spec/messages.js

@@ -2344,7 +2344,8 @@
                 .c('status').attrs({code:'210'}).nodeTree;
             _converse.connection._dataRecv(test_utils.createRequest(presence));
             view.model.sendMessage('hello world');
-            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+            await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3);
+
             expect(view.model.messages.last().get('affiliation')).toBe('owner');
             expect(view.model.messages.last().get('role')).toBe('moderator');
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);

+ 148 - 36
spec/muc.js

@@ -359,7 +359,7 @@
                  */
                 const presence = $pres({
                         to:'romeo@montague.lit/orchard',
-                        from:'lounge@montague.lit/thirdwitch',
+                        from:'lounge@montague.lit/nicky',
                         id:'5025e055-036c-4bc5-a227-706e7e352053'
                 }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
                 .c('item').attrs({
@@ -371,9 +371,12 @@
                 .c('status').attrs({code:'201'}).nodeTree;
 
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                const info_text = view.el.querySelector('.chat-content .chat-info').textContent;
-                expect(info_text).toBe('A new groupchat has been created');
 
+                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');
+                expect(info_texts[1]).toBe('nicky has entered the groupchat');
 
                 // An instant room is created by saving the default configuratoin.
                 //
@@ -448,7 +451,7 @@
                 done()
             }));
 
-            it("shows a notification if its not anonymous",
+            it("shows a notification if it's not anonymous",
                 mock.initConverse(
                     null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
@@ -465,7 +468,7 @@
                  *      </x>
                  *  </presence></body>
                  */
-                let presence = $pres({
+                const presence = $pres({
                         to: 'romeo@montague.lit/orchard',
                         from: 'coven@chat.shakespeare.lit/some1'
                     }).c('x', {xmlns: Strophe.NS.MUC_USER})
@@ -476,28 +479,9 @@
                     }).up()
                     .c('status', {code: '110'}).up()
                     .c('status', {code: '100'});
-
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('.chat-info').length).toBe(2);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent)
-                    .toBe("This groupchat is not anonymous");
-                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
-                    .toBe("some1 has entered the groupchat");
 
-                // Check that we don't show the notification twice
-                presence = $pres({
-                        to: 'romeo@montague.lit/orchard',
-                        from: 'coven@chat.shakespeare.lit/some1'
-                    }).c('x', {xmlns: Strophe.NS.MUC_USER})
-                    .c('item', {
-                        'affiliation': 'owner',
-                        'jid': 'romeo@montague.lit/_converse.js-29092160',
-                        'role': 'moderator'
-                    }).up()
-                    .c('status', {code: '110'}).up()
-                    .c('status', {code: '100'});
-                _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('.chat-info').length).toBe(2);
+                await test_utils.waitUntil(() => chat_content.querySelectorAll('.chat-info').length === 2);
                 expect(sizzle('div.chat-info:first', chat_content).pop().textContent)
                     .toBe("This groupchat is not anonymous");
                 expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
@@ -1814,6 +1798,7 @@
                     .c('status').attrs({code:'210'}).nodeTree;
 
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
+                await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
                 const info_text = sizzle('.chat-content .chat-info:first', view.el).pop().textContent;
                 expect(info_text).toBe('Your nickname has been automatically set to thirdwitch');
                 done();
@@ -2096,7 +2081,7 @@
                 done();
             }));
 
-            it("informs users if their nicknames has been changed.",
+            it("informs users if their nicknames have been changed.",
                 mock.initConverse(
                     null, ['rosterGroupsFetched'], {},
                     async function (done, _converse) {
@@ -2167,11 +2152,12 @@
                     .c('status').attrs({code:'110'}).nodeTree;
 
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(2);
+                await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
+
                 expect(sizzle('div.chat-info:last').pop().textContent).toBe(
                     __(_converse.muc.new_nickname_messages["303"], "newnick")
                 );
-                expect(view.model.get('connection_status')).toBe(converse.ROOMSTATUS.DISCONNECTED);
+                expect(view.model.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED);
 
                 occupants = view.el.querySelector('.occupant-list');
                 expect(occupants.childNodes.length).toBe(1);
@@ -2509,6 +2495,7 @@
                     .c('status', {code: '104'}).up()
                     .c('status', {code: '172'});
                 _converse.connection._dataRecv(test_utils.createRequest(message));
+                await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 3);
                 const chat_body = view.el.querySelector('.chatroom-body');
                 expect(sizzle('.message:last', chat_body).pop().textContent)
                     .toBe('This groupchat is now no longer anonymous');
@@ -2551,6 +2538,7 @@
                     .up()
                     .c('status').attrs({code:'110'}).up()
                     .c('status').attrs({code:'307'}).nodeTree;
+
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
 
                 const view = _converse.chatboxviews.get('lounge@montague.lit');
@@ -3232,6 +3220,8 @@
                         }).up()
                         .c('status', {'code': '307'});
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
+
+                await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 4);
                 expect(view.el.querySelectorAll('.chat-info')[3].textContent).toBe("annoying guy has been kicked out");
                 expect(view.el.querySelectorAll('.chat-info').length).toBe(4);
                 done();
@@ -3628,6 +3618,33 @@
 
                 const groupchat_jid = 'members-only@muc.montague.lit'
                 await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo');
+                const view = _converse.chatboxviews.get(groupchat_jid);
+                const iq = await test_utils.waitUntil(() => _.filter(
+                    _converse.connection.IQ_stanzas,
+                    iq => iq.querySelector(
+                        `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+                    )).pop());
+
+                // State that the chat is members-only via the features IQ
+                const features_stanza = $iq({
+                        'from': groupchat_jid,
+                        'id': iq.getAttribute('id'),
+                        'to': 'romeo@montague.lit/desktop',
+                        'type': 'result'
+                    })
+                    .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
+                        .c('identity', {
+                            'category': 'conference',
+                            'name': 'A Dark Cave',
+                            'type': 'text'
+                        }).up()
+                        .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up()
+                        .c('feature', {'var': 'muc_hidden'}).up()
+                        .c('feature', {'var': 'muc_temporary'}).up()
+                        .c('feature', {'var': 'muc_membersonly'}).up();
+                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
+                await test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
+
                 const presence = $pres().attrs({
                         from: `${groupchat_jid}/romeo`,
                         id: u.getUniqueId(),
@@ -3637,8 +3654,6 @@
                       .c('error').attrs({by:'lounge@montague.lit', type:'auth'})
                           .c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
 
-                const view = _converse.chatboxviews.get(groupchat_jid);
-                spyOn(view, 'showErrorMessage').and.callThrough();
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent)
                     .toBe('You are not on the member list of this groupchat.');
@@ -3652,6 +3667,29 @@
 
                 const groupchat_jid = 'off-limits@muc.montague.lit'
                 await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo');
+
+                const iq = await test_utils.waitUntil(() => _.filter(
+                    _converse.connection.IQ_stanzas,
+                    iq => iq.querySelector(
+                        `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+                    )).pop());
+
+                const features_stanza = $iq({
+                        'from': groupchat_jid,
+                        'id': iq.getAttribute('id'),
+                        'to': 'romeo@montague.lit/desktop',
+                        'type': 'result'
+                    })
+                    .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
+                        .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up()
+                        .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up()
+                        .c('feature', {'var': 'muc_hidden'}).up()
+                        .c('feature', {'var': 'muc_temporary'}).up()
+                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
+
+                const view = _converse.chatboxviews.get(groupchat_jid);
+                await test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
+
                 const presence = $pres().attrs({
                         from: `${groupchat_jid}/romeo`,
                         id: u.getUniqueId(),
@@ -3661,7 +3699,6 @@
                       .c('error').attrs({by:'lounge@montague.lit', type:'auth'})
                           .c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
 
-                const view = _converse.chatboxviews.get(groupchat_jid);
                 spyOn(view, 'showErrorMessage').and.callThrough();
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent)
@@ -3697,6 +3734,7 @@
                 done();
             }));
 
+
             it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true",
                 mock.initConverse(
                     null, ['rosterGroupsFetched'], {},
@@ -3765,7 +3803,26 @@
 
                 const groupchat_jid = 'impermissable@muc.montague.lit'
                 await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo')
-                var presence = $pres().attrs({
+
+                // We pretend this is a new room, so no disco info is returned.
+                const iq = await test_utils.waitUntil(() => _.filter(
+                    _converse.connection.IQ_stanzas,
+                    iq => iq.querySelector(
+                        `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+                    )).pop());
+                const features_stanza = $iq({
+                        'from': 'room@conference.example.org',
+                        'id': iq.getAttribute('id'),
+                        'to': 'romeo@montague.lit/desktop',
+                        'type': 'error'
+                    }).c('error', {'type': 'cancel'})
+                        .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
+                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
+
+                const view = _converse.chatboxviews.get(groupchat_jid);
+                await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
+
+                const presence = $pres().attrs({
                         from: `${groupchat_jid}/romeo`,
                         id: u.getUniqueId(),
                         to:'romeo@montague.lit/pda',
@@ -3773,7 +3830,6 @@
                     }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
                       .c('error').attrs({by:'lounge@montague.lit', type:'cancel'})
                           .c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
-                const view = _converse.chatboxviews.get(groupchat_jid);
                 spyOn(view, 'showErrorMessage').and.callThrough();
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent)
@@ -3788,6 +3844,25 @@
 
                 const groupchat_jid = 'conformist@muc.montague.lit'
                 await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo');
+
+                const iq = await test_utils.waitUntil(() => _.filter(
+                    _converse.connection.IQ_stanzas,
+                    iq => iq.querySelector(
+                        `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+                    )).pop());
+                const features_stanza = $iq({
+                        'from': groupchat_jid,
+                        'id': iq.getAttribute('id'),
+                        'to': 'romeo@montague.lit/desktop',
+                        'type': 'result'
+                    }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
+                        .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up()
+                        .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up()
+                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
+
+                const view = _converse.chatboxviews.get(groupchat_jid);
+                await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
+
                 const presence = $pres().attrs({
                         from: `${groupchat_jid}/romeo`,
                         id: u.getUniqueId(),
@@ -3797,7 +3872,6 @@
                       .c('error').attrs({by:'lounge@montague.lit', type:'cancel'})
                           .c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
 
-                const view = _converse.chatboxviews.get(groupchat_jid);
                 spyOn(view, 'showErrorMessage').and.callThrough();
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent)
@@ -3812,6 +3886,25 @@
 
                 const groupchat_jid = 'nonexistent@muc.montague.lit'
                 await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo');
+
+                const iq = await test_utils.waitUntil(() => _.filter(
+                    _converse.connection.IQ_stanzas,
+                    iq => iq.querySelector(
+                        `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+                    )).pop());
+                const features_stanza = $iq({
+                        'from': groupchat_jid,
+                        'id': iq.getAttribute('id'),
+                        'to': 'romeo@montague.lit/desktop',
+                        'type': 'result'
+                    }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
+                        .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up()
+                        .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up()
+                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
+
+                const view = _converse.chatboxviews.get(groupchat_jid);
+                await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
+
                 const presence = $pres().attrs({
                         from: `${groupchat_jid}/romeo`,
                         id: u.getUniqueId(),
@@ -3821,7 +3914,6 @@
                       .c('error').attrs({by:'lounge@montague.lit', type:'cancel'})
                           .c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
 
-                const view = _converse.chatboxviews.get(groupchat_jid);
                 spyOn(view, 'showErrorMessage').and.callThrough();
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent)
@@ -3836,6 +3928,25 @@
 
                 const groupchat_jid = 'maxed-out@muc.montague.lit'
                 await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo')
+
+                const iq = await test_utils.waitUntil(() => _.filter(
+                    _converse.connection.IQ_stanzas,
+                    iq => iq.querySelector(
+                        `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+                    )).pop());
+                const features_stanza = $iq({
+                        'from': groupchat_jid,
+                        'id': iq.getAttribute('id'),
+                        'to': 'romeo@montague.lit/desktop',
+                        'type': 'result'
+                    }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
+                        .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up()
+                        .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up()
+                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
+
+                const view = _converse.chatboxviews.get(groupchat_jid);
+                await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
+
                 const presence = $pres().attrs({
                         from: `${groupchat_jid}/romeo`,
                         id: u.getUniqueId(),
@@ -3845,7 +3956,6 @@
                       .c('error').attrs({by:'lounge@montague.lit', type:'cancel'})
                           .c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
 
-                const view = _converse.chatboxviews.get(groupchat_jid);
                 spyOn(view, 'showErrorMessage').and.callThrough();
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent)
@@ -4878,3 +4988,5 @@
         });
     });
 }));
+
+

+ 12 - 0
src/converse-message-view.js

@@ -112,6 +112,8 @@ converse.plugins.add('converse-message-view', {
                     this.renderFileUploadProgresBar();
                 } else if (this.model.get('type') === 'error') {
                     this.renderErrorMessage();
+                } else if (this.model.get('type') === 'info') {
+                    this.renderInfoMessage();
                 } else {
                     await this.renderChatMessage();
                 }
@@ -212,6 +214,16 @@ converse.plugins.add('converse-message-view', {
                 }
             },
 
+            renderInfoMessage () {
+                const msg = u.stringToElement(
+                    tpl_info(Object.assign(this.model.toJSON(), {
+                        'extra_classes': 'chat-info',
+                        'isodate': dayjs(this.model.get('time')).toISOString()
+                    }))
+                );
+                return this.replaceElement(msg);
+            },
+
             renderErrorMessage () {
                 const msg = u.stringToElement(
                     tpl_info(Object.assign(this.model.toJSON(), {

+ 12 - 0
src/converse-minimize.js

@@ -197,6 +197,12 @@ converse.plugins.add('converse-minimize', {
 
 
         const minimizableChatBoxView = {
+
+            /**
+             * Maximizes a minimized chat box.
+             * Will trigger {@link _converse#chatBoxMaximized}
+             * @returns {_converse.ChatBoxView|_converse.ChatRoomView}
+             */
             maximize () {
                 // Restores a minimized chat box
                 const { _converse } = this.__super__;
@@ -216,6 +222,11 @@ converse.plugins.add('converse-minimize', {
                 return this;
             },
 
+            /**
+             * Minimizes a chat box.
+             * Will trigger {@link _converse#chatBoxMinimized}
+             * @returns {_converse.ChatBoxView|_converse.ChatRoomView}
+             */
             minimize (ev) {
                 const { _converse } = this.__super__;
                 if (ev && ev.preventDefault) { ev.preventDefault(); }
@@ -234,6 +245,7 @@ converse.plugins.add('converse-minimize', {
                  * @example _converse.api.listen.on('chatBoxMinimized', view => { ... });
                  */
                 _converse.api.trigger('chatBoxMinimized', this);
+                return this;
             },
 
             onMinimizedChanged (item) {

+ 45 - 312
src/converse-muc-views.js

@@ -139,87 +139,6 @@ converse.plugins.add('converse-muc-views', {
             Object.assign(_converse.ControlBoxView.prototype, { renderRoomsPanel });
         }
 
-
-        function ___ (str) {
-            /* This is part of a hack to get gettext to scan strings to be
-            * translated. Strings we cannot send to the function above because
-            * they require variable interpolation and we don't yet have the
-            * variables at scan time.
-            *
-            * See actionInfoMessages further below.
-            */
-            return str;
-        }
-
-        /* https://xmpp.org/extensions/xep-0045.html
-         * ----------------------------------------
-         * 100 message      Entering a groupchat         Inform user that any occupant is allowed to see the user's full JID
-         * 101 message (out of band)                     Affiliation change  Inform user that his or her affiliation changed while not in the groupchat
-         * 102 message      Configuration change         Inform occupants that groupchat now shows unavailable members
-         * 103 message      Configuration change         Inform occupants that groupchat now does not show unavailable members
-         * 104 message      Configuration change         Inform occupants that a non-privacy-related groupchat configuration change has occurred
-         * 110 presence     Any groupchat presence       Inform user that presence refers to one of its own groupchat occupants
-         * 170 message or initial presence               Configuration change    Inform occupants that groupchat logging is now enabled
-         * 171 message      Configuration change         Inform occupants that groupchat logging is now disabled
-         * 172 message      Configuration change         Inform occupants that the groupchat is now non-anonymous
-         * 173 message      Configuration change         Inform occupants that the groupchat is now semi-anonymous
-         * 174 message      Configuration change         Inform occupants that the groupchat is now fully-anonymous
-         * 201 presence     Entering a groupchat         Inform user that a new groupchat has been created
-         * 210 presence     Entering a groupchat         Inform user that the service has assigned or modified the occupant's roomnick
-         * 301 presence     Removal from groupchat       Inform user that he or she has been banned from the groupchat
-         * 303 presence     Exiting a groupchat          Inform all occupants of new groupchat nickname
-         * 307 presence     Removal from groupchat       Inform user that he or she has been kicked from the groupchat
-         * 321 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because of an affiliation change
-         * 322 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member
-         * 332 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because of a system shutdown
-         */
-        _converse.muc = {
-            info_messages: {
-                100: __('This groupchat is not anonymous'),
-                102: __('This groupchat now shows unavailable members'),
-                103: __('This groupchat does not show unavailable members'),
-                104: __('The groupchat configuration has changed'),
-                170: __('groupchat logging is now enabled'),
-                171: __('groupchat logging is now disabled'),
-                172: __('This groupchat is now no longer anonymous'),
-                173: __('This groupchat is now semi-anonymous'),
-                174: __('This groupchat is now fully-anonymous'),
-                201: __('A new groupchat has been created')
-            },
-
-            disconnect_messages: {
-                301: __('You have been banned from this groupchat'),
-                307: __('You have been kicked from this groupchat'),
-                321: __("You have been removed from this groupchat because of an affiliation change"),
-                322: __("You have been removed from this groupchat because the groupchat has changed to members-only and you're not a member"),
-                332: __("You have been removed from this groupchat because the service hosting it is being shut down")
-            },
-
-            action_info_messages: {
-                /* XXX: Note the triple underscore function and not double
-                * underscore.
-                *
-                * This is a hack. We can't pass the strings to __ because we
-                * don't yet know what the variable to interpolate is.
-                *
-                * Triple underscore will just return the string again, but we
-                * can then at least tell gettext to scan for it so that these
-                * strings are picked up by the translation machinery.
-                */
-                301: ___("%1$s has been banned"),
-                303: ___("%1$s's nickname has changed"),
-                307: ___("%1$s has been kicked out"),
-                321: ___("%1$s has been removed because of an affiliation change"),
-                322: ___("%1$s has been removed for not being a member")
-            },
-
-            new_nickname_messages: {
-                210: ___('Your nickname has been automatically set to %1$s'),
-                303: ___('Your nickname has been changed to %1$s')
-            }
-        };
-
-
         /* Insert groupchat info (based on returned #disco IQ stanza)
          * @function insertRoomInfo
          * @param { HTMLElement } el - The HTML DOM element that contains the info.
@@ -581,7 +500,6 @@ converse.plugins.add('converse-muc-views', {
                 this.render();
                 this.updateAfterMessagesFetched();
                 this.createOccupantsView();
-                this.registerHandlers();
                 this.onConnectionStatusChanged();
                 /**
                  * Triggered once a groupchat has been opened
@@ -779,6 +697,8 @@ converse.plugins.add('converse-muc-views', {
                 const conn_status = this.model.get('connection_status');
                 if (conn_status === converse.ROOMSTATUS.NICKNAME_REQUIRED) {
                     this.renderNicknameForm();
+                } else if (conn_status === converse.ROOMSTATUS.PASSWORD_REQUIRED) {
+                    this.renderPasswordForm();
                 } else if (conn_status === converse.ROOMSTATUS.CONNECTING) {
                     this.showSpinner();
                 } else if (conn_status === converse.ROOMSTATUS.ENTERED) {
@@ -786,6 +706,10 @@ converse.plugins.add('converse-muc-views', {
                     this.setChatState(_converse.ACTIVE);
                     this.scrollDown();
                     this.focus();
+                } else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) {
+                    this.showDisconnectMessage();
+                } else if (conn_status === converse.ROOMSTATUS.DESTROYED) {
+                    this.showDestroyedMessage();
                 }
             },
 
@@ -1106,7 +1030,8 @@ converse.plugins.add('converse-muc-views', {
                         if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) {
                             break;
                         } else if (args.length === 0) {
-                            this.showErrorMessage(__('You need to provide a nickname'))
+                            // e.g. Your nickname is "coolguy69"
+                            this.showErrorMessage(__('Your nickname is "%1$s"', this.model.get('nick')))
                         } else {
                             const jid = Strophe.getBareJidFromJid(this.model.get('jid'));
                             _converse.api.send($pres({
@@ -1152,42 +1077,6 @@ converse.plugins.add('converse-muc-views', {
                 return true;
             },
 
-            registerHandlers () {
-                /* Register presence and message handlers for this chat
-                 * groupchat
-                 */
-                // XXX: Ideally this can be refactored out so that we don't
-                // need to do stanza processing inside the views in this
-                // module. See the comment in "onPresence" for more info.
-                this.model.addHandler('presence', 'ChatRoomView.onPresence', this.onPresence.bind(this));
-                // XXX instead of having a method showStatusMessages, we could instead
-                // create message models in converse-muc.js and then give them views in this module.
-                this.model.addHandler('message', 'ChatRoomView.showStatusMessages', this.showStatusMessages.bind(this));
-            },
-
-            /**
-             * Handles all MUC presence stanzas.
-             * @private
-             * @method _converse.ChatRoomView#onPresence
-             * @param { XMLElement } pres - The stanza
-             */
-            onPresence (pres) {
-                // XXX: Current thinking is that excessive stanza
-                // processing inside a view is a "code smell".
-                // Instead stanza processing should happen inside the
-                // models/collections.
-                if (pres.getAttribute('type') === 'error') {
-                    this.showErrorMessageFromPresence(pres);
-                } else {
-                    // Instead of doing it this way, we could perhaps rather
-                    // create StatusMessage objects inside the messages
-                    // Collection and then simply render those. Then stanza
-                    // processing is done on the model and rendering in the
-                    // view(s).
-                    this.showStatusMessages(pres);
-                }
-            },
-
             /**
              * Renders a form given an IQ stanza containing the current
              * groupchat configuration.
@@ -1246,32 +1135,6 @@ converse.plugins.add('converse-muc-views', {
                 }
             },
 
-            onNicknameClash (presence) {
-                /* When the nickname is already taken, we either render a
-                 * form for the user to choose a new nickname, or we
-                 * try to make the nickname unique by adding an integer to
-                 * it. So john will become john-2, and then john-3 and so on.
-                 *
-                 * Which option is take depends on the value of
-                 * muc_nickname_from_jid.
-                 */
-                if (_converse.muc_nickname_from_jid) {
-                    const nick = presence.getAttribute('from').split('/')[1];
-                    if (nick === _converse.getDefaultMUCNickname()) {
-                        this.model.join(nick + '-2');
-                    } else {
-                        const del= nick.lastIndexOf("-");
-                        const num = nick.substring(del+1, nick.length);
-                        this.model.join(nick.substring(0, del+1) + String(Number(num)+1));
-                    }
-                } else {
-                    this.renderNicknameForm(
-                        __("The nickname you chose is reserved or "+
-                           "currently in use, please choose a different one.")
-                    );
-                }
-            },
-
             hideChatRoomContents () {
                 const container_el = this.el.querySelector('.chatroom-body');
                 if (!_.isNull(container_el)) {
@@ -1279,9 +1142,11 @@ converse.plugins.add('converse-muc-views', {
                 }
             },
 
-            renderNicknameForm (message='') {
+            renderNicknameForm () {
                 /* Render a form which allows the user to choose theirnickname.
                  */
+                const message = this.model.get('nickname_validation_message');
+                this.model.save('nickname_validation_message', undefined);
                 this.hideChatRoomContents();
                 if (!this.nickname_form) {
                     this.nickname_form = new _converse.MUCNicknameForm({
@@ -1297,8 +1162,11 @@ converse.plugins.add('converse-muc-views', {
                 u.safeSave(this.model, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
             },
 
-            renderPasswordForm (message='') {
+            renderPasswordForm () {
                 this.hideChatRoomContents();
+                const message = this.model.get('password_validation_message');
+                this.model.save('password_validation_message', undefined);
+
                 if (!this.password_form) {
                     this.password_form = new _converse.MUCPasswordForm({
                         'model': new Backbone.Model(),
@@ -1308,33 +1176,32 @@ converse.plugins.add('converse-muc-views', {
                     const container_el = this.el.querySelector('.chatroom-body');
                     container_el.insertAdjacentElement('beforeend', this.password_form.el);
                 } else {
-                    this.model.set('validation_message', message);
+                    this.password_form.model.set('validation_message', message);
                 }
                 u.showElement(this.password_form.el);
                 this.model.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED);
             },
 
-            showDestroyedMessage (error) {
+            showDestroyedMessage () {
                 u.hideElement(this.el.querySelector('.chat-area'));
                 u.hideElement(this.el.querySelector('.occupants'));
                 sizzle('.spinner', this.el).forEach(u.removeElement);
 
+                const message = this.model.get('destroyed_message');
+                const reason = this.model.get('destroyed_reason');
+                const moved_jid = this.model.get('moved_jid');
+                this.model.save({
+                    'destroyed_message': undefined,
+                    'destroyed_reason': undefined,
+                    'moved_jid': undefined
+                });
                 const container = this.el.querySelector('.disconnect-container');
-                const moved_jid = _.get(
-                        sizzle('gone[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', error).pop(),
-                        'textContent'
-                    ).replace(/^xmpp:/, '').replace(/\?join$/, '');
-                const reason = _.get(
-                        sizzle('text[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', error).pop(),
-                        'textContent'
-                    );
                 container.innerHTML = tpl_chatroom_destroyed({
                     '_': _,
                     '__':__,
                     'jid': moved_jid,
                     'reason': reason ? `"${reason}"` : null
                 });
-
                 const switch_el = container.querySelector('a.switch-chat');
                 if (switch_el) {
                     switch_el.addEventListener('click', ev => {
@@ -1348,51 +1215,37 @@ converse.plugins.add('converse-muc-views', {
                 u.showElement(container);
             },
 
-            showDisconnectMessages (msgs) {
-                if (_.isString(msgs)) {
-                    msgs = [msgs];
+            showDisconnectMessage () {
+                const message = this.model.get('disconnection_message');
+                if (!message) {
+                    return;
                 }
                 u.hideElement(this.el.querySelector('.chat-area'));
                 u.hideElement(this.el.querySelector('.occupants'));
                 sizzle('.spinner', this.el).forEach(u.removeElement);
+
+                const messages = [message];
+                const actor = this.model.get('disconnection_actor');
+                if (actor) {
+                    messages.push(__('This action was done by %1$s.', actor));
+                }
+                const reason = this.model.get('disconnection_reason');
+                if (reason) {
+                    messages.push(__('The reason given is: "%1$s".', reason));
+                }
+                this.model.save({
+                    'disconnection_message': undefined,
+                    'disconnection_reason': undefined,
+                    'disconnection_actor': undefined
+                });
                 const container = this.el.querySelector('.disconnect-container');
                 container.innerHTML = tpl_chatroom_disconnect({
                     '_': _,
-                    'disconnect_messages': msgs
+                    'disconnect_messages': messages
                 })
                 u.showElement(container);
             },
 
-            /**
-             * @private
-             * @method _converse.ChatRoomView#getMessageFromStatus
-             * @param { XMLElement } stat: A <status> element
-             * @param { Boolean } is_self: Whether the element refers to the current user
-             * @param { XMLElement } stanza: The original stanza received
-             */
-            getMessageFromStatus (stat, stanza, is_self) {
-                const code = stat.getAttribute('code');
-                if (code === '110' || (code === '100' && !is_self)) { return; }
-                if (code in _converse.muc.info_messages) {
-                    return _converse.muc.info_messages[code];
-                }
-                let nick;
-                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;
-            },
-
             getNotificationWithMessage (message) {
                 let el = this.content.lastElementChild;
                 while (!_.isNil(el)) {
@@ -1407,49 +1260,6 @@ converse.plugins.add('converse-muc-views', {
                 }
             },
 
-            parseXUserElement (x, stanza, is_self) {
-                /* Parse the passed-in <x xmlns='http://jabber.org/protocol/muc#user'>
-                 * element and construct a map containing relevant
-                 * information.
-                 */
-                // 1. Get notification messages based on the <status> elements.
-                const statuses = x.querySelectorAll('status');
-                const mapper = _.partial(this.getMessageFromStatus, _, stanza, is_self);
-                const notification = {};
-                const messages = _.reject(
-                    _.reject(_.map(statuses, mapper), _.isUndefined),
-                    message => this.getNotificationWithMessage(message)
-                );
-                if (messages.length) {
-                    notification.messages = messages;
-                }
-                // 2. Get disconnection messages based on the <status> elements
-                const codes = _.invokeMap(statuses, Element.prototype.getAttribute, 'code');
-                const disconnection_codes = _.intersection(codes, Object.keys(_converse.muc.disconnect_messages));
-                const disconnected = is_self && disconnection_codes.length > 0;
-                if (disconnected) {
-                    notification.disconnected = true;
-                    notification.disconnection_message = _converse.muc.disconnect_messages[disconnection_codes[0]];
-                }
-                // 3. Find the reason and actor from the <item> element
-                const item = x.querySelector('item');
-                // By using querySelector above, we assume here there is
-                // one <item> per <x xmlns='http://jabber.org/protocol/muc#user'>
-                // element. This appears to be a safe assumption, since
-                // each <x/> element pertains to a single user.
-                if (!_.isNull(item)) {
-                    const reason = item.querySelector('reason');
-                    if (reason) {
-                        notification.reason = reason ? reason.textContent : undefined;
-                    }
-                    const actor = item.querySelector('actor');
-                    if (actor) {
-                        notification.actor = actor ? actor.getAttribute('nick') : undefined;
-                    }
-                }
-                return notification;
-            },
-
             insertNotification (message) {
                 this.content.insertAdjacentHTML(
                     'beforeend',
@@ -1461,33 +1271,6 @@ converse.plugins.add('converse-muc-views', {
                 );
             },
 
-            showNotificationsforUser (notification) {
-                /* Given the notification object generated by
-                 * parseXUserElement, display any relevant messages and
-                 * information to the user.
-                 */
-                if (notification.disconnected) {
-                    const messages = [];
-                    messages.push(notification.disconnection_message);
-                    if (notification.actor) {
-                        messages.push(__('This action was done by %1$s.', notification.actor));
-                    }
-                    if (notification.reason) {
-                        messages.push(__('The reason given is: "%1$s".', notification.reason));
-                    }
-                    this.showDisconnectMessages(messages);
-                    this.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
-                    return;
-                }
-                if (_.get(notification.messages, 'length')) {
-                    notification.messages.forEach(message => this.insertNotification(message));
-                    this.scrollDown();
-                }
-                if (notification.reason) {
-                    this.showChatEvent(__('The reason given is: "%1$s".', notification.reason));
-                }
-            },
-
             onOccupantAdded (occupant) {
                 if (occupant.get('show') === 'online') {
                     this.showJoinNotification(occupant);
@@ -1644,56 +1427,6 @@ converse.plugins.add('converse-muc-views', {
                 this.scrollDown();
             },
 
-            /**
-             * Check for status codes and communicate their purpose to the user.
-             * See: https://xmpp.org/registrar/mucstatus.html
-             * @private
-             * @method _converse.ChatRoomView#showStatusMessages
-             * @param { XMLElement } stanza - The message or presence stanza containing the status codes
-             */
-            showStatusMessages (stanza) {
-                const elements = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza);
-                const is_self = stanza.querySelectorAll("status[code='110']").length;
-                const iteratee = _.partial(this.parseXUserElement.bind(this), _, stanza, is_self);
-                const notifications = _.reject(_.map(elements, iteratee), _.isEmpty);
-                notifications.forEach(n => this.showNotificationsforUser(n));
-            },
-
-            showErrorMessageFromPresence (presence) {
-                // We didn't enter the groupchat, so we must remove it from the MUC add-on
-                const error = presence.querySelector('error');
-                if (error.getAttribute('type') === 'auth') {
-                    if (!_.isNull(error.querySelector('not-authorized'))) {
-                        this.renderPasswordForm(__("Password incorrect"));
-                    } else if (!_.isNull(error.querySelector('registration-required'))) {
-                        this.showDisconnectMessages(__('You are not on the member list of this groupchat.'));
-                    } else if (!_.isNull(error.querySelector('forbidden'))) {
-                        this.showDisconnectMessages(__('You have been banned from this groupchat.'));
-                    }
-                } else if (error.getAttribute('type') === 'cancel') {
-                    if (!_.isNull(error.querySelector('not-allowed'))) {
-                        this.showDisconnectMessages(__('You are not allowed to create new groupchats.'));
-                    } else if (!_.isNull(error.querySelector('not-acceptable'))) {
-                        this.showDisconnectMessages(__("Your nickname doesn't conform to this groupchat's policies."));
-                    } else if (sizzle('gone[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', error).length) {
-                        this.showDestroyedMessage(error);
-                    } else if (!_.isNull(error.querySelector('conflict'))) {
-                        this.onNicknameClash(presence);
-                    } else if (!_.isNull(error.querySelector('item-not-found'))) {
-                        this.showDisconnectMessages(__("This groupchat does not (yet) exist."));
-                    } else if (!_.isNull(error.querySelector('service-unavailable'))) {
-                        this.showDisconnectMessages(__("This groupchat has reached its maximum number of participants."));
-                    } else if (!_.isNull(error.querySelector('remote-server-not-found'))) {
-                        const messages = [__("Remote server not found")];
-                        const reason = _.get(error.querySelector('text'), 'textContent');
-                        if (reason) {
-                            messages.push(__('The explanation given is: "%1$s".', reason));
-                        }
-                        this.showDisconnectMessages(messages);
-                    }
-                }
-            },
-
             renderAfterTransition () {
                 /* Rerender the groupchat after some kind of transition. For
                  * example after the spinner has been removed or after a

+ 236 - 33
src/headless/converse-muc.js

@@ -61,7 +61,8 @@ converse.ROOMSTATUS = {
     NICKNAME_REQUIRED: 2,
     PASSWORD_REQUIRED: 3,
     DISCONNECTED: 4,
-    ENTERED: 5
+    ENTERED: 5,
+    DESTROYED: 6
 };
 
 
@@ -124,6 +125,76 @@ converse.plugins.add('converse-muc', {
         }
 
 
+        function ___ (str) {
+            /* This is part of a hack to get gettext to scan strings to be
+            * translated. Strings we cannot send to the function above because
+            * they require variable interpolation and we don't yet have the
+            * variables at scan time.
+            */
+            return str;
+        }
+
+        /* https://xmpp.org/extensions/xep-0045.html
+         * ----------------------------------------
+         * 100 message      Entering a groupchat         Inform user that any occupant is allowed to see the user's full JID
+         * 101 message (out of band)                     Affiliation change  Inform user that his or her affiliation changed while not in the groupchat
+         * 102 message      Configuration change         Inform occupants that groupchat now shows unavailable members
+         * 103 message      Configuration change         Inform occupants that groupchat now does not show unavailable members
+         * 104 message      Configuration change         Inform occupants that a non-privacy-related groupchat configuration change has occurred
+         * 110 presence     Any groupchat presence       Inform user that presence refers to one of its own groupchat occupants
+         * 170 message or initial presence               Configuration change    Inform occupants that groupchat logging is now enabled
+         * 171 message      Configuration change         Inform occupants that groupchat logging is now disabled
+         * 172 message      Configuration change         Inform occupants that the groupchat is now non-anonymous
+         * 173 message      Configuration change         Inform occupants that the groupchat is now semi-anonymous
+         * 174 message      Configuration change         Inform occupants that the groupchat is now fully-anonymous
+         * 201 presence     Entering a groupchat         Inform user that a new groupchat has been created
+         * 210 presence     Entering a groupchat         Inform user that the service has assigned or modified the occupant's roomnick
+         * 301 presence     Removal from groupchat       Inform user that he or she has been banned from the groupchat
+         * 303 presence     Exiting a groupchat          Inform all occupants of new groupchat nickname
+         * 307 presence     Removal from groupchat       Inform user that he or she has been kicked from the groupchat
+         * 321 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because of an affiliation change
+         * 322 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member
+         * 332 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because of a system shutdown
+         */
+        _converse.muc = {
+            info_messages: {
+                100: __('This groupchat is not anonymous'),
+                102: __('This groupchat now shows unavailable members'),
+                103: __('This groupchat does not show unavailable members'),
+                104: __('The groupchat configuration has changed'),
+                170: __('groupchat logging is now enabled'),
+                171: __('groupchat logging is now disabled'),
+                172: __('This groupchat is now no longer anonymous'),
+                173: __('This groupchat is now semi-anonymous'),
+                174: __('This groupchat is now fully-anonymous'),
+                201: __('A new groupchat has been created')
+            },
+
+            new_nickname_messages: {
+                // XXX: Note the triple underscore function and not double underscore.
+                210: ___('Your nickname has been automatically set to %1$s'),
+                303: ___('Your nickname has been changed to %1$s')
+            },
+
+            disconnect_messages: {
+                301: __('You have been banned from this groupchat'),
+                307: __('You have been kicked from this groupchat'),
+                321: __("You have been removed from this groupchat because of an affiliation change"),
+                322: __("You have been removed from this groupchat because the groupchat has changed to members-only and you're not a member"),
+                332: __("You have been removed from this groupchat because the service hosting it is being shut down")
+            },
+
+            action_info_messages: {
+                // XXX: Note the triple underscore function and not double underscore.
+                301: ___("%1$s has been banned"),
+                303: ___("%1$s's nickname has changed"),
+                307: ___("%1$s has been kicked out"),
+                321: ___("%1$s has been removed because of an affiliation change"),
+                322: ___("%1$s has been removed for not being a member")
+            }
+        }
+
+
         async function openRoom (jid) {
             if (!u.isValidMUCJID(jid)) {
                 return _converse.log(
@@ -300,7 +371,6 @@ converse.plugins.add('converse-muc', {
                 const room_jid = this.get('jid');
                 this.removeHandlers();
                 this.presence_handler = _converse.connection.addHandler(stanza => {
-                        Object.values(this.handlers.presence).forEach(callback => callback(stanza));
                         this.onPresence(stanza);
                         return true;
                     },
@@ -308,7 +378,6 @@ converse.plugins.add('converse-muc', {
                     {'ignoreNamespaceFragment': true, 'matchBareFromJid': true}
                 );
                 this.message_handler = _converse.connection.addHandler(stanza => {
-                        Object.values(this.handlers.message).forEach(callback => callback(stanza));
                         this.onMessage(stanza);
                         return true;
                     }, null, 'message', 'groupchat', null, room_jid,
@@ -331,21 +400,6 @@ converse.plugins.add('converse-muc', {
                 return this;
             },
 
-            addHandler (type, name, callback) {
-                /* Allows 'presence' and 'message' handlers to be
-                 * registered. These will be executed once presence or
-                 * message stanzas are received, and *before* this model's
-                 * own handlers are executed.
-                 */
-                if (_.isNil(this.handlers)) {
-                    this.handlers = {};
-                }
-                if (_.isNil(this.handlers[type])) {
-                    this.handlers[type] = {};
-                }
-                this.handlers[type][name] = callback;
-            },
-
             getDisplayName () {
                 const name = this.get('name');
                 if (name) {
@@ -1013,9 +1067,9 @@ converse.plugins.add('converse-muc', {
                         }).c('query', {'xmlns': Strophe.NS.MUC_REGISTER})
                     );
                 } catch (e) {
-                    if (sizzle('not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) {
+                    if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
                         err_msg = __("You're not allowed to register yourself in this groupchat.");
-                    } else if (sizzle('registration-required[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) {
+                    } else if (sizzle(`registration-required[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
                         err_msg = __("You're not allowed to register in this groupchat because it's members-only.");
                     }
                     _converse.log(e, Strophe.LogLevel.ERROR);
@@ -1036,9 +1090,9 @@ converse.plugins.add('converse-muc', {
                                 .c('field', {'var': 'muc#register_roomnick'}).c('value').t(nick)
                     );
                 } catch (e) {
-                    if (sizzle('service-unavailable[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) {
+                    if (sizzle(`service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
                         err_msg = __("Can't register your nickname in this groupchat, it doesn't support registration.");
-                    } else if (sizzle('bad-request[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) {
+                    } else if (sizzle(`bad-request[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
                         err_msg = __("Can't register your nickname in this groupchat, invalid data form supplied.");
                     }
                     _converse.log(err_msg);
@@ -1320,6 +1374,7 @@ converse.plugins.add('converse-muc', {
              * @param { XMLElement } stanza - The message stanza.
              */
             async onMessage (stanza) {
+                this.createInfoMessages(stanza);
                 this.fetchFeaturesIfConfigurationChanged(stanza);
                 const original_stanza = stanza;
                 const forwarded = sizzle(`forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).pop();
@@ -1360,21 +1415,168 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
+            handleDisconnection (stanza) {
+                const is_self = !_.isNull(stanza.querySelector("status[code='110']"));
+                const x = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza).pop();
+                if (!x) {
+                    return;
+                }
+                const codes = sizzle('status', x).map(s => s.getAttribute('code'));
+                const disconnection_codes = _.intersection(codes, Object.keys(_converse.muc.disconnect_messages));
+                const disconnected = is_self && disconnection_codes.length > 0;
+                if (!disconnected) {
+                    return;
+                }
+                // By using querySelector we assume here there is
+                // one <item> per <x xmlns='http://jabber.org/protocol/muc#user'>
+                // element. This appears to be a safe assumption, since
+                // each <x/> element pertains to a single user.
+                const item = x.querySelector('item');
+                const reason = item ? _.get(item.querySelector('reason'), 'textContent') : undefined;
+                const actor = item ? _.invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
+                const message = _converse.muc.disconnect_messages[disconnection_codes[0]];
+                this.setDisconnectionMessage(message, reason, actor);
+            },
+
+
+            /**
+             * Create info messages based on a received presence stanza
+             * @private
+             * @method _converse.ChatRoom#createInfoMessages
+             * @param { XMLElement } stanza: The presence stanza received
+             */
+            createInfoMessages (stanza) {
+                const is_self = !_.isNull(stanza.querySelector("status[code='110']"));
+                const x = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza).pop();
+                if (!x) {
+                    return;
+                }
+                const codes = sizzle('status', x).map(s => s.getAttribute('code'));
+
+                codes.forEach(code => {
+                    let message;
+                    if (code === '110' || (code === '100' && !is_self)) {
+                        return;
+                    } else if (code in _converse.muc.info_messages) {
+                        message = _converse.muc.info_messages[code];
+
+                    } else if (!is_self && (code in _converse.muc.action_info_messages)) {
+                        const nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
+                        message = __(_converse.muc.action_info_messages[code], nick);
+                        const item = x.querySelector('item');
+                        const reason = item ? _.get(item.querySelector('reason'), 'textContent') : undefined;
+                        const actor = item ? _.invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
+                        if (actor) {
+                            message += '\n' + __('This action was done by %1$s.', actor);
+                        }
+                        if (reason) {
+                            message += '\n' + __('The reason given is: "%1$s".', reason);
+                        }
+                    } else if (is_self && (code in _converse.muc.new_nickname_messages)) {
+                        let nick;
+                        if (is_self && code === "210") {
+                            nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
+                        } else if (is_self && code === "303") {
+                            nick = stanza.querySelector('x item').getAttribute('nick');
+                        }
+                        this.save('nick', nick);
+                        message = __(_converse.muc.new_nickname_messages[code], nick);
+                    }
+
+                    if (message) {
+                        this.messages.create({'type': 'info', message});
+                    }
+                });
+            },
+
 
-            onErrorPresence (pres) {
-                // TODO: currently showErrorMessageFromPresence handles
-                // 'error" presences in converse-muc-views.
-                // Instead, they should be handled here and the presence
-                // handler removed from there.
-                if (sizzle(`error not-authorized[xmlns="${Strophe.NS.STANZAS}"]`, pres).length) {
-                    this.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED);
-                } else if (sizzle(`error[type="modify"]`, pres).length) {
-                    this.handleModifyError(pres);
+            setDisconnectionMessage (message, reason, actor) {
+                this.save({
+                    'connection_status': converse.ROOMSTATUS.DISCONNECTED,
+                    'disconnection_message': message,
+                    'disconnection_reason': reason,
+                    'disconnection_actor': actor
+                });
+            },
+
+
+            onNicknameClash (presence) {
+                if (_converse.muc_nickname_from_jid) {
+                    const nick = presence.getAttribute('from').split('/')[1];
+                    if (nick === _converse.getDefaultMUCNickname()) {
+                        this.join(nick + '-2');
+                    } else {
+                        const del= nick.lastIndexOf("-");
+                        const num = nick.substring(del+1, nick.length);
+                        this.join(nick.substring(0, del+1) + String(Number(num)+1));
+                    }
                 } else {
-                    this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
+                    this.save({
+                        'nickname_validation_message': __(
+                            "The nickname you chose is reserved or "+
+                            "currently in use, please choose a different one."),
+                        'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED
+                    });
                 }
             },
 
+
+            /**
+             * Parses a <presence> stanza with type "error" and sets the proper
+             * `connection_status` value for this {@link _converse.ChatRoom} as
+             * well as any additional output that can be shown to the user.
+             * @private
+             * @param { XMLElement } stanza - The presence stanza
+             */
+            onErrorPresence (stanza) {
+                if (sizzle(`error not-authorized[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
+                    this.save({
+                        'password_validation_message': __("Password incorrect"),
+                        'connection_status': converse.ROOMSTATUS.PASSWORD_REQUIRED
+                    });
+                }
+                const error = stanza.querySelector('error');
+                const error_type = error.getAttribute('type');
+
+                if (error_type === 'modify') {
+                    this.handleModifyError(stanza);
+                } else if (error_type === 'auth') {
+                    if (error.querySelector('registration-required')) {
+                        this.setDisconnectionMessage(__('You are not on the member list of this groupchat.'));
+                    } else if (error.querySelector('forbidden')) {
+                        this.setDisconnectionMessage(__('You have been banned from this groupchat.'));
+                    }
+                } else if (error_type === 'cancel') {
+                    if (error.querySelector('not-allowed')) {
+                        this.setDisconnectionMessage(__('You are not allowed to create new groupchats.'));
+                    } else if (error.querySelector('not-acceptable')) {
+                        this.setDisconnectionMessage(__("Your nickname doesn't conform to this groupchat's policies."));
+                    } else if (sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).length) {
+                        const moved_jid = _.get(sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent')
+                            .replace(/^xmpp:/, '')
+                            .replace(/\?join$/, '');
+                        const reason = _.get(sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent');
+                        this.save({
+                            'connection_status': converse.ROOMSTATUS.DESTROYED,
+                            'destroyed_reason': reason,
+                            'moved_jid': moved_jid
+                        });
+                    } else if (error.querySelector('conflict')) {
+                        this.onNicknameClash(stanza);
+                    } else if (error.querySelector('item-not-found')) {
+                        this.setDisconnectionMessage(__("This groupchat does not (yet) exist."));
+                    } else if (error.querySelector('service-unavailable')) {
+                        this.setDisconnectionMessage(__("This groupchat has reached its maximum number of participants."));
+                    } else if (error.querySelector('remote-server-not-found')) {
+                        const message = __("Remote server not found");
+                        const text = _.get(error.querySelector('text'), 'textContent');
+                        const reason = text ? __('The explanation given is: "%1$s".', text) : undefined;
+                        this.setDisconnectionMessage(message, reason);
+                    }
+                }
+            },
+
+
             /**
              * Handles all MUC presence stanzas.
              * @private
@@ -1388,6 +1590,7 @@ converse.plugins.add('converse-muc', {
                 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) {
@@ -1414,7 +1617,7 @@ converse.plugins.add('converse-muc', {
                 this.saveAffiliationAndRole(stanza);
 
                 if (stanza.getAttribute('type') === 'unavailable') {
-                    this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
+                    this.handleDisconnection(stanza);
                 } else {
                     const locked_room = stanza.querySelector("status[code='201']");
                     if (locked_room) {

+ 4 - 2
tests/utils.js

@@ -114,8 +114,10 @@
         return _converse.chatboxviews.get(jid);
     };
 
-    utils.openChatRoom = function (_converse, room, server) {
-        return _converse.api.rooms.open(`${room}@${server}`);
+    utils.openChatRoom = async function (_converse, room, server) {
+        const model = await _converse.api.rooms.open(`${room}@${server}`);
+        await model.messages.fetched;
+        return model;
     };
 
     utils.getRoomFeatures = async function (_converse, room, server, features=[]) {