Jelajahi Sumber

MUC: create role/affiliation change message objects

in @converse/headless instead of injecting HTML via the view
JC Brand 5 tahun lalu
induk
melakukan
e91a38547a
4 mengubah file dengan 157 tambahan dan 101 penghapusan
  1. 42 27
      spec/muc.js
  2. 30 22
      spec/retractions.js
  3. 1 49
      src/converse-muc-views.js
  4. 84 3
      src/headless/converse-muc.js

+ 42 - 27
spec/muc.js

@@ -2804,8 +2804,8 @@
                             'role': 'visitor'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                let info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
-                expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy has been muted");
+                const info_msg = await u.waitUntil(() => view.el.querySelector('.chat-info__message'));
+                expect(info_msg.textContent.trim()).toBe("annoyingGuy has been muted");
 
                 presence = $pres({
                         'from': 'lounge@montague.lit/annoyingGuy',
@@ -2818,8 +2818,10 @@
                             'role': 'participant'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
-                expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy has been given a voice");
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                        "annoyingGuy has been given a voice"
+                );
 
                 // Check that we don't see an info message concerning the role,
                 // if the affiliation has changed.
@@ -2834,8 +2836,10 @@
                             'role': 'visitor'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
-                expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy is no longer a member of this groupchat");
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "annoyingGuy is no longer a member of this groupchat"
+                );
                 done();
             }));
 
@@ -3331,7 +3335,10 @@
                             'role': 'participant'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.el.querySelectorAll('.chat-info')[2].textContent.trim()).toBe("annoyingGuy is now an owner of this groupchat");
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "annoyingGuy is now an owner of this groupchat"
+                );
                 done();
             }));
 
@@ -3547,7 +3554,6 @@
                 });
                 spyOn(view.model, 'setRole').and.callThrough();
                 spyOn(view, 'showErrorMessage').and.callThrough();
-                spyOn(view, 'showChatEvent').and.callThrough();
                 spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
 
                 // New user enters the groupchat
@@ -3628,8 +3634,11 @@
                             'role': 'moderator'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                let info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
-                expect(info_msgs.pop().textContent.trim()).toBe("trustworthyguy is now a moderator");
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "trustworthyguy is now a moderator"
+                );
+
                 // Call now with the correct amount of arguments.
                 // XXX: Calling onFormSubmitted directly, trying
                 // again via triggering Event doesn't work for some weird
@@ -3638,7 +3647,6 @@
                 view.onFormSubmitted(new Event('submit'));
 
                 expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
-                expect(view.showChatEvent.calls.count()).toBe(1);
                 expect(view.model.setRole).toHaveBeenCalled();
                 expect(sent_IQ.toLocaleString()).toBe(
                     `<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@@ -3667,10 +3675,12 @@
                             'jid': 'trustworthyguy@montague.lit',
                             'affiliation': 'member',
                             'role': 'participant'
-                        });
+                });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
-                expect(info_msgs.pop().textContent.trim()).toBe("trustworthyguy is no longer a moderator");
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "trustworthyguy is no longer a moderator"
+                );
                 done();
             }));
 
@@ -3690,7 +3700,6 @@
                 });
                 spyOn(view.model, 'setRole').and.callThrough();
                 spyOn(view, 'showErrorMessage').and.callThrough();
-                spyOn(view, 'showChatEvent').and.callThrough();
                 spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
 
                 // New user enters the groupchat
@@ -3770,8 +3779,10 @@
                             'role': 'visitor'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                let info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
-                expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy has been muted");
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "annoyingGuy has been muted"
+                );
 
                 // Call now with the correct of arguments.
                 // XXX: Calling onFormSubmitted directly, trying
@@ -3781,7 +3792,6 @@
                 view.onFormSubmitted(new Event('submit'));
 
                 expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
-                expect(view.showChatEvent.calls.count()).toBe(1);
                 expect(view.model.setRole).toHaveBeenCalled();
                 expect(sent_IQ.toLocaleString()).toBe(
                     `<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@@ -3813,8 +3823,10 @@
                             'role': 'participant'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
-                expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy has been given a voice");
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "annoyingGuy has been given a voice"
+                );
                 done();
             }));
 
@@ -5231,9 +5243,10 @@
                 expect(bottom_panel.textContent.trim()).toBe("You're not allowed to send messages in this room");
 
                 // Check now that things get restored when the user is given a voice
