Browse Source

Fixes #1094

- Show users who are registered on the different members lists
- Show badges indicating user's roles and affiliations
JC Brand 7 years ago
parent
commit
859bc0616e
9 changed files with 194 additions and 88 deletions
  1. 12 10
      css/converse.css
  2. 12 10
      css/inverse.css
  3. 11 5
      mockup/chatroom.html
  4. 15 9
      sass/_chatrooms.scss
  5. 23 13
      spec/chatroom.js
  6. 16 7
      src/converse-muc-views.js
  7. 49 22
      src/converse-muc.js
  8. 32 5
      src/templates/occupant.html
  9. 24 7
      src/utils/muc.js

+ 12 - 10
css/converse.css

@@ -8309,9 +8309,8 @@ body.reset {
             display: block;
             font-size: 12px;
             overflow: hidden;
-            padding: 0.2em 0.5em 0.2em 0;
-            text-overflow: ellipsis;
-            white-space: nowrap; }
+            padding: 0.25em 0.25em 0.25em 0;
+            text-overflow: ellipsis; }
             #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li .fa,
             #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li .fa {
               margin-right: 0.5em; }
@@ -8321,10 +8320,16 @@ body.reset {
             #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant,
             #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant {
               cursor: pointer; }
+              #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant div.row.no-gutters,
+              #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant div.row.no-gutters {
+                flex-wrap: nowrap; }
+              #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .badge,
+              #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .badge {
+                margin-top: 0.125rem; }
               #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status,
               #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status {
                 display: inline-block;
-                margin-right: 0.5em;
+                margin: 0.5em 0.5em 0.5em 0;
                 width: 0.5em;
                 height: 0.5em; }
                 #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-online, #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-chat,
@@ -8340,12 +8345,9 @@ body.reset {
                 #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-xa,
                 #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-xa {
                   background-color: orange; }
-            #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.moderator,
-            #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.moderator {
-              color: #D24E2B; }
-            #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.visitor,
-            #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.visitor {
-              color: #A8ABA1; }
+                #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-offline,
+                #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-offline {
+                  background-color: darkgrey; }
       #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .chatroom-form-container,
       #conversejs .chatroom .box-flyout .chatroom-body .chatroom-form-container {
         background-color: white;

+ 12 - 10
css/inverse.css

@@ -8449,9 +8449,8 @@ body {
             display: block;
             font-size: 14px;
             overflow: hidden;
-            padding: 0.2em 0.5em 0.2em 0;
-            text-overflow: ellipsis;
-            white-space: nowrap; }
+            padding: 0.25em 0.25em 0.25em 0;
+            text-overflow: ellipsis; }
             #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li .fa,
             #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li .fa {
               margin-right: 0.5em; }
@@ -8461,10 +8460,16 @@ body {
             #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant,
             #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant {
               cursor: pointer; }
+              #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant div.row.no-gutters,
+              #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant div.row.no-gutters {
+                flex-wrap: nowrap; }
+              #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .badge,
+              #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .badge {
+                margin-top: 0.125rem; }
               #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status,
               #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status {
                 display: inline-block;
-                margin-right: 0.5em;
+                margin: 0.5em 0.5em 0.5em 0;
                 width: 0.5em;
                 height: 0.5em; }
                 #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-online, #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-chat,
@@ -8480,12 +8485,9 @@ body {
                 #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-xa,
                 #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-xa {
                   background-color: orange; }
-            #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.moderator,
-            #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.moderator {
-              color: #D24E2B; }
-            #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.visitor,
-            #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.visitor {
-              color: #A8ABA1; }
+                #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-offline,
+                #conversejs .chatroom .box-flyout .chatroom-body .occupants ul li.occupant .occupant-status.occupant-offline {
+                  background-color: darkgrey; }
       #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .chatroom-form-container,
       #conversejs .chatroom .box-flyout .chatroom-body .chatroom-form-container {
         background-color: white;

+ 11 - 5
mockup/chatroom.html

@@ -233,15 +233,21 @@
 
                             <ul class="occupant-list">
                                 <li class="moderator occupant" title="Click to mention leia in your message.">
-                                    <div class="occupant-status occupant-online circle" title="Online"></div>Juliet Capulet</li>
+                                    <div class="occupant-status occupant-online circle" title="Online"></div>Juliet Capulet
+                                    <span class="badge badge-info">Owner</span>
+                                </li>
                                 <li class="moderator occupant" title="Click to mention leia in your message.">
