Преглед на файлове

muc: handle join/leave notifications similarly to CSNs

JC Brand преди 5 години
родител
ревизия
46a12cb660
променени са 5 файла, в които са добавени 149 реда и са изтрити 242 реда
  1. 59 79
      spec/muc.js
  2. 15 7
      spec/muc_messages.js
  3. 24 144
      src/converse-muc-views.js
  4. 2 2
      src/headless/converse-chat.js
  5. 49 10
      src/headless/converse-muc.js

+ 59 - 79
spec/muc.js

@@ -329,11 +329,14 @@
 
                 await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED);
                 await test_utils.returnMemberLists(_converse, muc_jid);
-                await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
+                const num_info_msgs = await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length);
+                expect(num_info_msgs).toBe(1);
 
                 const info_texts = Array.from(view.el.querySelectorAll('.chat-content .chat-info')).map(e => e.textContent.trim());
                 expect(info_texts[0]).toBe('A new groupchat has been created');
-                expect(info_texts[1]).toBe('nicky has entered the groupchat');
+
+                const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
+                expect(csntext.trim()).toEqual("nicky has entered the groupchat");
 
                 // An instant room is created by saving the default configuratoin.
                 //
@@ -589,11 +592,12 @@
                     .c('status', {code: '100'});
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
 
-                await u.waitUntil(() => view.content.querySelectorAll('.chat-info').length === 2);
-                expect(sizzle('div.chat-info:first', view.content).pop().textContent.trim())
-                    .toBe("This groupchat is not anonymous");
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim())
-                    .toBe("some1 has entered the groupchat");
+                const num_info_msgs = await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length);
+                expect(num_info_msgs).toBe(1);
+                expect(sizzle('div.chat-info', view.content).pop().textContent.trim()).toBe("This groupchat is not anonymous");
+
+                const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
+                expect(csntext.trim()).toEqual("some1 has entered the groupchat");
                 done();
             }));
 