-                let info_msgs = sizzle('.chat-info', view.el);
-                expect(info_msgs.length).toBe(1);
-                expect(info_msgs[0].textContent.trim()).toBe("troll is no longer an owner of this groupchat");
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "troll is no longer an owner of this groupchat"
+                );
 
                 stanza = u.toStanza(`
                     <presence
@@ -5247,16 +5260,18 @@
                     </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                info_msgs = sizzle('.chat-info', view.el);
-
                 bottom_panel = view.el.querySelector('.muc-bottom-panel');
                 expect(bottom_panel).toBe(null);
 
                 textarea = view.el.querySelector('.chat-textarea');
                 expect(textarea === null).toBe(false);
 
-                expect(info_msgs.length).toBe(2);
-                expect(info_msgs[1].textContent.trim()).toBe("troll has been given a voice");
+                // Check now that things get restored when the user is given a voice
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "troll has been given a voice"
+                );
+                expect(view.el.querySelectorAll('.chat-info__message').length).toBe(2);
                 done();
             }));
         });

+ 30 - 22
spec/retractions.js

@@ -12,8 +12,8 @@
 
     async function sendAndThenRetractMessage (_converse, view) {
         view.model.sendMessage('hello world');
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
-        const msg_obj = view.model.messages.at(0);
+        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 1);
+        const msg_obj = view.model.messages.last();
         const reflection_stanza = u.toStanza(`
             <message xmlns="jabber:client"
                     from="${msg_obj.get('from')}"
@@ -625,7 +625,7 @@
                 const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
 
-                const msg_obj = view.model.messages.at(0);
+                const msg_obj = view.model.messages.last();
                 expect(Strophe.serialize(retraction_stanza)).toBe(
                     `<message id="${retraction_stanza.getAttribute('id')}" to="${muc_jid}" type="groupchat" xmlns="jabber:client">`+
                         `<store xmlns="urn:xmpp:hints"/>`+
@@ -634,7 +634,7 @@
                         `</apply-to>`+
                     `</message>`);
 
-                const message = view.model.messages.at(0);
+                const message = view.model.messages.last();
                 expect(message.get('retracted')).toBeTruthy();
                 expect(message.get('is_ephemeral')).toBe(false);
                 expect(message.get('editable')).toBeFalsy();
@@ -654,10 +654,10 @@
                 _converse.connection._dataRecv(test_utils.createRequest(reflection));
                 await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
 
-                expect(view.model.messages.length).toBe(1);
-                expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
-                expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
-                expect(view.model.messages.at(0).get('editable')).toBe(false);
+                expect(view.model.messages.length).toBe(2);
+                expect(view.model.messages.last().get('retracted')).toBeTruthy();
+                expect(view.model.messages.last().get('is_ephemeral')).toBe(false);
+                expect(view.model.messages.last().get('editable')).toBe(false);
                 expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
                 const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
                 expect(el.textContent).toBe('romeo has removed this message');
@@ -676,15 +676,19 @@
                 const occupant = view.model.getOwnOccupant();
                 expect(occupant.get('role')).toBe('moderator');
                 occupant.save('role', 'member');
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "romeo is no longer a moderator"
+                );
                 const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
 
-                expect(view.model.messages.length).toBe(1);
-                expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
+                expect(view.model.messages.length).toBe(2);
+                expect(view.model.messages.last().get('retracted')).toBeTruthy();
                 const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
                 expect(el.textContent.trim()).toBe('romeo has removed this message');
 