-                                    <div class="occupant-status occupant-online circle" title="Online"></div>Romeo Montague</li>
-                                <li class="participant occupant" title="Click to mention leia in your message.">
-                                    <div class="occupant-status occupant-online circle" title="Online"></div>Mercutio</li>
+                                    <div class="occupant-status occupant-online circle" title="Online"></div>Romeo Montague
+                                    <span class="badge badge-info">Moderator</span>
+                                </li>
                                 <li class="participant occupant" title="Click to mention leia in your message.">
                                     <div class="occupant-status occupant-away circle" title="Away"></div>Lady Montague</li>
                                 <li class="participant occupant" title="Click to mention leia in your message.">
-                                    <div class="occupant-status occupant-online circle" title="Online"></div>Lord Montague</li>
+                                    <div class="occupant-status occupant-online circle" title="Online"></div>Mercutio
+                                    <span class="badge badge-secondary">Visitor</span>
+                                </li>
+                                <li class="participant occupant" title="Click to mention leia in your message.">
+                                    <div class="occupant-status occupant-offline circle" title="Offline"></div>Lord Montague</li>
                                 <li class="participant occupant" title="Click to mention leia in your message.">
                                     <div class="occupant-status occupant-online circle" title="Online"></div>Friar Laurence</li>
                                 <li class="participant occupant" title="Click to mention leia in your message.">

+ 15 - 9
sass/_chatrooms.scss

@@ -172,9 +172,8 @@
                             display: block;
                             font-size: $font-size-small;
                             overflow: hidden;
-                            padding: 0.2em 0.5em 0.2em 0;
+                            padding: 0.25em 0.25em 0.25em 0;
                             text-overflow: ellipsis;
-                            white-space: nowrap;
                             .fa {
                                 margin-right: 0.5em;
                             }
@@ -183,11 +182,21 @@
                             }
                             &.occupant {
                                 cursor: pointer;
+
+                                div.row.no-gutters {
+                                    flex-wrap: nowrap;
+                                }
+
+                                .badge {
+                                    margin-top: 0.125rem;
+                                }
+
                                 .occupant-status {
                                     display: inline-block;
-                                    margin-right: 0.5em;
+                                    margin: 0.5em 0.5em 0.5em 0;
                                     width: 0.5em;
                                     height: 0.5em;
+
                                     &.occupant-online,
                                     &.occupant-chat {
                                         background-color: #1A9707;
@@ -201,14 +210,11 @@
                                     &.occupant-xa {
                                         background-color: orange;
                                     }
+                                    &.occupant-offline {
+                                        background-color: darkgrey;
+                                    }
                                 }
                             }
-                            &.moderator {
-                                color: $moderator-color;
-                            }
-                            &.visitor {
-                                color: $visitor-color;
-                            }
                         }
                     }
                 }

+ 23 - 13
spec/chatroom.js

@@ -1142,8 +1142,7 @@
                         expect(occupants.querySelectorAll('li').length).toBe(2+i);
                         model = view.occupantsview.model.where({'nick': name})[0];
                         var index = view.occupantsview.model.indexOf(model);
-                        expect(occupants.querySelectorAll('li')[index].textContent).toBe(mock.chatroom_names[i]);
-                        expect($(occupants.querySelectorAll('li')[index]).hasClass('moderator')).toBe(role === "moderator");
+                        expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(mock.chatroom_names[i]);
                     }
 
                     // Test users leaving the room
@@ -1166,7 +1165,7 @@
                         expect(occupants.querySelectorAll('li').length).toBe(i+1);
                     }
                     done();
-                });
+                }).catch(_.partial(console.error, _));
             }));
 
             it("escapes occupant nicknames when rendering them, to avoid JS-injection attacks",
@@ -1193,9 +1192,9 @@
 
                     _converse.connection._dataRecv(test_utils.createRequest(presence));
                     var view = _converse.chatboxviews.get('lounge@localhost');
-                    var occupants = view.el.querySelector('.occupant-list').querySelectorAll('li');
+                    var occupants = view.el.querySelector('.occupant-list').querySelectorAll('li .occupant-nick');
                     expect(occupants.length).toBe(2);
-                    expect($(occupants).first().text()).toBe("&lt;img src=&quot;x&quot; onerror=&quot;alert(123)&quot;/&gt;");
+                    expect($(occupants).first().text().trim()).toBe("&lt;img src=&quot;x&quot; onerror=&quot;alert(123)&quot;/&gt;");
                     done();
                 });
             }));