@@ -627,7 +631,6 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(0);
 
                 /* <presence to="romeo@montague.lit/_converse.js-29092160"
                  *           from="coven@chat.shakespeare.lit/some1">
@@ -648,8 +651,9 @@
                     }).up()
                     .c('status', {code: '110'});
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                const text = await u.waitUntil(() => sizzle('div.chat-info:first', view.content).pop()?.textContent);
-                expect(text.trim()).toBe("some1 has entered the groupchat");
+
+                const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
+                expect(csntext.trim()).toEqual("some1 has entered the groupchat");
 
                 await room_creation_promise;
                 await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
@@ -666,9 +670,8 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                await u.waitUntil(() => view.content.querySelectorAll('div.chat-info').length === 2);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim())
-                    .toBe("newguy has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1 and newguy have entered the groupchat");
 
                 const msg = $msg({
                     'from': 'coven@chat.shakespeare.lit/some1',
@@ -692,9 +695,8 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                await u.waitUntil(() => view.content.querySelectorAll('div.chat-info').length === 3);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim())
-                    .toBe("newgirl has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1, newguy and newgirl have entered the groupchat");
 
                 // Don't show duplicate join messages
                 presence = $pres({
@@ -707,7 +709,6 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(3);
 
                 /*  <presence
                  *      from='coven@chat.shakespeare.lit/thirdwitch'
@@ -734,10 +735,8 @@
                             'role': 'none'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(4);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe(
-                    'newguy has left the groupchat. '+
-                    '"Disconnected: Replaced by new connection"');
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1 and newgirl have entered the groupchat\n newguy has left the groupchat");
 
                 // When the user immediately joins again, we collapse the
                 // multiple join/leave messages.
@@ -751,10 +750,9 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(4);
-                let msg_el = sizzle('div.chat-info:last', view.content).pop();
-                expect(msg_el.textContent.trim()).toBe("newguy has left and re-entered the groupchat");
-                expect(msg_el.getAttribute('data-leavejoin')).toBe('newguy');
+
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1, newgirl and newguy have entered the groupchat");
 
                 presence = $pres({
                         to: 'romeo@montague.lit/_converse.js-29092160',
@@ -768,10 +766,8 @@
                             'role': 'none'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(4);
-                msg_el = sizzle('div.chat-info', view.content).pop();
-                expect(msg_el.textContent.trim()).toBe('newguy has left the groupchat');
-                expect(msg_el.getAttribute('data-leave')).toBe('newguy');
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1 and newgirl have entered the groupchat\n newguy has left the groupchat");
 
                 presence = $pres({
                         to: 'romeo@montague.lit/_converse.js-29092160',
@@ -784,9 +780,8 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(5);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim())
-                    .toBe("nomorenicks has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1, newgirl and nomorenicks have entered the groupchat\n newguy has left the groupchat");
 
                 presence = $pres({
                         to: 'romeo@montague.lit/_converse.js-290918392',
@@ -799,9 +794,8 @@
                         'role': 'none'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(5);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim())
-                    .toBe("nomorenicks has entered and left the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1 and newgirl have entered the groupchat\n newguy and nomorenicks have left the groupchat");
 
                 presence = $pres({
                         to: 'romeo@montague.lit/_converse.js-29092160',
@@ -814,9 +808,8 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(5);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim())
-                    .toBe("nomorenicks has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1, newgirl and nomorenicks have entered the groupchat\n newguy has left the groupchat");
 
                 // Test a member joining and leaving
                 presence = $pres({
@@ -829,7 +822,6 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(6);
 
                 /*  <presence
                  *      from='coven@chat.shakespeare.lit/thirdwitch'
@@ -856,10 +848,8 @@
                             'role': 'none'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(6);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe(
-                    'insider has entered and left the groupchat. '+
-                    '"Disconnected: Replaced by new connection"');
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1, newgirl and nomorenicks have entered the groupchat\n newguy and insider have left the groupchat");
 
                 expect(view.model.occupants.length).toBe(5);
                 expect(view.model.occupants.findWhere({'jid': 'insider@montague.lit'}).get('show')).toBe('offline');
@@ -878,8 +868,8 @@
                     });
 
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.content.querySelectorAll('div.chat-info').length).toBe(6);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe("newgirl has entered and left the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "some1 and nomorenicks have entered the groupchat\n newguy, insider and newgirl have left the groupchat");
                 expect(view.model.occupants.length).toBe(4);
                 done();
             }));
@@ -891,9 +881,7 @@
 
                 await test_utils.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'romeo')
                 const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
-
-                expect(sizzle('div.chat-info', view.content).length).toBe(1);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe("romeo has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo has entered the groupchat");
 
                 let presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/fabio">
@@ -903,8 +891,7 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(2);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe("fabio has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo and fabio have entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/Dele Olajide">
@@ -913,8 +900,7 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(3);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe("Dele Olajide has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo, fabio and Dele Olajide have entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/jcbrand">
@@ -924,10 +910,7 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                await u.waitUntil(() => sizzle('div.chat-info', view.content).length > 3);
-
-                expect(sizzle('div.chat-info', view.content).length).toBe(4);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe("jcbrand has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo, fabio and others have entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/Dele Olajide">
@@ -936,8 +919,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(4);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe("Dele Olajide has entered and left the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "romeo, fabio and jcbrand have entered the groupchat\n Dele Olajide has left the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/Dele Olajide">
@@ -946,8 +929,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(4);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe("Dele Olajide has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "romeo, fabio and others have entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/fuvuv" xml:lang="en">
@@ -958,8 +941,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(5);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe("fuvuv has entered the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "romeo, fabio and others have entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/fuvuv">
@@ -968,8 +951,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(5);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe("fuvuv has entered and left the groupchat");
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "romeo, fabio and others have entered the groupchat\n fuvuv has left the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/fabio">
@@ -979,9 +962,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(5);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe(
-                    `fabio has entered and left the groupchat. "Disconnected: Replaced by new connection"`);
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "romeo, jcbrand and Dele Olajide have entered the groupchat\n fuvuv and fabio have left the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/fabio">
@@ -992,9 +974,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(5);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe(
-                    `fabio has entered the groupchat. "Ready for a new day"`);
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "romeo, jcbrand and others have entered the groupchat\n fuvuv has left the groupchat");
 
                 // XXX: hack so that we can test leave/enter of occupants
                 // who were already in the room when we joined.
@@ -1008,9 +989,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(1);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe(
-                    `fabio has left the groupchat. "Disconnected: closed"`);
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "romeo, jcbrand and Dele Olajide have entered the groupchat\n fuvuv and fabio have left the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/Dele Olajide">
@@ -1019,9 +999,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(2);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe(
-                    `Dele Olajide has left the groupchat`);
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "romeo and jcbrand have entered the groupchat\n fuvuv, fabio and Dele Olajide have left the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/fabio">
@@ -1031,9 +1010,10 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info', view.content).length).toBe(2);
-                expect(sizzle('div.chat-info:last', view.content).pop().textContent.trim()).toBe(
-                    `fabio has left and re-entered the groupchat`);
+                await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                    "romeo, jcbrand and fabio have entered the groupchat\n fuvuv and Dele Olajide have left the groupchat");
+
+                expect(1).toBe(1);
                 done();
             }));
 

+ 15 - 7
spec/muc_messages.js

@@ -21,8 +21,7 @@
                 const muc_jid = 'lounge@montague.lit';
                 await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
                 const view = _converse.api.chatviews.get(muc_jid);
-                await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length);
-                const presence = u.toStanza(`
+                let presence = u.toStanza(`
                     <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
                         <x xmlns="http://jabber.org/protocol/muc#user">
                             <status code="201"/>
@@ -32,6 +31,18 @@
                     </presence>
                 `);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
+                await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1);
+
+                presence = u.toStanza(`
+                    <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo1">
+                        <x xmlns="http://jabber.org/protocol/muc#user">
+                            <status code="210"/>
+                            <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
+                            <status code="110"/>
+                        </x>
+                    </presence>
+                `);
+                _converse.connection._dataRecv(test_utils.createRequest(presence));
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
 
                 const messages = view.el.querySelectorAll('.chat-info');
@@ -48,9 +59,6 @@
                 const muc_jid = 'lounge@montague.lit';
                 await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
                 const view = _converse.api.chatviews.get(muc_jid);
-                await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length);
-                expect(view.el.querySelectorAll('.chat-info').length).toBe(1);
-
                 const presence = u.toStanza(`
                     <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
                         <x xmlns="http://jabber.org/protocol/muc#user">
@@ -68,11 +76,11 @@
                 spyOn(view.model, 'createInfoMessages').and.callThrough();
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 await u.waitUntil(() => view.model.createInfoMessages.calls.count());
-                await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
+                await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1);
 
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 await u.waitUntil(() => view.model.createInfoMessages.calls.count() === 2);
-                expect(view.el.querySelectorAll('.chat-info').length).toBe(2);
+                expect(view.el.querySelectorAll('.chat-info').length).toBe(1);
                 done();
             }));
         });

+ 24 - 144
src/converse-muc-views.js

@@ -702,7 +702,7 @@ converse.plugins.add('converse-muc-views', {
                     this.removeAll();
                 });
 
-                this.listenTo(this.model.csn, 'change', this.renderChatStateNotifications);
+                this.listenTo(this.model.csn, 'change', this.renderNotifications);
                 this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
 
                 this.listenTo(this.model, 'change', this.renderHeading);
@@ -745,23 +745,26 @@ converse.plugins.add('converse-muc-views', {
                     'muc_show_logs_before_join': _converse.muc_show_logs_before_join,
                     'show_send_button': _converse.show_send_button
                 }), this.el);
-                await this.renderHeading();
-                this.renderBottomPanel();
+                this.csn = this.el.querySelector('.chat-content__notifications');
                 this.content = this.el.querySelector('.chat-content');
                 this.msgs_container = this.el.querySelector('.chat-content__messages');
-                this.csn = this.el.querySelector('.chat-content__notifications');
-                if (!_converse.muc_show_logs_before_join) {
-                    this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED && this.showSpinner();
-                }
-                if (!this.model.get('hidden')) {
-                    this.show();
+
+                this.renderBottomPanel();
+                if (!_converse.muc_show_logs_before_join &&
+                        this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
+                    this.showSpinner();
                 }
-                return this;
+                // Render header as late as possible since it's async and we
+                // want the rest of the DOM elements to be available ASAP.
+                // Otherwise e.g. this.csn is not yet defined when accessed elsewhere.
+                await this.renderHeading();
+                !this.model.get('hidden') && this.show();
             },
 
-            renderChatStateNotifications () {
+            renderNotifications () {
                 const actors_per_state = this.model.csn.toJSON();
-                const message = converse.CHAT_STATES.reduce((result, state) => {
+                const states = [...converse.CHAT_STATES, ...converse.MUC_TRAFFIC_STATES];
+                const message = states.reduce((result, state) => {
                     const existing_actors = actors_per_state[state];
                     if (!(existing_actors?.length)) {
                         return result;
@@ -774,6 +777,10 @@ converse.plugins.add('converse-muc-views', {
                             return `${result} ${__('%1$s has stopped typing', actors[0])}\n`;
                         } else if (state === _converse.GONE) {
                             return `${result} ${__('%1$s has gone away', actors[0])}\n`;
+                        } else if (state === 'entered') {
+                            return `${result} ${__('%1$s has entered the groupchat', actors[0])}\n`;
+                        } else if (state === 'exited') {
+                            return `${result} ${__('%1$s has left the groupchat', actors[0])}\n`;
                         }
                     } else if (actors.length > 1) {
                         let actors_str;
@@ -783,12 +790,17 @@ converse.plugins.add('converse-muc-views', {
                             const last_actor = actors.pop();
                             actors_str = __('%1$s and %2$s', actors.join(', '), last_actor);
                         }
+
                         if (state === 'composing') {
                             return `${result} ${__('%1$s are typing', actors_str)}\n`;
                         } else if (state === 'paused') {
                             return `${result} ${__('%1$s have stopped typing', actors_str)}\n`;
                         } else if (state === _converse.GONE) {
                             return `${result} ${__('%1$s have gone away', actors_str)}\n`;
+                        } else if (state === 'entered') {
+                            return `${result} ${__('%1$s have entered the groupchat', actors_str)}\n`;
+                        } else if (state === 'exited') {
+                            return `${result} ${__('%1$s have left the groupchat', actors_str)}\n`;
                         }
                     }
                     return result;
@@ -1905,27 +1917,6 @@ converse.plugins.add('converse-muc-views', {
                     this.renderHeading();
                     this.renderBottomPanel();
                 }
-                if (occupant.get('show') === 'online') {
-                    this.showJoinNotification(occupant);
-                }
-            },
-
-            onOccupantRemoved (occupant) {
-                if (this.model.session.get('connection_status') ===  converse.ROOMSTATUS.ENTERED &&
-                        occupant.get('show') === 'online') {
-                    this.showLeaveNotification(occupant);
-                }
-            },
-
-            showJoinOrLeaveNotification (occupant) {
-                if (occupant.get('states').includes('303')) {
-                    return;
-                }
-                if (occupant.get('show') === 'offline') {
-                    this.showLeaveNotification(occupant);
-                } else if (occupant.get('show') === 'online') {
-                    this.showJoinNotification(occupant);
-                }
             },
 
             /**
@@ -1959,117 +1950,6 @@ converse.plugins.add('converse-muc-views', {
                 }
             },
 
-            async showJoinNotification (occupant) {
-                if (!_converse.muc_show_join_leave ||
-                        this.model.session.get('connection_status') !==  converse.ROOMSTATUS.ENTERED) {
-                    return;
-                }
-                await api.waitUntil('chatRoomViewInitialized');
-                const nick = occupant.get('nick');
-                const stat = _converse.muc_show_join_leave_status ? occupant.get('status') : null;
-                const prev_info_el = this.getPreviousJoinOrLeaveNotification(this.msgs_container.lastElementChild, nick);
-                const data = prev_info_el?.dataset || {};
-
-                if (data.leave === nick) {
-                    let message;
-                    if (stat) {
-                        message = __('%1$s has left and re-entered the groupchat. "%2$s"', nick, stat);
-                    } else {
-                        message = __('%1$s has left and re-entered the groupchat', nick);
-                    }
-                    const data = {
-                        'data_name': 'leavejoin',
-                        'data_value': nick,
-                        'isodate': (new Date()).toISOString(),
-                        'extra_classes': 'chat-event',
-                        'message': message
-                    };
-                    this.msgs_container.removeChild(prev_info_el);
-                    this.msgs_container.insertAdjacentHTML('beforeend', tpl_info(data));
-                    const el = this.msgs_container.lastElementChild;
-                    setTimeout(() => u.addClass('fade-out', el), 5000);
-                    setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5500);
-                } else {
-                    let message;
-                    if (stat) {
-                        message = __('%1$s has entered the groupchat. "%2$s"', nick, stat);
-                    } else {
-                        message = __('%1$s has entered the groupchat', nick);
-                    }
-                    const data = {
-                        'data_name': 'join',
-                        'data_value': nick,
-                        'isodate': (new Date()).toISOString(),
-                        'extra_classes': 'chat-event',
-                        'message': message
-                    };
-                    if (prev_info_el) {
-                        this.msgs_container.removeChild(prev_info_el);
-                        this.msgs_container.insertAdjacentHTML('beforeend', tpl_info(data));
-                    } else {
-                        this.msgs_container.insertAdjacentHTML('beforeend', tpl_info(data));
-                        this.insertDayIndicator(this.msgs_container.lastElementChild);
-                    }
-                }
-                this.scrollDown();
-            },
-
-            async showLeaveNotification (occupant) {
-                if (!api.settings.get('muc_show_join_leave') ||
-                        occupant.get('states').includes('303') ||
-                        occupant.get('states').includes('307')) {
-                    return;
-                }
-                await api.waitUntil('chatRoomViewInitialized');
-                const nick = occupant.get('nick'),
-                      stat = _converse.muc_show_join_leave_status ? occupant.get('status') : null,
-                      prev_info_el = this.getPreviousJoinOrLeaveNotification(this.msgs_container.lastElementChild, nick),
-                      dataset = prev_info_el?.dataset || {};
-
-                if (dataset.join === nick) {
-                    let message;
-                    if (stat) {
-                        message = __('%1$s has entered and left the groupchat. "%2$s"', nick, stat);
-                    } else {
-                        message = __('%1$s has entered and left the groupchat', nick);
-                    }
-                    const data = {
-                        'data_name': 'joinleave',
-                        'data_value': nick,
-                        'isodate': (new Date()).toISOString(),
-                        'extra_classes': 'chat-event',
-                        'message': message
-                    };
-                    this.msgs_container.removeChild(prev_info_el);
-                    this.msgs_container.insertAdjacentHTML('beforeend', tpl_info(data));
-                    const el = this.msgs_container.lastElementChild;
-                    setTimeout(() => u.addClass('fade-out', el), 5000);
-                    setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5500);
-                } else {
-                    let message;
-                    if (stat) {
-                        message = __('%1$s has left the groupchat. "%2$s"', nick, stat);
-                    } else {
-                        message = __('%1$s has left the groupchat', nick);
-                    }
-                    const data = {
-                        'message': message,
-                        'isodate': (new Date()).toISOString(),
-                        'extra_classes': 'chat-event',
-                        'data_name': 'leave',
-                        'data_value': nick
-                    }
-                    if (prev_info_el) {
-                        this.msgs_container.removeChild(prev_info_el);
-                        this.msgs_container.insertAdjacentHTML('beforeend', tpl_info(data));
-                    } else {
-                        this.msgs_container.insertAdjacentHTML('beforeend', tpl_info(data));
-                        this.insertDayIndicator(this.msgs_container.lastElementChild);
-                    }
-                }
-                this.scrollDown();
-            },
-
             /**
              * Rerender the groupchat after some kind of transition. For
              * example after the spinner has been removed or after a

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

@@ -334,7 +334,7 @@ converse.plugins.add('converse-chat', {
                 }
                 this.set({'box_id': `box-${btoa(jid)}`});
                 this.initMessages();
-                this.initCSN();
+                this.initNotifications();
 
                 if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
                     this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
@@ -367,7 +367,7 @@ converse.plugins.add('converse-chat', {
                 });
             },
 
-            initCSN () {
+            initNotifications () {
                 this.csn = new Model();
             },
 

+ 49 - 10
src/headless/converse-muc.js

@@ -16,6 +16,8 @@ import muc_utils from "./utils/muc";
 import stanza_utils from "./utils/stanza";
 import u from "./utils/form";
 
+converse.MUC_TRAFFIC_STATES = ['entered', 'exited'];
+
 const MUC_ROLE_WEIGHTS = {
     'moderator':    1,
     'participant':  2,
@@ -368,7 +370,7 @@ converse.plugins.add('converse-muc', {
                 this.debouncedRejoin = debounce(this.rejoin, 250);
                 this.set('box_id', `box-${btoa(this.get('jid'))}`);
                 this.initMessages();
-                this.initCSN();
+                this.initNotifications();
                 this.initOccupants();
                 this.initDiscoModels(); // sendChatState depends on this.features
                 this.registerHandlers();
@@ -377,6 +379,10 @@ converse.plugins.add('converse-muc', {
                 await this.restoreSession();
                 this.session.on('change:connection_status', this.onConnectionStatusChanged, this);
 
+                this.listenTo(this.occupants, 'add', this.onOccupantAdded);
+                this.listenTo(this.occupants, 'remove', this.onOccupantRemoved);
+                this.listenTo(this.occupants, 'change:show', this.onOccupantShowChanged);
+
                 const restored = await this.restoreFromCache()
                 if (!restored) {
                     this.join();
@@ -461,6 +467,31 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
+            onOccupantAdded (occupant) {
+                if (this.session.get('connection_status') ===  converse.ROOMSTATUS.ENTERED &&
+                        occupant.get('show') === 'online') {
+                    this.updateNotifications(occupant.get('nick'), 'entered');
+                }
+            },
+
+            onOccupantRemoved (occupant) {
+                if (this.session.get('connection_status') ===  converse.ROOMSTATUS.ENTERED &&
+                        occupant.get('show') === 'online') {
+                    this.updateNotifications(occupant.get('nick'), 'exited');
+                }
+            },
+
+            onOccupantShowChanged (occupant) {
+                if (occupant.get('states').includes('303')) {
+                    return;
+                }
+                if (occupant.get('show') === 'offline') {
+                    this.updateNotifications(occupant.get('nick'), 'exited');
+                } else if (occupant.get('show') === 'online') {
+                    this.updateNotifications(occupant.get('nick'), 'entered');
+                }
+            },
+
             /**
              * Clear stale cache and re-join a MUC we've been in before.
              * @private
@@ -1885,7 +1916,7 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
-            removeCSNFor (actor, state) {
+            removeNotification (actor, state) {
                 const actors_per_state = this.csn.toJSON();
                 const existing_actors = Array.from(actors_per_state[state]) || [];
                 if (existing_actors.includes(actor)) {
@@ -1895,24 +1926,32 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
-            updateCSN (attrs) {
-                const actor = attrs.nick;
-                const state = attrs.chat_state;
+            /**
+             * Update the notifications model by adding the passed in nickname
+             * to the array of nicknames that all match a particular state.
+             * The state can be a XEP-0085 Chat State or a XEP-0045 join/leave
+             * state.
+             * @param {String} actor - The nickname of the actor that causes the notification
+             * @param {String} state - The state representing the type of notificcation
+             */
+            updateNotifications (actor, state) {
                 const actors_per_state = this.csn.toJSON();
                 const existing_actors = actors_per_state[state] || [];
                 if (existing_actors.includes(actor)) {
                     return;
                 }
-                const new_actors_per_state = converse.CHAT_STATES.reduce((out, s) => {
+                const reducer = (out, s) => {
                     if (s === state) {
                         out[s] =  [...existing_actors, actor];
                     } else {
                         out[s] = (actors_per_state[s] || []).filter(a => a !== actor);
                     }
                     return out;
-                }, {});
-                this.csn.set(new_actors_per_state);
-                window.setTimeout(() => this.removeCSNFor(actor, state), 10000);
+                };
+                const actors_per_chat_state = converse.CHAT_STATES.reduce(reducer, {});
+                const actors_per_traffic_state = converse.MUC_TRAFFIC_STATES.reduce(reducer, {});
+                this.csn.set(Object.assign(actors_per_chat_state, actors_per_traffic_state));
+                window.setTimeout(() => this.removeNotification(actor, state), 10000);
             },
 
             /**
@@ -1963,7 +2002,7 @@ converse.plugins.add('converse-muc', {
                 this.setEditable(attrs, attrs.time);
 
                 if (attrs['chat_state']) {
-                    this.updateCSN(attrs);
+                    this.updateNotifications(attrs.nick, attrs.chat_state);
                 }
                 if (u.shouldCreateGroupchatMessage(attrs)) {
                     const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);