-                const message = view.model.messages.at(0);
+                const message = view.model.messages.last();
                 const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
                 // The server responds with an error message
                 const error = u.toStanza(`
@@ -702,10 +706,10 @@
                 _converse.connection._dataRecv(test_utils.createRequest(error));
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 1);
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
-                expect(view.model.messages.length).toBe(1);
-                expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
-                expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
-                expect(view.model.messages.at(0).get('editable')).toBeTruthy();
+                expect(view.model.messages.length).toBe(2);
+                expect(view.model.messages.last().get('retracted')).toBeFalsy();
+                expect(view.model.messages.last().get('is_ephemeral')).toBeFalsy();
+                expect(view.model.messages.last().get('editable')).toBeTruthy();
 
                 expect(view.el.querySelectorAll('.chat-error').length).toBe(1);
                 const errmsg = view.el.querySelector('.chat-error');
@@ -713,7 +717,7 @@
                 done();
             }));
 
-            it("can be retracted by its author, causing an timeout error in response",
+            it("can be retracted by its author, causing a timeout error in response",
                 mock.initConverse(
                     ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
@@ -727,21 +731,25 @@
                 const occupant = view.model.getOwnOccupant();
                 expect(occupant.get('role')).toBe('moderator');
                 occupant.save('role', 'member');
+                await u.waitUntil(() =>
+                    Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
+                    "romeo is no longer a moderator"
+                );
                 await sendAndThenRetractMessage(_converse, view);
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
 
-                expect(view.model.messages.length).toBe(1);
-                expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
+                expect(view.model.messages.length).toBe(2);
+                expect(view.model.messages.last().get('retracted')).toBeTruthy();
                 const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
                 expect(el.textContent.trim()).toBe('romeo has removed this message');
 
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
 
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
-                expect(view.model.messages.length).toBe(1);
-                expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
-                expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
-                expect(view.model.messages.at(0).get('editable')).toBeTruthy();
+                expect(view.model.messages.length).toBe(2);
+                expect(view.model.messages.last().get('retracted')).toBeFalsy();
+                expect(view.model.messages.last().get('is_ephemeral')).toBeFalsy();
+                expect(view.model.messages.last().get('editable')).toBeTruthy();
 
                 const error_messages = view.el.querySelectorAll('.chat-error');
                 expect(error_messages.length).toBe(2);

+ 1 - 49
src/converse-muc-views.js

@@ -1123,60 +1123,12 @@ converse.plugins.add('converse-muc-views', {
                 if (occupant.get('jid') === _converse.bare_jid) {
                     this.renderHeading();
                 }
-                this.informOfOccupantsAffiliationChange(occupant);
             },
 
-            informOfOccupantsAffiliationChange (occupant) {
-                const previous_affiliation = occupant._previousAttributes.affiliation;
-                const current_affiliation = occupant.get('affiliation');
-
-                if (previous_affiliation === 'admin') {
-                    this.showChatEvent(__("%1$s is no longer an admin of this groupchat", occupant.get('nick')))
-                } else if (previous_affiliation === 'owner') {
-                    this.showChatEvent(__("%1$s is no longer an owner of this groupchat", occupant.get('nick')))
-                } else if (previous_affiliation === 'outcast') {
-                    this.showChatEvent(__("%1$s is no longer banned from this groupchat", occupant.get('nick')))
-                }
-                if (current_affiliation === 'none' && previous_affiliation === 'member') {
-                    this.showChatEvent(__("%1$s is no longer a member of this groupchat", occupant.get('nick')))
-                } if (current_affiliation === 'member') {
-                    this.showChatEvent(__("%1$s is now a member of this groupchat", occupant.get('nick')))
-                } else if (current_affiliation === 'admin' || current_affiliation == 'owner') {
-                    // For example: AppleJack is now an (admin|owner) of this groupchat
-                    this.showChatEvent(__('%1$s is now an %2$s of this groupchat', occupant.get('nick'), current_affiliation))
-                }
-            },
-
-            onOccupantRoleChanged (occupant, changed) {
+            onOccupantRoleChanged (occupant) {
                 if (occupant.get('jid') === _converse.bare_jid) {
                     this.renderBottomPanel();
                 }
-                this.informOfOccupantsRoleChange(occupant, changed);
-            },
-
-            informOfOccupantsRoleChange (occupant, changed) {
-                if (changed === "none" || occupant.changed.affiliation) {
-                    // We don't inform of role changes if they accompany affiliation changes.
-                    return;
-                }
-                const previous_role = occupant._previousAttributes.role;
-                if (previous_role === 'moderator') {
-                    this.showChatEvent(__("%1$s is no longer a moderator", occupant.get('nick')))
-                }
-                if (previous_role === 'visitor') {
-                    this.showChatEvent(__("%1$s has been given a voice", occupant.get('nick')))
-                }
-                if (occupant.get('role') === 'visitor') {
-                    this.showChatEvent(__("%1$s has been muted", occupant.get('nick')))
-                }
-                if (occupant.get('role') === 'moderator') {
-                    if (!['owner', 'admin'].includes(occupant.get('affiliation'))) {
-                        // We only show this message if the user isn't already
-                        // an admin or owner, otherwise this isn't new
-                        // information.
-                        this.showChatEvent(__("%1$s is now a moderator", occupant.get('nick')))
-                    }
-                }
             },
 
             /**

+ 84 - 3
src/headless/converse-muc.js

@@ -383,6 +383,8 @@ converse.plugins.add('converse-muc', {
                 this.listenTo(this.occupants, 'add', this.onOccupantAdded);
                 this.listenTo(this.occupants, 'remove', this.onOccupantRemoved);
                 this.listenTo(this.occupants, 'change:show', this.onOccupantShowChanged);
+                this.listenTo(this.occupants, 'change:affiliation', this.createAffiliationChangeMessage);
+                this.listenTo(this.occupants, 'change:role', this.createRoleChangeMessage);
 
                 const restored = await this.restoreFromCache()
                 if (!restored) {
@@ -2109,6 +2111,86 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
+            createAffiliationChangeMessage (occupant) {
+                const previous_affiliation = occupant._previousAttributes.affiliation;
+                const current_affiliation = occupant.get('affiliation');
+                if (previous_affiliation === 'admin') {
+                    this.createMessage({
+                        'type': 'info',
+                        'message': __("%1$s is no longer an admin of this groupchat", occupant.get('nick'))
+                    });
+                } else if (previous_affiliation === 'owner') {
+                    this.createMessage({
+                        'type': 'info',
+                        'message': __("%1$s is no longer an owner of this groupchat", occupant.get('nick'))
+                    });
+                } else if (previous_affiliation === 'outcast') {
+                    this.createMessage({
+                        'type': 'info',
+                        'message': __("%1$s is no longer banned from this groupchat", occupant.get('nick'))
+                    });
+                }
+
+                if (current_affiliation === 'none' && previous_affiliation === 'member') {
+                    this.createMessage({
+                        'type': 'info',
+                        'message': __("%1$s is no longer a member of this groupchat", occupant.get('nick'))
+                    });
+                }
+
+                if (current_affiliation === 'member') {
+                    this.createMessage({
+                        'type': 'info',
+                        'message': __("%1$s is now a member of this groupchat", occupant.get('nick'))
+                    });
+                } else if (current_affiliation === 'admin' || current_affiliation == 'owner') {
+                    // For example: AppleJack is now an (admin|owner) of this groupchat
+                    this.createMessage({
+                        'type': 'info',
+                        'message': __(
+                            '%1$s is now an %2$s of this groupchat',
+                            occupant.get('nick'),
+                            current_affiliation
+                        )
+                    });
+                }
+            },
+
+            createRoleChangeMessage (occupant, changed) {
+                if (changed === "none" || occupant.changed.affiliation) {
+                    // We don't inform of role changes if they accompany affiliation changes.
+                    return;
+                }
+                const previous_role = occupant._previousAttributes.role;
+                if (previous_role === 'moderator') {
+                    this.createMessage({
+                        'type': 'info',
+                        'message': __("%1$s is no longer a moderator", occupant.get('nick'))
+                    });
+                }
+                if (previous_role === 'visitor') {
+                    this.createMessage({
+                        'type': 'info',
+                        'message': __("%1$s has been given a voice", occupant.get('nick'))
+                    });
+                }
+                if (occupant.get('role') === 'visitor') {
+                    this.createMessage({
+                        'type': 'info',
+                        'message': __("%1$s has been muted", occupant.get('nick'))
+                    });
+                }
+                if (occupant.get('role') === 'moderator') {
+                    if (!['owner', 'admin'].includes(occupant.get('affiliation'))) {
+                        // Oly show this message if the user isn't already
+                        // an admin or owner, otherwise this isn't new information.
+                        this.createMessage({
+                            'type': 'info',
+                            'message': __("%1$s is now a moderator", occupant.get('nick'))
+                        });
+                    }
+                }
+            },
 
             /**
              * Create info messages based on a received presence or message stanza
@@ -2122,10 +2204,9 @@ converse.plugins.add('converse-muc', {
                 if (!x) {
                     return;
                 }
-                const codes = sizzle('status', x).map(s => s.getAttribute('code'));
-                codes.forEach(code => {
+                sizzle('status', x).map(s => s.getAttribute('code')).forEach(code => {
                     const data = {
-                        'type': 'info'
+                        'type': 'info',
                     };
                     if (code === '110' || (code === '100' && !is_self)) {
                         return;