@@ -1208,6 +1207,13 @@
                 test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () {
                     var view = _converse.chatboxviews.get('lounge@localhost');
                     var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
+
+                    var occupants = view.el.querySelector('.occupant-list').querySelectorAll('li');
+                    expect(occupants.length).toBe(1);
+                    expect($(occupants).first().find('.occupant-nick').text().trim()).toBe("dummy");
+                    expect($(occupants).first().find('.badge').length).toBe(1);
+                    expect($(occupants).first().find('.badge').first().text()).toBe('Member');
+
                     var presence = $pres({
                             to:'dummy@localhost/pda',
                             from:'lounge@localhost/moderatorman'
@@ -1220,11 +1226,14 @@
                     .c('status').attrs({code:'110'}).nodeTree;
 
                     _converse.connection._dataRecv(test_utils.createRequest(presence));
-                    var occupants = view.el.querySelector('.occupant-list').querySelectorAll('li');
+                    occupants = view.el.querySelector('.occupant-list').querySelectorAll('li');
                     expect(occupants.length).toBe(2);
-                    expect($(occupants).first().text()).toBe("moderatorman");
-                    expect($(occupants).last().text()).toBe("dummy");
-                    expect($(occupants).first().attr('class').indexOf('moderator')).not.toBe(-1);
+                    expect($(occupants).first().find('.occupant-nick').text().trim()).toBe("moderatorman");
+                    expect($(occupants).last().find('.occupant-nick').text().trim()).toBe("dummy");
+                    expect($(occupants).first().find('.badge').length).toBe(2);
+                    expect($(occupants).first().find('.badge').first().text()).toBe('Admin');
+                    expect($(occupants).first().find('.badge').last().text()).toBe('Moderator');
+
                     expect($(occupants).first().attr('title')).toBe(
                         contact_jid + ' This user is a moderator. Click to mention moderatorman in your message.'
                     );
@@ -1242,13 +1251,14 @@
                     _converse.connection._dataRecv(test_utils.createRequest(presence));
 
                     occupants = view.el.querySelector('.occupant-list').querySelectorAll('li');
-                    expect($(occupants).last().text()).toBe("visitorwoman");
-                    expect($(occupants).last().attr('class').indexOf('visitor')).not.toBe(-1);
+                    expect($(occupants).last().find('.occupant-nick').text().trim()).toBe("visitorwoman");
+                    expect($(occupants).last().find('.badge').length).toBe(1);
+                    expect($(occupants).last().find('.badge').last().text()).toBe('Visitor');
                     expect($(occupants).last().attr('title')).toBe(
                         contact_jid + ' This user can NOT send messages in this room. Click to mention visitorwoman in your message.'
                     );
                     done();
-                });
+                }).catch(_.partial(console.error, _));
             }));
 
             it("will use the user's reserved nickname, if it exists",
@@ -1656,7 +1666,7 @@
 
                     var $occupants = $(view.el.querySelector('.occupant-list'));
                     expect($occupants.children().length).toBe(1);
-                    expect($occupants.children().first(0).text()).toBe("oldnick");
+                    expect($occupants.children().first(0).find('.occupant-nick').text().trim()).toBe("oldnick");
 
                     expect($chat_content.find('div.chat-info').length).toBe(1);
                     expect($chat_content.find('div.chat-info:first').html()).toBe("oldnick has entered the room");

+ 16 - 7
src/converse-muc-views.js

@@ -537,8 +537,7 @@
                                 // collection anymore).
                                 return;
                             }
-                            this.join();
-                            this.fetchMessages();
+                            this.populateAndJoin();
                             _converse.emit('chatRoomOpened', this);
                         }
                         this.model.getRoomFeatures().then(handler, handler);
@@ -919,6 +918,12 @@
                     }
                 },
 
