فهرست منبع

Save room features in separate model

As a namespacing mechanism to avoid clashes.
Fixes bug where two chats are shown as currently being active in the rooms list.
JC Brand 6 سال پیش
والد
کامیت
a4d608dcdf

+ 1 - 0
CHANGES.md

@@ -3,6 +3,7 @@
 ## 4.0.7 (Unreleased)
 
 - Bugfix: MUC commands were being ignored
+- Bugfix: Multiple rooms shown active in the rooms list
 - UI: Always show the OMEMO lock icon (grayed out if not available).
 - Use `publish-options` with `pubsub#access_model` set to `open` when publishing OMEMO public keys and devices
 - Add a new `converse-pubsub` plugin, for generic PubSub operations

+ 69 - 55
dist/converse.js

@@ -53857,11 +53857,12 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins
         return templates_chatroom_details_modal_html__WEBPACK_IMPORTED_MODULE_9___default()(_.extend(this.model.toJSON(), {
           '_': _,
           '__': __,
+          'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),
+          'features': this.model.features.toJSON(),
+          'num_occupants': this.model.occupants.length,
           'topic': u.addHyperlinks(xss__WEBPACK_IMPORTED_MODULE_26___default.a.filterXSS(_.get(this.model.get('subject'), 'text'), {
             'whiteList': {}
-          })),
-          'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),
-          'num_occupants': this.model.occupants.length
+          }))
         }));
       }
 
@@ -55361,13 +55362,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins
         this.chatroomview = this.model.chatroomview;
         this.chatroomview.model.on('change:open', this.renderInviteWidget, this);
         this.chatroomview.model.on('change:affiliation', this.renderInviteWidget, this);
-        this.chatroomview.model.on('change', () => {
-          if (_.intersection(_converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].ROOM_FEATURES, Object.keys(this.chatroomview.model.changed)).length === 0) {
-            return;
-          }
-
-          this.renderRoomFeatures();
-        }, this);
+        this.chatroomview.model.features.on('change', this.renderRoomFeatures, this);
         this.render();
         this.model.fetch({
           'add': true,
@@ -55409,15 +55404,18 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins
       },
 
       renderRoomFeatures() {
-        const picks = _.pick(this.chatroomview.model.attributes, _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].ROOM_FEATURES),
-              iteratee = (a, v) => a || v,
-              el = this.el.querySelector('.chatroom-features');
+        const features = this.chatroomview.model.features,
+              picks = _.pick(features.attributes, _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].ROOM_FEATURES),
+              iteratee = (a, v) => a || v;
+
+        if (_.reduce(_.values(picks), iteratee)) {
+          const el = this.el.querySelector('.chatroom-features');
+          el.innerHTML = templates_chatroom_features_html__WEBPACK_IMPORTED_MODULE_11___default()(_.extend(features.toJSON(), {
+            __
+          }));
+          this.setOccupantsHeight();
+        }
 
-        el.innerHTML = templates_chatroom_features_html__WEBPACK_IMPORTED_MODULE_11___default()(_.extend(this.chatroomview.model.toJSON(), {
-          '__': __,
-          'has_features': _.reduce(_.values(picks), iteratee)
-        }));
-        this.setOccupantsHeight();
         return this;
       },
 
@@ -56448,7 +56446,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
       },
 
       renderOMEMOToolbarButton() {
-        if (this.model.get('membersonly') && this.model.get('nonanonymous')) {
+        if (this.model.features.get('membersonly') && this.model.features.get('nonanonymous')) {
           this.__super__.renderOMEMOToolbarButton.apply(arguments);
         } else {
           const icon = this.el.querySelector('.toggle-omemo');
@@ -57265,7 +57263,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
     }
 
     async function onOccupantAdded(chatroom, occupant) {
-      if (occupant.isSelf() || !chatroom.get('nonanonymous') || !chatroom.get('membersonly')) {
+      if (occupant.isSelf() || !chatroom.features.get('nonanonymous') || !chatroom.features.get('membersonly')) {
         return;
       }
 
@@ -57290,7 +57288,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
 
       if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
         await _converse.api.waitUntil('OMEMOInitialized');
-        supported = chatbox.get('nonanonymous') && chatbox.get('membersonly');
+        supported = chatbox.features.get('nonanonymous') && chatbox.features.get('membersonly');
       } else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
         supported = await _converse.contactHasOMEMOSupport(chatbox.get('jid'));
       }
@@ -57303,8 +57301,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
 
       if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
         chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o));