+                populateAndJoin () {
+                    this.model.occupants.fetchMembers();
+                    this.join();
+                    this.fetchMessages();
+                },
+
                 join (nick, password) {
                     /* Join the chat room.
                      *
@@ -1536,7 +1541,7 @@
                 },
 
                 toHTML () {
-                    const show = this.model.get('show') || 'online';
+                    const show = this.model.get('show');
                     return tpl_occupant(
                         _.extend(
                             { 'jid': '',
@@ -1544,8 +1549,13 @@
                               'hint_show': _converse.PRETTY_CHAT_STATUS[show],
                               'hint_occupant': __('Click to mention %1$s in your message.', this.model.get('nick')),
                               'desc_moderator': __('This user is a moderator.'),
-                              'desc_occupant': __('This user can send messages in this room.'),
-                              'desc_visitor': __('This user can NOT send messages in this room.')
+                              'desc_participant': __('This user can send messages in this room.'),
+                              'desc_visitor': __('This user can NOT send messages in this room.'),
+                              'label_moderator': __('Moderator'),
+                              'label_visitor': __('Visitor'),
+                              'label_owner': __('Owner'),
+                              'label_member': __('Member'),
+                              'label_admin': __('Admin')
                             }, this.model.toJSON())
                     );
                 },
@@ -1824,8 +1834,7 @@
                     if (view.model.get('type') === converse.CHATROOMS_TYPE) {
                         view.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
                         view.model.registerHandlers();
-                        view.join();
-                        view.fetchMessages();
+                        view.populateAndJoin();
                     }
                 });
             }

+ 49 - 22
src/converse-muc.js

@@ -22,7 +22,7 @@
         'moderator':    1,
         'participant':  2,
         'visitor':      3,
-        'none':         4,
+        'none':         2,
     };
 
     const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, b64_sha1, sizzle, _, moment } = converse.env;
@@ -187,6 +187,8 @@
                     this.occupants.browserStorage = new Backbone.BrowserStorage.session(
                         b64_sha1(`converse.occupants-${_converse.bare_jid}${this.get('jid')}`)
                     );
+                    this.occupants.chatroom  = this;
+
                     this.registerHandlers();
                     this.on('change:chat_state', this.sendChatState, this);
                 },
@@ -726,22 +728,6 @@
                     return this;
                 },
 
-                findOccupant (data) {
-                    /* Try to find an existing occupant based on the passed in
-                     * data object.
-                     *
-                     * If we have a JID, we use that as lookup variable,
-                     * otherwise we use the nick. We don't always have both,
-                     * but should have at least one or the other.
-                     */
-                    const jid = Strophe.getBareJidFromJid(data.jid);
-                    if (jid !== null) {
-                        return this.occupants.where({'jid': jid}).pop();
-                    } else {
-                        return this.occupants.where({'nick': data.nick}).pop();
-                    }
-                },
-
                 updateOccupantsOnPresence (pres) {
                     /* Given a presence stanza, update the occupant model
                      * based on its contents.
@@ -753,7 +739,7 @@
                     if (data.type === 'error') {
                         return true;
                     }
-                    const occupant = this.findOccupant(data);
+                    const occupant = this.occupants.findOccupant(data);
                     if (data.type === 'unavailable') {
                         if (occupant) {
                             // Even before destroying, we set the new data, so
@@ -788,7 +774,8 @@
                             'from': from,
                             'nick': Strophe.getResourceFromJid(from),
                             'type': pres.getAttribute("type"),
-                            'states': []
+                            'states': [],
+                            'show': 'online'
                           };
                     _.each(pres.childNodes, function (child) {
                         switch (child.nodeName) {
@@ -987,6 +974,11 @@
 
 
             _converse.ChatRoomOccupant = Backbone.Model.extend({
+
+                defaults: {
+                    'show': 'offline'
+                },
+
                 initialize (attributes) {
                     this.set(_.extend({
                         'id': _converse.connection.getUniqueId(),
@@ -1014,13 +1006,48 @@
                     const role1 = occupant1.get('role') || 'none';
                     const role2 = occupant2.get('role') || 'none';
                     if (MUC_ROLE_WEIGHTS[role1] === MUC_ROLE_WEIGHTS[role2]) {
-                        const nick1 = occupant1.get('nick').toLowerCase();
-                        const nick2 = occupant2.get('nick').toLowerCase();
-                        return nick1 < nick2 ? -1 : (nick1 > nick2? 1 : 0);
+                        try {
+                            const nick1 = occupant1.get('nick').toLowerCase();
+                            const nick2 = occupant2.get('nick').toLowerCase();
+                            return nick1 < nick2 ? -1 : (nick1 > nick2? 1 : 0);
+                        } catch (e) {
+                            debugger;
+                        }
                     } else  {
                         return MUC_ROLE_WEIGHTS[role1] < MUC_ROLE_WEIGHTS[role2] ? -1 : 1;
                     }
                 },
+
+                fetchMembers () {
+                    this.chatroom.getJidsWithAffiliations(['member', 'owner', 'admin'])
+                    .then((jids) => {
+                        _.each(jids, (attrs) => {
+                            const occupant = this.findOccupant({'jid': attrs.jid});
+                            if (occupant) {
+                                occupant.save(attrs);
+                            } else {
+                                this.create(attrs);
+                            }
+                        });
+                    }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+                },
+
+                findOccupant (data) {
+                    /* Try to find an existing occupant based on the passed in
+                     * data object.
+                     *
+                     * If we have a JID, we use that as lookup variable,
+                     * otherwise we use the nick. We don't always have both,
+                     * but should have at least one or the other.
+                     */
+                    const jid = Strophe.getBareJidFromJid(data.jid);
+                    if (jid !== null) {
+                        return this.where({'jid': jid}).pop();
+                    } else {
+                        return this.where({'nick': data.nick}).pop();
+                    }
+                },
+
             });
 
 

+ 32 - 5
src/templates/occupant.html

@@ -1,13 +1,40 @@
-<li class="{{{ o.role }}} occupant" id="{{{ o.id }}}"
+<li class="occupant" id="{{{ o.id }}}"
     {[ if (o.role === "moderator") { ]}
        title="{{{ o.jid }}} {{{ o.desc_moderator }}} {{{ o.hint_occupant }}}"
     {[ } ]}
-    {[ if (o.role === "occupant") { ]}
-       title="{{{ o.jid }}} {{{ o.desc_occupant }}} {{{ o.hint_occupant }}}"
+    {[ if (o.role === "participant") { ]}
+       title="{{{ o.jid }}} {{{ o.desc_participant }}} {{{ o.hint_occupant }}}"
     {[ } ]}
     {[ if (o.role === "visitor") { ]}
        title="{{{ o.jid }}} {{{ o.desc_visitor }}} {{{ o.hint_occupant }}}"
     {[ } ]}
-    {[ if (!_.includes(["visitor", "occupant", "moderator"], o.role)) { ]}
+    {[ if (!_.includes(["visitor", "participant", "moderator"], o.role)) { ]}
        title="{{{ o.jid }}} {{{ o.hint_occupant }}}"
-       {[ } ]}><div class="occupant-status occupant-{{{o.show}}} circle" title="{{{o.hint_show}}}"></div>{{{o.nick}}}</li>
+    {[ } ]}>
+    <div class="row no-gutters">
+        <div class="col-auto">
+            <div class="occupant-status occupant-{{{o.show}}} circle" title="{{{o.hint_show}}}"></div>
+        </div>
+        <div class="col">
+            <span class="occupant-nick">
+                {{{o.nick || o.jid}}}
+            </span>
+            {[ if (o.affiliation === "owner") { ]}
+                <span class="badge badge-danger">{{{o.label_owner}}}</span>
+            {[ } ]}
+            {[ if (o.affiliation === "admin") { ]}
+                <span class="badge badge-info">{{{o.label_admin}}}</span>
+            {[ } ]}
+            {[ if (o.affiliation === "member") { ]}
+                <span class="badge badge-info">{{{o.label_member}}}</span>
+            {[ } ]}
+
+            {[ if (o.role === "moderator") { ]}
+                <span class="badge badge-info">{{{o.label_moderator}}}</span>
+            {[ } ]}
+            {[ if (o.role === "visitor") { ]}
+                <span class="badge badge-secondary">{{{o.label_visitor}}}</span>
+            {[ } ]}
+        </div>
+    </div>
+</li>

+ 24 - 7
src/utils/muc.js

@@ -72,15 +72,32 @@
     };
 
     u.parseMemberListIQ = function parseMemberListIQ (iq) {
-        /* Given an IQ stanza with a member list, create an array of member
-            * objects.
-            */
+        /* Given an IQ stanza with a member list, create an array of member objects.
+        */
         return _.map(
             sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq),
-            (item) => ({
-                'jid': item.getAttribute('jid'),
-                'affiliation': item.getAttribute('affiliation'),
-            })
+            (item) => {
+                const data = {
+                    'affiliation': item.getAttribute('affiliation'),
+                }
+                const jid = item.getAttribute('jid');
+                if (u.isValidJID(jid)) {
+                    data['jid'] = jid;
+                } else {
+                    // XXX: Prosody sends nick for the jid attribute value
+                    // Perhaps for anonymous room?
+                    data['nick'] = jid;
+                }
+                const nick = item.getAttribute('nick');
+                if (nick) {
+                    data['nick'] = nick;
+                }
+                const role = item.getAttribute('role');
+                if (role) {
+                    data['role'] = nick;
+                }
+                return data;
+            }
         );
     };