-        chatbox.on('change:nonanonymous', checkOMEMOSupported);
-        chatbox.on('change:membersonly', checkOMEMOSupported);
+        chatbox.features.on('change', () => checkOMEMOSupported(chatbox));
       }
     }));
 
@@ -63515,7 +63512,7 @@ _converse.initialize = function (settings, callback) {
   };
 
   this.initSession = function () {
-    const id = b64_sha1('converse.bosh-session');
+    const id = 'converse.bosh-session';
     _converse.session = new Backbone.Model({
       id
     });
@@ -66188,7 +66185,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
 
     _converse.ChatRoom = _converse.ChatBox.extend({
       defaults() {
-        return _.assign(_.clone(_converse.ChatBox.prototype.defaults), _.zipObject(_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES, _.map(_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES, _.stubFalse)), {
+        return _.assign(_.clone(_converse.ChatBox.prototype.defaults), {
           // For group chats, we distinguish between generally unread
           // messages and those ones that specifically mention the
           // user.
@@ -66203,7 +66200,6 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
           'name': '',
           'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
           'description': '',
-          'features_fetched': false,
           'roomconfig': {},
           'type': _converse.CHATROOMS_TYPE,
           'message_type': 'groupchat'
@@ -66214,6 +66210,15 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
         this.constructor.__super__.initialize.apply(this, arguments);
 
         this.on('change:connection_status', this.onConnectionStatusChanged, this);
+
+        const storage = _converse.config.get('storage');
+
+        const id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
+        this.features = new Backbone.Model(_.assign({
+          id
+        }, _.zipObject(_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES, _.map(_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES, _.stubFalse))));
+        this.features.browserStorage = new Backbone.BrowserStorage.session(id);
+        this.features.fetch();
         this.occupants = new _converse.ChatRoomOccupants();
         this.occupants.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.occupants-${_converse.bare_jid}${this.get('jid')}`));
         this.occupants.chatroom = this;
@@ -66342,6 +66347,8 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
          *  (String) exit_msg: Optional message to indicate your
          *      reason for leaving.
          */
+        this.features.destroy();
+
         this.occupants.browserStorage._clear();
 
         this.occupants.reset();
@@ -66583,13 +66590,27 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
         return this.getRoomFeatures();
       },
 
+      async getRoomIdentity() {
+        const _ref = await Promise.all([_converse.api.disco.getIdentity('conference', 'text', this.get('jid')), _converse.api.disco.getFields(this.get('jid'))]),
+              _ref2 = _slicedToArray(_ref, 2),
+              identity = _ref2[0],
+              fields = _ref2[1];
+
+        this.save({
+          'name': identity && identity.get('name'),
+          'description': _.get(fields.findWhere({
+            'var': "muc#roominfo_description"
+          }), 'attributes.value')
+        });
+      },
+
       async getRoomFeatures() {
+        // XXX: not sure whet the right place is to get the room identitiy
+        this.getRoomIdentity();
+
         const features = await _converse.api.disco.getFeatures(this.get('jid')),
-              fields = await _converse.api.disco.getFields(this.get('jid')),
-              identity = await _converse.api.disco.getIdentity('conference', 'text', this.get('jid')),
               attrs = _.extend(_.zipObject(_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES, _.map(_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES, _.stubFalse)), {
-          'features_fetched': moment().format(),
-          'name': identity && identity.get('name')
+          'fetched': moment().format()
         });
 
         features.each(feature => {
@@ -66605,10 +66626,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
 
           attrs[fieldname.replace('muc_', '')] = true;
         });
-        attrs.description = _.get(fields.findWhere({
-          'var': "muc#roominfo_description"
-        }), 'attributes.value');
-        this.save(attrs);
+        this.features.save(attrs);
       },
 
       requestMemberList(affiliation) {
@@ -67230,7 +67248,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
             this.trigger('configurationNeeded');
             return; // We haven't yet entered the groupchat, so bail here.
           }
-        } else if (!this.get('features_fetched')) {
+        } else if (!this.features.get('fetched')) {
           // The features for this groupchat weren't fetched.
           // That must mean it's a new groupchat without locking
           // (in which case Prosody doesn't send a 201 status),
@@ -92568,7 +92586,7 @@ __e(o.num_occupants) +
 '</p>\n                    <p class="room-info"><strong>' +
 __e(o.__('Features')) +
 '</strong>:\n                        <div class="chatroom-features">\n                        <ul class="features-list">\n                        ';
- if (o.passwordprotected) { ;
+ if (o.features.passwordprotected) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-lock"></span>' +
 __e( o.__('Password protected') ) +
 ' - <em>' +
@@ -92576,7 +92594,7 @@ __e( o.__('This groupchat requires a password before entry') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.unsecured) { ;
+ if (o.features.unsecured) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-unlock"></span>' +
 __e( o.__('No password required') ) +
 ' - <em>' +
@@ -92584,7 +92602,7 @@ __e( o.__('This groupchat does not require a password upon entry') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.hidden) { ;
+ if (o.features.hidden) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-eye-slash"></span>' +
 __e( o.__('Hidden') ) +
 ' - <em>' +
@@ -92592,7 +92610,7 @@ __e( o.__('This groupchat is not publicly searchable') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.public_room) { ;
+ if (o.features.public_room) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-eye"></span>' +
 __e( o.__('Public') ) +
 ' - <em>' +
@@ -92600,7 +92618,7 @@ __e( o.__('This groupchat is publicly searchable') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.membersonly) { ;
+ if (o.features.membersonly) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-address-book"></span>' +
 __e( o.__('Members only') ) +
 ' - <em>' +
@@ -92608,7 +92626,7 @@ __e( o.__('This groupchat is restricted to members only') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.open) { ;
+ if (o.features.open) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-globe"></span>' +
 __e( o.__('Open') ) +
 ' - <em>' +
@@ -92616,7 +92634,7 @@ __e( o.__('Anyone can join this groupchat') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.persistent) { ;
+ if (o.features.persistent) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-save"></span>' +
 __e( o.__('Persistent') ) +
 ' - <em>' +
@@ -92624,7 +92642,7 @@ __e( o.__('This groupchat persists even if it\'s unoccupied') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.temporary) { ;
+ if (o.features.temporary) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-snowflake-o"></span>' +
 __e( o.__('Temporary') ) +
 ' - <em>' +
@@ -92632,7 +92650,7 @@ __e( o.__('This groupchat will disappear once the last person leaves') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.nonanonymous) { ;
+ if (o.features.nonanonymous) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-id-card"></span>' +
 __e( o.__('Not anonymous') ) +
 ' - <em>' +
@@ -92640,7 +92658,7 @@ __e( o.__('All other groupchat participants can see your XMPP username') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.semianonymous) { ;
+ if (o.features.semianonymous) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-user-secret"></span>' +
 __e( o.__('Semi-anonymous') ) +
 ' - <em>' +
@@ -92648,7 +92666,7 @@ __e( o.__('Only moderators can see your XMPP username') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.moderated) { ;
+ if (o.features.moderated) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-gavel"></span>' +
 __e( o.__('Moderated') ) +
 ' - <em>' +
@@ -92656,7 +92674,7 @@ __e( o.__('Participants entering this groupchat need to request permission to wr
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.unmoderated) { ;
+ if (o.features.unmoderated) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-info-circle"></span>' +
 __e( o.__('Not moderated') ) +
 ' - <em>' +
@@ -92664,7 +92682,7 @@ __e( o.__('Participants entering this groupchat can write right away') ) +
 '</em></li>\n                        ';
  } ;
 __p += '\n                        ';
- if (o.mam_enabled) { ;
+ if (o.features.mam_enabled) { ;
 __p += '\n                        <li class="feature" ><span class="fa fa-database"></span>' +
 __e( o.__('Message archiving') ) +
 ' - <em>' +
@@ -92715,13 +92733,9 @@ var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./no
 module.exports = function(o) {
 var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
 function print() { __p += __j.call(arguments, '') }
-__p += '<!-- src/templates/chatroom_features.html -->\n';
- if (o.has_features) { ;
-__p += '\n<p class="occupants-heading">' +
+__p += '<!-- src/templates/chatroom_features.html -->\n<p class="occupants-heading">' +
 __e(o.__('Features')) +
-'</p>\n';
- } ;
-__p += '\n<ul class="features-list">\n';
+'</p>\n<ul class="features-list">\n';
  if (o.passwordprotected) { ;
 __p += '\n<li class="feature" title="' +
 __e( o.__('This groupchat requires a password before entry') ) +
@@ -92781,7 +92795,7 @@ __p += '\n';
  if (o.temporary) { ;
 __p += '\n<li class="feature" title="' +
 __e( o.__('This groupchat will disappear once the last person leaves') ) +
-'"><span class="fa fa-snowflake-o"></span>' +
+'"><span class="fa fa-snowflake"></span>' +
 __e( o.__('Temporary') ) +
 '</li>\n';
  } ;

+ 38 - 39
spec/chatroom.js

@@ -2141,13 +2141,13 @@
                 let view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
                 await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
                 view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
-                expect(view.model.get('features_fetched')).toBeTruthy();
-                expect(view.model.get('passwordprotected')).toBe(true);
-                expect(view.model.get('hidden')).toBe(true);
-                expect(view.model.get('temporary')).toBe(true);
-                expect(view.model.get('open')).toBe(true);
-                expect(view.model.get('unmoderated')).toBe(true);
-                expect(view.model.get('nonanonymous')).toBe(true);
+                expect(view.model.features.get('fetched')).toBeTruthy();
+                expect(view.model.features.get('passwordprotected')).toBe(true);
+                expect(view.model.features.get('hidden')).toBe(true);
+                expect(view.model.features.get('temporary')).toBe(true);
+                expect(view.model.features.get('open')).toBe(true);
+                expect(view.model.features.get('unmoderated')).toBe(true);
+                expect(view.model.features.get('nonanonymous')).toBe(true);
                 done();
             }));
 
@@ -2172,19 +2172,19 @@
                 let features_list = chatroomview.el.querySelector('.features-list');
                 let features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s);
                 expect(_.difference(["Password protected", "Open", "Temporary", "Not anonymous", "Not moderated"], features_shown).length).toBe(0);
-                expect(chatroomview.model.get('hidden')).toBe(false);
-                expect(chatroomview.model.get('mam_enabled')).toBe(false);
-                expect(chatroomview.model.get('membersonly')).toBe(false);
-                expect(chatroomview.model.get('moderated')).toBe(false);
-                expect(chatroomview.model.get('nonanonymous')).toBe(true);
-                expect(chatroomview.model.get('open')).toBe(true);
-                expect(chatroomview.model.get('passwordprotected')).toBe(true);
-                expect(chatroomview.model.get('persistent')).toBe(false);
-                expect(chatroomview.model.get('publicroom')).toBe(true);
-                expect(chatroomview.model.get('semianonymous')).toBe(false);
-                expect(chatroomview.model.get('temporary')).toBe(true);
-                expect(chatroomview.model.get('unmoderated')).toBe(true);
-                expect(chatroomview.model.get('unsecured')).toBe(false);
+                expect(chatroomview.model.features.get('hidden')).toBe(false);
+                expect(chatroomview.model.features.get('mam_enabled')).toBe(false);
+                expect(chatroomview.model.features.get('membersonly')).toBe(false);
+                expect(chatroomview.model.features.get('moderated')).toBe(false);
+                expect(chatroomview.model.features.get('nonanonymous')).toBe(true);
+                expect(chatroomview.model.features.get('open')).toBe(true);
+                expect(chatroomview.model.features.get('passwordprotected')).toBe(true);
+                expect(chatroomview.model.features.get('persistent')).toBe(false);
+                expect(chatroomview.model.features.get('publicroom')).toBe(true);
+                expect(chatroomview.model.features.get('semianonymous')).toBe(false);
+                expect(chatroomview.model.features.get('temporary')).toBe(true);
+                expect(chatroomview.model.features.get('unmoderated')).toBe(true);
+                expect(chatroomview.model.features.get('unsecured')).toBe(false);
                 expect(chatroomview.el.querySelector('.chat-title').textContent.trim()).toBe('Room');
 
                 chatroomview.el.querySelector('.configure-chatroom-button').click();
@@ -2304,7 +2304,7 @@
                     'muc_passwordprotected',
                     'muc_hidden',
                     'muc_temporary',
-                    'muc_open',
+                    'muc_membersonly',
                     'muc_unmoderated',
                     'muc_nonanonymous'
                 ];
@@ -2316,27 +2316,26 @@
                         .c('value').t('This is the description').up().up()
                     .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
                         .c('value').t(0);
-                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
 
-                spyOn(chatroomview.occupantsview, 'renderRoomFeatures').and.callThrough();
+                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
 
-                await test_utils.waitUntil(() => chatroomview.occupantsview.renderRoomFeatures.calls.count());
+                await test_utils.waitUntil(() => new Promise(success => chatroomview.model.features.on('change', success)));
                 features_list = chatroomview.el.querySelector('.features-list');
                 features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s);
-                expect(_.difference(["Password protected", "Hidden", "Open", "Temporary", "Not anonymous", "Not moderated"], features_shown).length).toBe(0);
-                expect(chatroomview.model.get('hidden')).toBe(true);
-                expect(chatroomview.model.get('mam_enabled')).toBe(false);
-                expect(chatroomview.model.get('membersonly')).toBe(false);
-                expect(chatroomview.model.get('moderated')).toBe(false);
-                expect(chatroomview.model.get('nonanonymous')).toBe(true);
-                expect(chatroomview.model.get('open')).toBe(true);
-                expect(chatroomview.model.get('passwordprotected')).toBe(true);
-                expect(chatroomview.model.get('persistent')).toBe(false);
-                expect(chatroomview.model.get('publicroom')).toBe(false);
-                expect(chatroomview.model.get('semianonymous')).toBe(false);
-                expect(chatroomview.model.get('temporary')).toBe(true);
-                expect(chatroomview.model.get('unmoderated')).toBe(true);
-                expect(chatroomview.model.get('unsecured')).toBe(false);
+                expect(_.difference(["Password protected", "Hidden", "Members only", "Temporary", "Not anonymous", "Not moderated"], features_shown).length).toBe(0);
+                expect(chatroomview.model.features.get('hidden')).toBe(true);
+                expect(chatroomview.model.features.get('mam_enabled')).toBe(false);
+                expect(chatroomview.model.features.get('membersonly')).toBe(true);
+                expect(chatroomview.model.features.get('moderated')).toBe(false);
+                expect(chatroomview.model.features.get('nonanonymous')).toBe(true);
+                expect(chatroomview.model.features.get('open')).toBe(false);
+                expect(chatroomview.model.features.get('passwordprotected')).toBe(true);
+                expect(chatroomview.model.features.get('persistent')).toBe(false);
+                expect(chatroomview.model.features.get('publicroom')).toBe(false);
+                expect(chatroomview.model.features.get('semianonymous')).toBe(false);
+                expect(chatroomview.model.features.get('temporary')).toBe(true);
+                expect(chatroomview.model.features.get('unmoderated')).toBe(true);
+                expect(chatroomview.model.features.get('unsecured')).toBe(false);
                 expect(chatroomview.el.querySelector('.chat-title').textContent.trim()).toBe('New room name');
                 done();
             }));
@@ -3708,7 +3707,7 @@
                         .c('feature', {'var': 'muc_membersonly'}).up();
                 _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
                 await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
-                expect(view.model.get('membersonly')).toBeTruthy();
+                expect(view.model.features.get('membersonly')).toBeTruthy();
 
                 test_utils.createContacts(_converse, 'current');
 

+ 4 - 4
spec/omemo.js

@@ -1254,13 +1254,13 @@
 
             // Test that the button gets disabled when the room becomes
             // anonymous or semi-anonymous
-            view.model.save({'nonanonymous': false, 'semianonymous': true});
+            view.model.features.save({'nonanonymous': false, 'semianonymous': true});
             await test_utils.waitUntil(() => !view.model.get('omemo_supported'));
             toggle = toolbar.querySelector('.toggle-omemo');
             expect(_.isNull(toggle)).toBe(true);
             expect(view.model.get('omemo_supported')).toBe(false);
 
-            view.model.save({'nonanonymous': true, 'semianonymous': false});
+            view.model.features.save({'nonanonymous': true, 'semianonymous': false});
             await test_utils.waitUntil(() => view.model.get('omemo_supported'));
             toggle = toolbar.querySelector('.toggle-omemo');
             expect(_.isNull(toggle)).toBe(false);
@@ -1269,12 +1269,12 @@
             expect(u.hasClass('disabled', toggle)).toBe(false);
 
             // Test that the button gets disabled when the room becomes open
-            view.model.save({'membersonly': false, 'open': true});
+            view.model.features.save({'membersonly': false, 'open': true});
             await test_utils.waitUntil(() => !view.model.get('omemo_supported'));
             toggle = toolbar.querySelector('.toggle-omemo');
             expect(_.isNull(toggle)).toBe(true);
 
-            view.model.save({'membersonly': true, 'open': false});
+            view.model.features.save({'membersonly': true, 'open': false});
             await test_utils.waitUntil(() => view.model.get('omemo_supported'));
             toggle = toolbar.querySelector('.toggle-omemo');
             expect(_.isNull(toggle)).toBe(false);

+ 14 - 19
src/converse-muc-views.js

@@ -236,7 +236,7 @@ converse.plugins.add('converse-muc-views', {
         function toggleRoomInfo (ev) {
             /* Show/hide extra information about a groupchat in a listing. */
             const parent_el = u.ancestor(ev.target, '.room-item'),
-                    div_el = parent_el.querySelector('div.room-info');
+                  div_el = parent_el.querySelector('div.room-info');
             if (div_el) {
                 u.slideIn(div_el).then(u.removeElement)
                 parent_el.querySelector('a.room-info').classList.remove('selected');
@@ -439,9 +439,10 @@ converse.plugins.add('converse-muc-views', {
                     this.model.toJSON(), {
                         '_': _,
                         '__': __,
-                        'topic': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})),
                         'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),
-                        'num_occupants': this.model.occupants.length
+                        'features': this.model.features.toJSON(),
+                        'num_occupants': this.model.occupants.length,
+                        'topic': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}}))
                     })
                 );
             }
@@ -1831,12 +1832,7 @@ converse.plugins.add('converse-muc-views', {
                 this.chatroomview = this.model.chatroomview;
                 this.chatroomview.model.on('change:open', this.renderInviteWidget, this);
                 this.chatroomview.model.on('change:affiliation', this.renderInviteWidget, this);
-                this.chatroomview.model.on('change', () => {
-                    if (_.intersection(converse.ROOM_FEATURES, Object.keys(this.chatroomview.model.changed)).length === 0) {
-                        return;
-                    }
-                    this.renderRoomFeatures();
-                }, this);
+                this.chatroomview.model.features.on('change', this.renderRoomFeatures, this);
 
                 this.render();
                 this.model.fetch({
@@ -1882,16 +1878,15 @@ converse.plugins.add('converse-muc-views', {
             },
 
             renderRoomFeatures () {
-                const picks = _.pick(this.chatroomview.model.attributes, converse.ROOM_FEATURES),
-                    iteratee = (a, v) => a || v,
-                    el = this.el.querySelector('.chatroom-features');
-
-                el.innerHTML = tpl_chatroom_features(
-                        _.extend(this.chatroomview.model.toJSON(), {
-                            '__': __,
-                            'has_features': _.reduce(_.values(picks), iteratee)
-                        }));
-                this.setOccupantsHeight();
+                const features = this.chatroomview.model.features,
+                      picks = _.pick(features.attributes, converse.ROOM_FEATURES),
+                      iteratee = (a, v) => a || v;
+
+                if (_.reduce(_.values(picks), iteratee)) {
+                    const el = this.el.querySelector('.chatroom-features');
+                    el.innerHTML = tpl_chatroom_features(_.extend(features.toJSON(), {__}));
+                    this.setOccupantsHeight();
+                }
                 return this;
             },
 

+ 4 - 5
src/converse-omemo.js

@@ -446,7 +446,7 @@ converse.plugins.add('converse-omemo', {
             },
 
             renderOMEMOToolbarButton () {
-                if (this.model.get('membersonly') && this.model.get('nonanonymous')) {
+                if (this.model.features.get('membersonly') && this.model.features.get('nonanonymous')) {
                     this.__super__.renderOMEMOToolbarButton.apply(arguments);
                 } else {
                     const icon = this.el.querySelector('.toggle-omemo');
@@ -1137,7 +1137,7 @@ converse.plugins.add('converse-omemo', {
         }
 
         async function onOccupantAdded (chatroom, occupant) {
-            if (occupant.isSelf() || !chatroom.get('nonanonymous') || !chatroom.get('membersonly')) {
+            if (occupant.isSelf() || !chatroom.features.get('nonanonymous') || !chatroom.features.get('membersonly')) {
                 return;
             }
             if (chatroom.get('omemo_active')) {
@@ -1157,7 +1157,7 @@ converse.plugins.add('converse-omemo', {
             let supported;
             if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
                 await _converse.api.waitUntil('OMEMOInitialized');
-                supported = chatbox.get('nonanonymous') && chatbox.get('membersonly');
+                supported = chatbox.features.get('nonanonymous') && chatbox.features.get('membersonly');
             } else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
                 supported = await _converse.contactHasOMEMOSupport(chatbox.get('jid'));
             }
@@ -1169,8 +1169,7 @@ converse.plugins.add('converse-omemo', {
                 checkOMEMOSupported(chatbox);
                 if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
                     chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o));
-                    chatbox.on('change:nonanonymous', checkOMEMOSupported);
-                    chatbox.on('change:membersonly', checkOMEMOSupported);
+                    chatbox.features.on('change', () => checkOMEMOSupported(chatbox));
                 }
             })
         );

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

@@ -795,7 +795,7 @@ _converse.initialize = function (settings, callback) {
 
 
     this.initSession = function () {
-        const id = b64_sha1('converse.bosh-session');
+        const id = 'converse.bosh-session';
         _converse.session = new Backbone.Model({id});
         _converse.session.browserStorage = new Backbone.BrowserStorage.session(id);
         _converse.session.fetch();

+ 29 - 13
src/headless/converse-muc.js

@@ -159,9 +159,7 @@ converse.plugins.add('converse-muc', {
 
             defaults () {
                 return _.assign(
-                    _.clone(_converse.ChatBox.prototype.defaults),
-                    _.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)),
-                    {
+                    _.clone(_converse.ChatBox.prototype.defaults), {
                       // For group chats, we distinguish between generally unread
                       // messages and those ones that specifically mention the
                       // user.
@@ -177,7 +175,6 @@ converse.plugins.add('converse-muc', {
                       'name': '',
                       'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
                       'description': '',
-                      'features_fetched': false,
                       'roomconfig': {},
                       'type': _converse.CHATROOMS_TYPE,
                       'message_type': 'groupchat'
@@ -189,6 +186,14 @@ converse.plugins.add('converse-muc', {
                 this.constructor.__super__.initialize.apply(this, arguments);
                 this.on('change:connection_status', this.onConnectionStatusChanged, this);
 
+                const storage = _converse.config.get('storage');
+                const id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
+                this.features = new Backbone.Model(
+                    _.assign({id}, _.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)))
+                );
+                this.features.browserStorage = new Backbone.BrowserStorage.session(id);
+                this.features.fetch();
+
                 this.occupants = new _converse.ChatRoomOccupants();
                 this.occupants.browserStorage = new Backbone.BrowserStorage.session(
                     b64_sha1(`converse.occupants-${_converse.bare_jid}${this.get('jid')}`)
@@ -304,6 +309,7 @@ converse.plugins.add('converse-muc', {
                  *  (String) exit_msg: Optional message to indicate your
                  *      reason for leaving.
                  */
+                this.features.destroy();
                 this.occupants.browserStorage._clear();
                 this.occupants.reset();
                 if (_converse.disco_entities) {
@@ -496,14 +502,25 @@ converse.plugins.add('converse-muc', {
                 return this.getRoomFeatures();
             },
 
+            async getRoomIdentity () {
+                const [identity, fields] = await Promise.all([
+                    _converse.api.disco.getIdentity('conference', 'text', this.get('jid')),
+                    _converse.api.disco.getFields(this.get('jid'))
+                ]);
+                this.save({
+                    'name': identity && identity.get('name'),
+                    'description': _.get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value')
+                });
+            },
+
             async getRoomFeatures () {
+                // XXX: not sure whet the right place is to get the room identitiy
+                this.getRoomIdentity();
                 const features = await _converse.api.disco.getFeatures(this.get('jid')),
-                      fields = await _converse.api.disco.getFields(this.get('jid')),
-                      identity = await _converse.api.disco.getIdentity('conference', 'text', this.get('jid')),
-                      attrs = _.extend(_.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)), {
-                            'features_fetched': moment().format(),
-                            'name': identity && identity.get('name')
-                      });
+                      attrs = _.extend(
+                            _.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)),
+                            {'fetched': moment().format()}
+                      );
 
                 features.each(feature => {
                     const fieldname = feature.get('var');
@@ -515,8 +532,7 @@ converse.plugins.add('converse-muc', {
                     }
                     attrs[fieldname.replace('muc_', '')] = true;
                 });
-                attrs.description = _.get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value');
-                this.save(attrs);
+                this.features.save(attrs);
             },
 
             requestMemberList (affiliation) {
@@ -1053,7 +1069,7 @@ converse.plugins.add('converse-muc', {
                         this.trigger('configurationNeeded');
                         return; // We haven't yet entered the groupchat, so bail here.
                     }
-                } else if (!this.get('features_fetched')) {
+                } else if (!this.features.get('fetched')) {
                     // The features for this groupchat weren't fetched.
                     // That must mean it's a new groupchat without locking
                     // (in which case Prosody doesn't send a 201 status),

+ 13 - 13
src/templates/chatroom_details_modal.html

@@ -18,43 +18,43 @@
                     <p class="room-info"><strong>{{{o.__('Features')}}}</strong>:
                         <div class="chatroom-features">
                         <ul class="features-list">
-                        {[ if (o.passwordprotected) { ]}
+                        {[ if (o.features.passwordprotected) { ]}
                         <li class="feature" ><span class="fa fa-lock"></span>{{{ o.__('Password protected') }}} - <em>{{{ o.__('This groupchat requires a password before entry') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.unsecured) { ]}
+                        {[ if (o.features.unsecured) { ]}
                         <li class="feature" ><span class="fa fa-unlock"></span>{{{ o.__('No password required') }}} - <em>{{{ o.__('This groupchat does not require a password upon entry') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.hidden) { ]}
+                        {[ if (o.features.hidden) { ]}
                         <li class="feature" ><span class="fa fa-eye-slash"></span>{{{ o.__('Hidden') }}} - <em>{{{ o.__('This groupchat is not publicly searchable') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.public_room) { ]}
+                        {[ if (o.features.public_room) { ]}
                         <li class="feature" ><span class="fa fa-eye"></span>{{{ o.__('Public') }}} - <em>{{{ o.__('This groupchat is publicly searchable') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.membersonly) { ]}
+                        {[ if (o.features.membersonly) { ]}
                         <li class="feature" ><span class="fa fa-address-book"></span>{{{ o.__('Members only') }}} - <em>{{{ o.__('This groupchat is restricted to members only') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.open) { ]}
+                        {[ if (o.features.open) { ]}
                         <li class="feature" ><span class="fa fa-globe"></span>{{{ o.__('Open') }}} - <em>{{{ o.__('Anyone can join this groupchat') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.persistent) { ]}
+                        {[ if (o.features.persistent) { ]}
                         <li class="feature" ><span class="fa fa-save"></span>{{{ o.__('Persistent') }}} - <em>{{{ o.__('This groupchat persists even if it\'s unoccupied') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.temporary) { ]}
+                        {[ if (o.features.temporary) { ]}
                         <li class="feature" ><span class="fa fa-snowflake-o"></span>{{{ o.__('Temporary') }}} - <em>{{{ o.__('This groupchat will disappear once the last person leaves') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.nonanonymous) { ]}
+                        {[ if (o.features.nonanonymous) { ]}
                         <li class="feature" ><span class="fa fa-id-card"></span>{{{ o.__('Not anonymous') }}} - <em>{{{ o.__('All other groupchat participants can see your XMPP username') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.semianonymous) { ]}
+                        {[ if (o.features.semianonymous) { ]}
                         <li class="feature" ><span class="fa fa-user-secret"></span>{{{ o.__('Semi-anonymous') }}} - <em>{{{ o.__('Only moderators can see your XMPP username') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.moderated) { ]}
+                        {[ if (o.features.moderated) { ]}
                         <li class="feature" ><span class="fa fa-gavel"></span>{{{ o.__('Moderated') }}} - <em>{{{ o.__('Participants entering this groupchat need to request permission to write') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.unmoderated) { ]}
+                        {[ if (o.features.unmoderated) { ]}
                         <li class="feature" ><span class="fa fa-info-circle"></span>{{{ o.__('Not moderated') }}} - <em>{{{ o.__('Participants entering this groupchat can write right away') }}}</em></li>
                         {[ } ]}
-                        {[ if (o.mam_enabled) { ]}
+                        {[ if (o.features.mam_enabled) { ]}
                         <li class="feature" ><span class="fa fa-database"></span>{{{ o.__('Message archiving') }}} - <em>{{{ o.__('Messages are archived on the server') }}}</em></li>
                         {[ } ]}
                         </ul>

+ 1 - 3
src/templates/chatroom_features.html

@@ -1,6 +1,4 @@
-{[ if (o.has_features) { ]}
 <p class="occupants-heading">{{{o.__('Features')}}}</p>
-{[ } ]}
 <ul class="features-list">
 {[ if (o.passwordprotected) { ]}
 <li class="feature" title="{{{ o.__('This groupchat requires a password before entry') }}}"><span class="fa fa-lock"></span>{{{ o.__('Password protected') }}}</li>
@@ -24,7 +22,7 @@
 <li class="feature" title="{{{ o.__('This groupchat persists even if it\'s unoccupied') }}}"><span class="fa fa-save"></span>{{{ o.__('Persistent') }}}</li>
 {[ } ]}
 {[ if (o.temporary) { ]}
-<li class="feature" title="{{{ o.__('This groupchat will disappear once the last person leaves') }}}"><span class="fa fa-snowflake-o"></span>{{{ o.__('Temporary') }}}</li>
+<li class="feature" title="{{{ o.__('This groupchat will disappear once the last person leaves') }}}"><span class="fa fa-snowflake"></span>{{{ o.__('Temporary') }}}</li>
 {[ } ]}
 {[ if (o.nonanonymous) { ]}
 <li class="feature" title="{{{ o.__('All other groupchat participants can see your XMPP username') }}}"><span class="fa fa-id-card"></span>{{{ o.__('Not anonymous') }}}</li>