Selaa lähdekoodia

Refactor initialization and defaults for chat boxes

- Let box_id start with char for valid HTML.
- No need to use SHA1 for box id
- No need for the user_id attribute.
- Set nickname when we set the roster contact.

Also...

- _converse.api.contacts.get is now async
- _converse.api.chats.create is now async
JC Brand 6 vuotta sitten
vanhempi
commit
725a382e3b

+ 1 - 0
CHANGES.md

@@ -17,6 +17,7 @@
   this was the default behavior.
   this was the default behavior.
 - `_converse.api.emit` has been removed in favor of [\_converse.api.trigger](https://conversejs.org/docs/html/api/-_converse.api.html#.trigger)
 - `_converse.api.emit` has been removed in favor of [\_converse.api.trigger](https://conversejs.org/docs/html/api/-_converse.api.html#.trigger)
 - `_converse.updateSettings` has been removed in favor of [\_converse.api.settings.update](https://conversejs.org/docs/html/api/-_converse.api.settings.html#.update)
 - `_converse.updateSettings` has been removed in favor of [\_converse.api.settings.update](https://conversejs.org/docs/html/api/-_converse.api.settings.html#.update)
+- `_converse.api.roster.get` now returns a promise.
 
 
 ## 4.2.0 (2019-04-04)
 ## 4.2.0 (2019-04-04)
 
 

+ 159 - 154
dist/converse.js

@@ -42258,6 +42258,7 @@ Strophe.Websocket.prototype = {
 });
 });
 //# sourceMappingURL=strophe.js.map
 //# sourceMappingURL=strophe.js.map
 
 
+
 /***/ }),
 /***/ }),
 
 
 /***/ "./node_modules/strophejs-plugin-ping/strophe.ping.js":
 /***/ "./node_modules/strophejs-plugin-ping/strophe.ping.js":
@@ -48638,19 +48639,17 @@ const AvatarMixin = {
       return;
       return;
     }
     }
 
 
-    const data = {
-      'classes': canvas_el.getAttribute('class'),
-      'width': canvas_el.width,
-      'height': canvas_el.height
-    };
-
     if (this.model.vcard) {
     if (this.model.vcard) {
+      const data = {
+        'classes': canvas_el.getAttribute('class'),
+        'width': canvas_el.width,
+        'height': canvas_el.height
+      };
       const image_type = this.model.vcard.get('image_type'),
       const image_type = this.model.vcard.get('image_type'),
             image = this.model.vcard.get('image');
             image = this.model.vcard.get('image');
       data['image'] = "data:" + image_type + ";base64," + image;
       data['image'] = "data:" + image_type + ";base64," + image;
+      canvas_el.outerHTML = templates_avatar_svg__WEBPACK_IMPORTED_MODULE_4___default()(data);
     }
     }
-
-    canvas_el.outerHTML = templates_avatar_svg__WEBPACK_IMPORTED_MODULE_4___default()(data);
   }
   }
 
 
 };
 };
@@ -48761,9 +48760,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins
         /* This method gets overridden in src/converse-controlbox.js if
         /* This method gets overridden in src/converse-controlbox.js if
          * the controlbox plugin is active.
          * the controlbox plugin is active.
          */
          */
-        this.each(function (view) {
-          view.close();
-        });
+        this.each(v => v.close());
         return this;
         return this;
       }
       }
 
 
@@ -49008,7 +49005,11 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
       initialize() {
       initialize() {
         this.model.on('change:status', this.onStatusMessageChanged, this);
         this.model.on('change:status', this.onStatusMessageChanged, this);
         this.debouncedRender = _.debounce(this.render, 50);
         this.debouncedRender = _.debounce(this.render, 50);
-        this.model.vcard.on('change', this.debouncedRender, this);
+
+        if (this.model.vcard) {
+          this.model.vcard.on('change', this.debouncedRender, this);
+        }
+
         this.model.on('rosterContactAdded', () => {
         this.model.on('rosterContactAdded', () => {
           this.model.contact.on('change:nickname', this.debouncedRender, this);
           this.model.contact.on('change:nickname', this.debouncedRender, this);
           this.debouncedRender();
           this.debouncedRender();
@@ -49016,7 +49017,10 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
       },
       },
 
 
       render() {
       render() {
-        this.el.innerHTML = templates_chatbox_head_html__WEBPACK_IMPORTED_MODULE_8___default()(_.extend(this.model.vcard.toJSON(), this.model.toJSON(), {
+        const vcard = _.get(this.model, 'vcard'),
+              vcard_json = vcard ? vcard.toJSON() : {};
+
+        this.el.innerHTML = templates_chatbox_head_html__WEBPACK_IMPORTED_MODULE_8___default()(_.extend(vcard_json, this.model.toJSON(), {
           '_converse': _converse,
           '_converse': _converse,
           'info_close': __('Close this chat box'),
           'info_close': __('Close this chat box'),
           'display_name': this.model.getDisplayName()
           'display_name': this.model.getDisplayName()
@@ -49053,7 +49057,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
       initialize() {
       initialize() {
         _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
         _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
 
 
-        this.model.on('contactAdded', this.registerContactEventHandlers, this);
+        this.model.on('rosterContactAdded', this.registerContactEventHandlers, this);
         this.model.on('change', this.render, this);
         this.model.on('change', this.render, this);
         this.registerContactEventHandlers();
         this.registerContactEventHandlers();
         /**
         /**
@@ -49067,7 +49071,10 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
       },
       },
 
 
       toHTML() {
       toHTML() {
-        return templates_user_details_modal_html__WEBPACK_IMPORTED_MODULE_20___default()(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), {
+        const vcard = _.get(this.model, 'vcard'),
+              vcard_json = vcard ? vcard.toJSON() : {};
+
+        return templates_user_details_modal_html__WEBPACK_IMPORTED_MODULE_20___default()(_.extend(this.model.toJSON(), vcard_json, {
           '_': _,
           '_': _,
           '__': __,
           '__': __,
           'view': this,
           'view': this,
@@ -50301,11 +50308,11 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
     });
     });
 
 
     _converse.api.listen.on('chatBoxViewsInitialized', () => {
     _converse.api.listen.on('chatBoxViewsInitialized', () => {
-      const that = _converse.chatboxviews;
+      const views = _converse.chatboxviews;
 
 
       _converse.chatboxes.on('add', item => {
       _converse.chatboxes.on('add', item => {
-        if (!that.get(item.get('id')) && item.get('type') === _converse.PRIVATE_CHAT_TYPE) {
-          that.add(item.get('id'), new _converse.ChatBoxView({
+        if (!views.get(item.get('id')) && item.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+          views.add(item.get('id'), new _converse.ChatBoxView({
             model: item
             model: item
           }));
           }));
         }
         }
@@ -50345,7 +50352,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
          */
          */
         'get'(jids) {
         'get'(jids) {
           if (_.isUndefined(jids)) {
           if (_.isUndefined(jids)) {
-            _converse.log("chats.create: You need to provide at least one JID", Strophe.LogLevel.ERROR);
+            _converse.log("chatviews.get: You need to provide at least one JID", Strophe.LogLevel.ERROR);
 
 
             return null;
             return null;
           }
           }
@@ -50595,20 +50602,17 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
     });
     });
 
 
     _converse.ControlBox = _converse.ChatBox.extend({
     _converse.ControlBox = _converse.ChatBox.extend({
-      defaults: {
-        'bookmarked': false,
-        'box_id': 'controlbox',
-        'chat_state': undefined,
-        'closed': !_converse.show_controlbox_by_default,
-        'num_unread': 0,
-        'type': _converse.CONTROLBOX_TYPE,
-        'url': ''
-      },
-
-      initialize() {
-        u.safeSave(this, {
-          'time_opened': this.get('time_opened') || moment().valueOf()
-        });
+      defaults() {
+        return {
+          'bookmarked': false,
+          'box_id': 'controlbox',
+          'chat_state': undefined,
+          'closed': !_converse.show_controlbox_by_default,
+          'num_unread': 0,
+          'time_opened': this.get('time_opened') || moment().valueOf(),
+          'type': _converse.CONTROLBOX_TYPE,
+          'url': ''
+        };
       }
       }
 
 
     });
     });
@@ -51720,6 +51724,7 @@ __webpack_require__.r(__webpack_exports__);
 
 
 const _converse$env = _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_1__["default"].env,
 const _converse$env = _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_1__["default"].env,
       _ = _converse$env._,
       _ = _converse$env._,
+      moment = _converse$env.moment,
       utils = _converse$env.utils;
       utils = _converse$env.utils;
 _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_1__["default"].plugins.add('converse-headline', {
 _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_1__["default"].plugins.add('converse-headline', {
   /* Plugin dependencies are other plugins which might be
   /* Plugin dependencies are other plugins which might be
@@ -51760,13 +51765,24 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_1__["default"].plugins
     const _converse = this._converse,
     const _converse = this._converse,
           __ = _converse.__;
           __ = _converse.__;
     _converse.HeadlinesBox = _converse.ChatBox.extend({
     _converse.HeadlinesBox = _converse.ChatBox.extend({
-      defaults: {
-        'type': _converse.HEADLINES_TYPE,
-        'bookmarked': false,
-        'chat_state': undefined,
-        'num_unread': 0,
-        'url': ''
+      defaults() {
+        return {
+          'bookmarked': false,
+          'hidden': _.includes(['mobile', 'fullscreen'], _converse.view_mode),
+          'message_type': 'headline',
+          'num_unread': 0,
+          'time_opened': this.get('time_opened') || moment().valueOf(),
+          'type': _converse.HEADLINES_TYPE
+        };
+      },
+
+      initialize() {
+        this.initMessages();
+        this.set({
+          'box_id': `box-${btoa(this.get('jid'))}`
+        });
       }
       }
+
     });
     });
     _converse.HeadlinesBoxView = _converse.ChatBoxView.extend({
     _converse.HeadlinesBoxView = _converse.ChatBoxView.extend({
       className: 'chatbox headlines',
       className: 'chatbox headlines',
@@ -51815,7 +51831,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_1__["default"].plugins
       if (utils.isHeadlineMessage(_converse, message)) {
       if (utils.isHeadlineMessage(_converse, message)) {
         const from_jid = message.getAttribute('from');
         const from_jid = message.getAttribute('from');
 
 
-        if (_.includes(from_jid, '@') && !_converse.api.contacts.get(from_jid) && !_converse.allow_non_roster_messaging) {
+        if (_.includes(from_jid, '@') && !_converse.roster.get(from_jid) && !_converse.allow_non_roster_messaging) {
           return;
           return;
         }
         }
 
 
@@ -51854,11 +51870,11 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_1__["default"].plugins
     _converse.api.listen.on('reconnected', registerHeadlineHandler);
     _converse.api.listen.on('reconnected', registerHeadlineHandler);
 
 
     _converse.api.listen.on('chatBoxViewsInitialized', () => {
     _converse.api.listen.on('chatBoxViewsInitialized', () => {
-      const that = _converse.chatboxviews;
+      const views = _converse.chatboxviews;
 
 
       _converse.chatboxes.on('add', item => {
       _converse.chatboxes.on('add', item => {
-        if (!that.get(item.get('id')) && item.get('type') === _converse.HEADLINES_TYPE) {
-          that.add(item.get('id'), new _converse.HeadlinesBoxView({
+        if (!views.get(item.get('id')) && item.get('type') === _converse.HEADLINES_TYPE) {
+          views.add(item.get('id'), new _converse.HeadlinesBoxView({
             model: item
             model: item
           }));
           }));
         }
         }
@@ -53404,10 +53420,11 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
 
 
     _converse.api.settings.update({
     _converse.api.settings.update({
       'auto_list_rooms': false,
       'auto_list_rooms': false,
+      'cache_muc_messages': true,
+      'locked_muc_domain': false,
+      'locked_muc_nickname': false,
       'muc_disable_moderator_commands': false,
       'muc_disable_moderator_commands': false,
       'muc_domain': undefined,
       'muc_domain': undefined,
-      'locked_muc_nickname': false,
-      'locked_muc_domain': false,
       'muc_show_join_leave': true,
       'muc_show_join_leave': true,
       'roomconfig_whitelist': [],
       'roomconfig_whitelist': [],
       'visible_toolbar_buttons': {
       'visible_toolbar_buttons': {
@@ -55566,7 +55583,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
       /* Upon a reconnection event from converse, join again
       /* Upon a reconnection event from converse, join again
        * all the open groupchats.
        * all the open groupchats.
        */
        */
-      _converse.chatboxviews.each(function (view) {
+      _converse.chatboxviews.each(view => {
         if (view.model.get('type') === _converse.CHATROOMS_TYPE) {
         if (view.model.get('type') === _converse.CHATROOMS_TYPE) {
           view.model.save('connection_status', _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].ROOMSTATUS.DISCONNECTED);
           view.model.save('connection_status', _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].ROOMSTATUS.DISCONNECTED);
           view.model.registerHandlers();
           view.model.registerHandlers();
@@ -61743,7 +61760,6 @@ const _converse$env = _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].env
       Backbone = _converse$env.Backbone,
       Backbone = _converse$env.Backbone,
       Promise = _converse$env.Promise,
       Promise = _converse$env.Promise,
       Strophe = _converse$env.Strophe,
       Strophe = _converse$env.Strophe,
-      b64_sha1 = _converse$env.b64_sha1,
       moment = _converse$env.moment,
       moment = _converse$env.moment,
       sizzle = _converse$env.sizzle,
       sizzle = _converse$env.sizzle,
       utils = _converse$env.utils,
       utils = _converse$env.utils,
@@ -61786,12 +61802,11 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
 
 
     const ModelWithContact = Backbone.Model.extend({
     const ModelWithContact = Backbone.Model.extend({
       async setRosterContact(jid) {
       async setRosterContact(jid) {
-        await _converse.api.waitUntil('rosterContactsFetched');
-
-        const contact = _converse.roster.get(jid);
+        const contact = await _converse.api.contacts.get(jid);
 
 
         if (contact) {
         if (contact) {
           this.contact = contact;
           this.contact = contact;
+          this.set('nickname', contact.get('nickname'));
           this.trigger('rosterContactAdded');
           this.trigger('rosterContactAdded');
         }
         }
       }
       }
@@ -62019,6 +62034,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           'message_type': 'chat',
           'message_type': 'chat',
           'nickname': undefined,
           'nickname': undefined,
           'num_unread': 0,
           'num_unread': 0,
+          'time_opened': this.get('time_opened') || moment().valueOf(),
           'type': _converse.PRIVATE_CHAT_TYPE,
           'type': _converse.PRIVATE_CHAT_TYPE,
           'url': ''
           'url': ''
         };
         };
@@ -62036,15 +62052,10 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           // This happens when the controlbox is in browser storage,
           // This happens when the controlbox is in browser storage,
           // but we're in embedded mode.
           // but we're in embedded mode.
           return;
           return;
-        } // XXX: this creates a dependency on converse-roster, which we
-        // probably shouldn't have here, so we should probably move
-        // ChatBox out of converse-chatboxes
-
+        }
 
 
-        this.presence = _converse.presences.findWhere({
-          'jid': jid
-        }) || _converse.presences.create({
-          'jid': jid
+        this.set({
+          'box_id': `box-${btoa(jid)}`
         });
         });
 
 
         if (_converse.vcards) {
         if (_converse.vcards) {
@@ -62056,31 +62067,30 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         }
         }
 
 
         if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
         if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+          this.presence = _converse.presences.findWhere({
+            'jid': jid
+          }) || _converse.presences.create({
+            'jid': jid
+          });
           this.setRosterContact(jid);
           this.setRosterContact(jid);
         }
         }
 
 
+        this.on('change:chat_state', this.sendChatState, this);
+        this.initMessages();
+      },
+
+      initMessages() {
         this.messages = new _converse.Messages();
         this.messages = new _converse.Messages();
 
 
         const storage = _converse.config.get('storage');
         const storage = _converse.config.get('storage');
 
 
-        this.messages.browserStorage = new Backbone.BrowserStorage[storage](b64_sha1(`converse.messages${jid}${_converse.bare_jid}`));
+        this.messages.browserStorage = new Backbone.BrowserStorage[storage](`converse.messages${this.get('jid')}${_converse.bare_jid}`);
         this.messages.chatbox = this;
         this.messages.chatbox = this;
         this.messages.on('change:upload', message => {
         this.messages.on('change:upload', message => {
           if (message.get('upload') === _converse.SUCCESS) {
           if (message.get('upload') === _converse.SUCCESS) {
             _converse.api.send(this.createMessageStanza(message));
             _converse.api.send(this.createMessageStanza(message));
           }
           }
         });
         });
-        this.on('change:chat_state', this.sendChatState, this); // Models get saved immediately after creation, so no need to
-        // call `save` here.
-
-        this.set({
-          // The chat_state will be set to ACTIVE once the chat box is opened
-          // and we listen for change:chat_state, so shouldn't set it to ACTIVE here.
-          'box_id': b64_sha1(this.get('jid')),
-          'time_opened': this.get('time_opened') || moment().valueOf(),
-          'user_id': Strophe.getNodeFromJid(this.get('jid')),
-          'nickname': _.get(_converse.api.contacts.get(this.get('jid')), 'attributes.nickname')
-        });
       },
       },
 
 
       validate(attrs, options) {
       validate(attrs, options) {
@@ -62812,8 +62822,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
         const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
               from_resource = Strophe.getResourceFromJid(from_jid),
               from_resource = Strophe.getResourceFromJid(from_jid),
               is_me = from_bare_jid === _converse.bare_jid;
               is_me = from_bare_jid === _converse.bare_jid;
-        let contact_jid,
-            is_roster_contact = false;
+        let contact_jid;
 
 
         if (is_me) {
         if (is_me) {
           // I am the sender, so this must be a forwarded message...
           // I am the sender, so this must be a forwarded message...
@@ -62824,17 +62833,18 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           contact_jid = Strophe.getBareJidFromJid(to_jid);
           contact_jid = Strophe.getBareJidFromJid(to_jid);
         } else {
         } else {
           contact_jid = from_bare_jid;
           contact_jid = from_bare_jid;
-          await _converse.api.waitUntil('rosterContactsFetched');
-          is_roster_contact = !_.isUndefined(_converse.roster.get(contact_jid));
+        }
 
 
-          if (!is_roster_contact && !_converse.allow_non_roster_messaging) {
-            return;
-          }
+        const contact = await _converse.api.contacts.get(contact_jid);
+        const is_roster_contact = !_.isUndefined(contact);
+
+        if (!is_me && !is_roster_contact && !_converse.allow_non_roster_messaging) {
+          return;
         } // Get chat box, but only create when the message has something to show to the user
         } // Get chat box, but only create when the message has something to show to the user
 
 
 
 
         const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0,
         const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0,
-              roster_nick = _.get(_converse.api.contacts.get(contact_jid), 'attributes.nickname'),
+              roster_nick = _.get(contact, 'attributes.nickname'),
               chatbox = this.getChatBox(contact_jid, {
               chatbox = this.getChatBox(contact_jid, {
           'nickname': roster_nick
           'nickname': roster_nick
         }, has_body);
         }, has_body);
@@ -62985,16 +62995,11 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
          * @param {string|string[]} jid|jids An jid or array of jids
          * @param {string|string[]} jid|jids An jid or array of jids
          * @param {object} [attrs] An object containing configuration attributes.
          * @param {object} [attrs] An object containing configuration attributes.
          */
          */
-        'create'(jids, attrs) {
-          if (_.isUndefined(jids)) {
-            _converse.log("chats.create: You need to provide at least one JID", Strophe.LogLevel.ERROR);
-
-            return null;
-          }
-
+        async create(jids, attrs) {
           if (_.isString(jids)) {
           if (_.isString(jids)) {
             if (attrs && !_.get(attrs, 'fullname')) {
             if (attrs && !_.get(attrs, 'fullname')) {
-              attrs.fullname = _.get(_converse.api.contacts.get(jids), 'attributes.fullname');
+              const contact = await _converse.api.contacts.get(jids);
+              attrs.fullname = _.get(contact, 'attributes.fullname');
             }
             }
 
 
             const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true);
             const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true);
@@ -63008,10 +63013,17 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
             return chatbox;
             return chatbox;
           }
           }
 
 
-          return _.map(jids, jid => {
-            attrs.fullname = _.get(_converse.api.contacts.get(jid), 'attributes.fullname');
-            return _converse.chatboxes.getChatBox(jid, attrs, true).maybeShow();
-          });
+          if (_.isArray(jids)) {
+            return Promise.all(jids.forEach(async jid => {
+              const contact = await _converse.api.contacts.get(jids);
+              attrs.fullname = _.get(contact, 'attributes.fullname');
+              return _converse.chatboxes.getChatBox(jid, attrs, true).maybeShow();
+            }));
+          }
+
+          _converse.log("chats.create: You need to provide at least one JID", Strophe.LogLevel.ERROR);
+
+          return null;
         },
         },
 
 
         /**
         /**
@@ -63020,6 +63032,8 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
          * @method _converse.api.chats.open
          * @method _converse.api.chats.open
          * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
          * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
          * @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
          * @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
+         * @param {Boolean} [attrs.minimized] - Should the chat be
+         *   created in minimized state.
          * @param {Boolean} [force=false] - By default, a minimized
          * @param {Boolean} [force=false] - By default, a minimized
          *   chat won't be maximized (in `overlayed` view mode) and in
          *   chat won't be maximized (in `overlayed` view mode) and in
          *   `fullscreen` view mode a newly opened chat won't replace
          *   `fullscreen` view mode a newly opened chat won't replace
@@ -63053,22 +63067,21 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
          *     }
          *     }
          * });
          * });
          */
          */
-        'open'(jids, attrs, force) {
-          return new Promise((resolve, reject) => {
-            Promise.all([_converse.api.waitUntil('rosterContactsFetched'), _converse.api.waitUntil('chatBoxesFetched')]).then(() => {
-              if (_.isUndefined(jids)) {
-                const err_msg = "chats.open: You need to provide at least one JID";
-
-                _converse.log(err_msg, Strophe.LogLevel.ERROR);
-
-                reject(new Error(err_msg));
-              } else if (_.isString(jids)) {
-                resolve(_converse.api.chats.create(jids, attrs).maybeShow(force));
-              } else {
-                resolve(_.map(jids, jid => _converse.api.chats.create(jid, attrs).maybeShow(force)));
-              }
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
-          });
+        async open(jids, attrs, force) {
+          await Promise.all([_converse.api.waitUntil('rosterContactsFetched'), _converse.api.waitUntil('chatBoxesFetched')]);
+
+          if (_.isString(jids)) {
+            const chat = await _converse.api.chats.create(jids, attrs);
+            return chat.maybeShow(force);
+          } else if (_.isArray(jids)) {
+            return Promise.all(jids.map(j => _converse.api.chats.create(j, attrs).then(c => c.maybeShow(force))));
+          }
+
+          const err_msg = "chats.open: You need to provide at least one JID";
+
+          _converse.log(err_msg, Strophe.LogLevel.ERROR);
+
+          throw new Error(err_msg);
         },
         },
 
 
         /**
         /**
@@ -63091,7 +63104,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
          * const models = _converse.api.chats.get();
          * const models = _converse.api.chats.get();
          *
          *
          */
          */
-        'get'(jids) {
+        get(jids) {
           if (_.isUndefined(jids)) {
           if (_.isUndefined(jids)) {
             const result = [];
             const result = [];
 
 
@@ -66506,7 +66519,6 @@ const _converse$env = _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].env
       $build = _converse$env.$build,
       $build = _converse$env.$build,
       $msg = _converse$env.$msg,
       $msg = _converse$env.$msg,
       $pres = _converse$env.$pres,
       $pres = _converse$env.$pres,
-      b64_sha1 = _converse$env.b64_sha1,
       sizzle = _converse$env.sizzle,
       sizzle = _converse$env.sizzle,
       f = _converse$env.f,
       f = _converse$env.f,
       moment = _converse$env.moment,
       moment = _converse$env.moment,
@@ -66647,7 +66659,6 @@ _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins.add('converse-muc
        */
        */
       settings.type = _converse.CHATROOMS_TYPE;
       settings.type = _converse.CHATROOMS_TYPE;
       settings.id = jid;
       settings.id = jid;
-      settings.box_id = b64_sha1(jid);
 
 
       const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
       const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
 
 
@@ -66665,7 +66676,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins.add('converse-muc
 
 
     _converse.ChatRoom = _converse.ChatBox.extend({
     _converse.ChatRoom = _converse.ChatBox.extend({
       defaults() {
       defaults() {
-        return _.assign(_.clone(_converse.ChatBox.prototype.defaults), {
+        return {
           // For group chats, we distinguish between generally unread
           // For group chats, we distinguish between generally unread
           // messages and those ones that specifically mention the
           // messages and those ones that specifically mention the
           // user.
           // user.
@@ -66676,19 +66687,33 @@ _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins.add('converse-muc
           // generally unread messages (which *includes* mentions!).
           // generally unread messages (which *includes* mentions!).
           'num_unread_general': 0,
           'num_unread_general': 0,
           'affiliation': null,
           'affiliation': null,
+          'bookmarked': false,
+          'chat_state': undefined,
           'connection_status': _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].ROOMSTATUS.DISCONNECTED,
           'connection_status': _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].ROOMSTATUS.DISCONNECTED,
+          'description': '',
+          'hidden': _.includes(['mobile', 'fullscreen'], _converse.view_mode),
+          'message_type': 'groupchat',
           'name': '',
           'name': '',
           'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
           'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
-          'description': '',
+          'num_unread': 0,
           'roomconfig': {},
           'roomconfig': {},
-          'type': _converse.CHATROOMS_TYPE,
-          'message_type': 'groupchat'
-        });
+          'time_opened': this.get('time_opened') || moment().valueOf(),
+          'type': _converse.CHATROOMS_TYPE
+        };
       },
       },
 
 
       initialize() {
       initialize() {
-        this.constructor.__super__.initialize.apply(this, arguments);
+        if (_converse.vcards) {
+          this.vcard = _converse.vcards.findWhere({
+            'jid': this.get('jid')
+          }) || _converse.vcards.create({
+            'jid': this.get('jid')
+          });
+        }
 
 
+        this.set('box_id', `box-${btoa(this.get('jid'))}`);
+        this.initMessages();
+        this.on('change:chat_state', this.sendChatState, this);
         this.on('change:connection_status', this.onConnectionStatusChanged, this);
         this.on('change:connection_status', this.onConnectionStatusChanged, this);
 
 
         const storage = _converse.config.get('storage');
         const storage = _converse.config.get('storage');
@@ -68056,7 +68081,6 @@ _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins.add('converse-muc
       jid = jid.toLowerCase();
       jid = jid.toLowerCase();
       attrs.type = _converse.CHATROOMS_TYPE;
       attrs.type = _converse.CHATROOMS_TYPE;
       attrs.id = jid;
       attrs.id = jid;
-      attrs.box_id = b64_sha1(jid);
       return _converse.chatboxes.getChatBox(jid, attrs, create);
       return _converse.chatboxes.getChatBox(jid, attrs, create);
     };
     };
 
 
@@ -69607,28 +69631,6 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
     /********** Event Handlers *************/
     /********** Event Handlers *************/
 
 
 
 
-    function addRelatedContactToChatbox(chatbox, contact) {
-      if (!_.isUndefined(contact)) {
-        chatbox.contact = contact;
-        chatbox.trigger('contactAdded', contact);
-      }
-    }
-
-    _converse.api.waitUntil('rosterContactsFetched').then(() => {
-      _converse.roster.on('add', contact => {
-        /* When a new contact is added, check if we already have a
-         * chatbox open for it, and if so attach it to the chatbox.
-         */
-        const chatbox = _converse.chatboxes.findWhere({
-          'jid': contact.get('jid')
-        });
-
-        if (chatbox) {
-          addRelatedContactToChatbox(chatbox, contact);
-        }
-      });
-    });
-
     function updateUnreadCounter(chatbox) {
     function updateUnreadCounter(chatbox) {
       const contact = _converse.roster.findWhere({
       const contact = _converse.roster.findWhere({
         'jid': chatbox.get('jid')
         'jid': chatbox.get('jid')
@@ -69644,11 +69646,10 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
     _converse.api.listen.on('chatBoxesInitialized', () => {
     _converse.api.listen.on('chatBoxesInitialized', () => {
       _converse.chatboxes.on('change:num_unread', updateUnreadCounter);
       _converse.chatboxes.on('change:num_unread', updateUnreadCounter);
 
 
-      _converse.chatboxes.on('add', async chatbox => {
-        await _converse.api.waitUntil('rosterContactsFetched');
-        addRelatedContactToChatbox(chatbox, _converse.roster.findWhere({
-          'jid': chatbox.get('jid')
-        }));
+      _converse.chatboxes.on('add', chatbox => {
+        if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+          chatbox.setRosterContact(chatbox.get('jid'));
+        }
       });
       });
     });
     });
 
 
@@ -69664,7 +69665,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
         });
         });
 
 
         if (chatbox) {
         if (chatbox) {
-          addRelatedContactToChatbox(chatbox, contact);
+          chatbox.setRosterContact(contact.get('jid'));
         }
         }
       });
       });
     });
     });
@@ -69751,20 +69752,20 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
          * @method _converse.api.contacts.get
          * @method _converse.api.contacts.get
          * @params {(string[]|string)} jid|jids The JID or JIDs of
          * @params {(string[]|string)} jid|jids The JID or JIDs of
          *      the contacts to be returned.
          *      the contacts to be returned.
-         * @returns {(RosterContact[]|RosterContact)} [Backbone.Model](http://backbonejs.org/#Model)
-         *      (or an array of them) representing the contact.
+         * @returns {promise} Promise which resolves with the
+         *  _converse.RosterContact (or an array of them) representing the contact.
          *
          *
          * @example
          * @example
          * // Fetch a single contact
          * // Fetch a single contact
          * _converse.api.listen.on('rosterContactsFetched', function () {
          * _converse.api.listen.on('rosterContactsFetched', function () {
-         *     const contact = _converse.api.contacts.get('buddy@example.com')
+         *     const contact = await _converse.api.contacts.get('buddy@example.com')
          *     // ...
          *     // ...
          * });
          * });
          *
          *
          * @example
          * @example
          * // To get multiple contacts, pass in an array of JIDs:
          * // To get multiple contacts, pass in an array of JIDs:
          * _converse.api.listen.on('rosterContactsFetched', function () {
          * _converse.api.listen.on('rosterContactsFetched', function () {
-         *     const contacts = _converse.api.contacts.get(
+         *     const contacts = await _converse.api.contacts.get(
          *         ['buddy1@example.com', 'buddy2@example.com']
          *         ['buddy1@example.com', 'buddy2@example.com']
          *     )
          *     )
          *     // ...
          *     // ...
@@ -69773,14 +69774,14 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
          * @example
          * @example
          * // To return all contacts, simply call ``get`` without any parameters:
          * // To return all contacts, simply call ``get`` without any parameters:
          * _converse.api.listen.on('rosterContactsFetched', function () {
          * _converse.api.listen.on('rosterContactsFetched', function () {
-         *     const contacts = _converse.api.contacts.get();
+         *     const contacts = await _converse.api.contacts.get();
          *     // ...
          *     // ...
          * });
          * });
          */
          */
-        'get'(jids) {
-          const _getter = function _getter(jid) {
-            return _converse.roster.get(Strophe.getBareJidFromJid(jid)) || null;
-          };
+        async get(jids) {
+          await _converse.api.waitUntil('rosterContactsFetched');
+
+          const _getter = jid => _converse.roster.get(Strophe.getBareJidFromJid(jid));
 
 
           if (_.isUndefined(jids)) {
           if (_.isUndefined(jids)) {
             jids = _converse.roster.pluck('jid');
             jids = _converse.roster.pluck('jid');
@@ -93084,7 +93085,11 @@ var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./no
 module.exports = function(o) {
 module.exports = function(o) {
 var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
 var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
 function print() { __p += __j.call(arguments, '') }
 function print() { __p += __j.call(arguments, '') }
-__p += '<!-- src/templates/chatbox_head.html -->\n<div class="chat-head chat-head-chatbox row no-gutters">\n    <div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>\n    <div class="chatbox-title">\n        <div class="row no-gutters">\n            <canvas class="avatar" height="36" width="36"></canvas>\n            <div class="col chat-title" title="' +
+__p += '<!-- src/templates/chatbox_head.html -->\n<div class="chat-head chat-head-chatbox row no-gutters">\n    <div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>\n    <div class="chatbox-title">\n        <div class="row no-gutters">\n            ';
+ if (o.type !== o._converse.HEADLINES_TYPE) { ;
+__p += '\n                <canvas class="avatar" height="36" width="36"></canvas>\n            ';
+ } ;
+__p += '\n            <div class="col chat-title" title="' +
 __e(o.jid) +
 __e(o.jid) +
 '">\n                ';
 '">\n                ';
  if (o.url) { ;
  if (o.url) { ;

+ 3 - 4
spec/chatbox.js

@@ -241,9 +241,8 @@
 
 
                 await test_utils.waitForRoster(_converse, 'current');
                 await test_utils.waitForRoster(_converse, 'current');
                 const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
                 const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                const chat = _converse.api.chats.create(sender_jid, {
-                    minimized: true
-                });
+                const chat = await _converse.api.chats.create(sender_jid, {'minimized': true});
+                await test_utils.waitUntil(() => _converse.chatboxes.length > 1);
                 const chatBoxView = _converse.chatboxviews.get(sender_jid);
                 const chatBoxView = _converse.chatboxviews.get(sender_jid);
                 expect(u.isVisible(chatBoxView.el)).toBeFalsy();
                 expect(u.isVisible(chatBoxView.el)).toBeFalsy();
 
 
@@ -1389,7 +1388,7 @@
 
 
                 msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread too');
                 msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread too');
                 await _converse.chatboxes.onMessage(msg);
                 await _converse.chatboxes.onMessage(msg);
-                await test_utils.waitUntil(() => chatbox.messages.length > 1);
+                await test_utils.waitUntil(() => chatbox.messages.length === 2);
                 indicator_el = sizzle(selector, _converse.rosterview.el).pop();
                 indicator_el = sizzle(selector, _converse.rosterview.el).pop();
                 expect(indicator_el.textContent).toBe('2');
                 expect(indicator_el.textContent).toBe('2');
                 done();
                 done();

+ 13 - 14
spec/converse.js

@@ -247,23 +247,24 @@
 
 
         describe("The \"contacts\" API", function () {
         describe("The \"contacts\" API", function () {
 
 
-            it("has a method 'get' which returns wrapped contacts", mock.initConverse((done, _converse) => {
+            it("has a method 'get' which returns wrapped contacts", mock.initConverse(async (done, _converse) => {
                 // Check that it returns nothing if a non-existing JID is given
                 // Check that it returns nothing if a non-existing JID is given
                 test_utils.createContacts(_converse, 'current');
                 test_utils.createContacts(_converse, 'current');
-                expect(_converse.api.contacts.get('non-existing@jabber.org')).toBeFalsy();
+                let contact = await _converse.api.contacts.get('non-existing@jabber.org');
+                expect(contact).toBeFalsy();
                 // Check when a single jid is given
                 // Check when a single jid is given
                 const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
                 const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                const contact = _converse.api.contacts.get(jid);
+                contact = await _converse.api.contacts.get(jid);
                 expect(contact.get('fullname')).toBe(mock.cur_names[0]);
                 expect(contact.get('fullname')).toBe(mock.cur_names[0]);
                 expect(contact.get('jid')).toBe(jid);
                 expect(contact.get('jid')).toBe(jid);
                 // You can retrieve multiple contacts by passing in an array
                 // You can retrieve multiple contacts by passing in an array
                 const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
                 const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
-                let list = _converse.api.contacts.get([jid, jid2]);
+                let list = await _converse.api.contacts.get([jid, jid2]);
                 expect(_.isArray(list)).toBeTruthy();
                 expect(_.isArray(list)).toBeTruthy();
                 expect(list[0].get('fullname')).toBe(mock.cur_names[0]);
                 expect(list[0].get('fullname')).toBe(mock.cur_names[0]);
                 expect(list[1].get('fullname')).toBe(mock.cur_names[1]);
                 expect(list[1].get('fullname')).toBe(mock.cur_names[1]);
                 // Check that all JIDs are returned if you call without any parameters
                 // Check that all JIDs are returned if you call without any parameters
-                list = _converse.api.contacts.get();
+                list = await _converse.api.contacts.get();
                 expect(list.length).toBe(mock.cur_names.length);
                 expect(list.length).toBe(mock.cur_names.length);
                 done();
                 done();
             }));
             }));
@@ -301,11 +302,9 @@
                 expect(_converse.chatboxes.length).toBe(1);
                 expect(_converse.chatboxes.length).toBe(1);
 
 
                 // Test for one JID
                 // Test for one JID
-                test_utils.openChatBoxFor(_converse, jid);
-                await test_utils.waitUntil(() => _converse.chatboxes.length == 1);
-                box = _converse.api.chats.get(jid);
+                box = await _converse.api.chats.open(jid);
                 expect(box instanceof Object).toBeTruthy();
                 expect(box instanceof Object).toBeTruthy();
-                expect(box.get('box_id')).toBe(b64_sha1(jid));
+                expect(box.get('box_id')).toBe(`box-${btoa(jid)}`);
 
 
                 const chatboxview = _converse.chatboxviews.get(jid);
                 const chatboxview = _converse.chatboxviews.get(jid);
                 expect(u.isVisible(chatboxview.el)).toBeTruthy();
                 expect(u.isVisible(chatboxview.el)).toBeTruthy();
@@ -314,8 +313,8 @@
                 await test_utils.waitUntil(() => _converse.chatboxes.length == 2);
                 await test_utils.waitUntil(() => _converse.chatboxes.length == 2);
                 const list = _converse.api.chats.get([jid, jid2]);
                 const list = _converse.api.chats.get([jid, jid2]);
                 expect(_.isArray(list)).toBeTruthy();
                 expect(_.isArray(list)).toBeTruthy();
-                expect(list[0].get('box_id')).toBe(b64_sha1(jid));
-                expect(list[1].get('box_id')).toBe(b64_sha1(jid2));
+                expect(list[0].get('box_id')).toBe(`box-${btoa(jid)}`);
+                expect(list[1].get('box_id')).toBe(`box-${btoa(jid2)}`);
                 done();
                 done();
             }));
             }));
 
 
@@ -334,7 +333,7 @@
 
 
                 const box = await _converse.api.chats.open(jid);
                 const box = await _converse.api.chats.open(jid);
                 expect(box instanceof Object).toBeTruthy();
                 expect(box instanceof Object).toBeTruthy();
-                expect(box.get('box_id')).toBe(b64_sha1(jid));
+                expect(box.get('box_id')).toBe(`box-${btoa(jid)}`);
                 expect(
                 expect(
                     _.keys(box),
                     _.keys(box),
                     ['close', 'endOTR', 'focus', 'get', 'initiateOTR', 'is_chatroom', 'maximize', 'minimize', 'open', 'set']
                     ['close', 'endOTR', 'focus', 'get', 'initiateOTR', 'is_chatroom', 'maximize', 'minimize', 'open', 'set']
@@ -344,8 +343,8 @@
                 // Test for multiple JIDs
                 // Test for multiple JIDs
                 const list = await _converse.api.chats.open([jid, jid2]);
                 const list = await _converse.api.chats.open([jid, jid2]);
                 expect(_.isArray(list)).toBeTruthy();
                 expect(_.isArray(list)).toBeTruthy();
-                expect(list[0].get('box_id')).toBe(b64_sha1(jid));
-                expect(list[1].get('box_id')).toBe(b64_sha1(jid2));
+                expect(list[0].get('box_id')).toBe(`box-${btoa(jid)}`);
+                expect(list[1].get('box_id')).toBe(`box-${btoa(jid2)}`);
                 done();
                 done();
             }));
             }));
         });
         });

+ 1 - 1
spec/messages.js

@@ -741,7 +741,7 @@
             let msg_obj = chatbox.messages.models[0];
             let msg_obj = chatbox.messages.models[0];
             expect(msg_obj.get('message')).toEqual(message);
             expect(msg_obj.get('message')).toEqual(message);
             expect(msg_obj.get('fullname')).toBeUndefined();
             expect(msg_obj.get('fullname')).toBeUndefined();
-            expect(msg_obj.get('nickname')).toBeUndefined();
+            expect(msg_obj.get('nickname')).toBe(null);
             expect(msg_obj.get('sender')).toEqual('them');
             expect(msg_obj.get('sender')).toEqual('them');
             expect(msg_obj.get('is_delayed')).toEqual(true);
             expect(msg_obj.get('is_delayed')).toEqual(true);
             await test_utils.waitUntil(() => chatbox.vcard.get('fullname') === 'Candice van der Knijff')
             await test_utils.waitUntil(() => chatbox.vcard.get('fullname') === 'Candice van der Knijff')

+ 1 - 2
spec/user-details-modal.js

@@ -24,9 +24,8 @@
 
 
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             test_utils.openChatBoxFor(_converse, contact_jid);
             test_utils.openChatBoxFor(_converse, contact_jid);
-            await test_utils.waitUntil(() => _converse.chatboxes.length);
+            await test_utils.waitUntil(() => _converse.chatboxes.length > 1);
             const view = _converse.chatboxviews.get(contact_jid);
             const view = _converse.chatboxviews.get(contact_jid);
-            await new Promise((resolve) => view.model.once('contactAdded', resolve));
             let show_modal_button = view.el.querySelector('.show-user-details-modal');
             let show_modal_button = view.el.querySelector('.show-user-details-modal');
             expect(u.isVisible(show_modal_button)).toBeTruthy();
             expect(u.isVisible(show_modal_button)).toBeTruthy();
             show_modal_button.click();
             show_modal_button.click();

+ 7 - 7
src/converse-chatboxviews.js

@@ -23,17 +23,17 @@ const AvatarMixin = {
         if (_.isNull(canvas_el)) {
         if (_.isNull(canvas_el)) {
             return;
             return;
         }
         }
-        const data = {
-            'classes': canvas_el.getAttribute('class'),
-            'width': canvas_el.width,
-            'height': canvas_el.height,
-        }
         if (this.model.vcard) {
         if (this.model.vcard) {
+            const data = {
+                'classes': canvas_el.getAttribute('class'),
+                'width': canvas_el.width,
+                'height': canvas_el.height,
+            }
             const image_type = this.model.vcard.get('image_type'),
             const image_type = this.model.vcard.get('image_type'),
                   image = this.model.vcard.get('image');
                   image = this.model.vcard.get('image');
             data['image'] = "data:" + image_type + ";base64," + image;
             data['image'] = "data:" + image_type + ";base64," + image;
+            canvas_el.outerHTML = tpl_avatar(data);
         }
         }
-        canvas_el.outerHTML = tpl_avatar(data);
     },
     },
 };
 };
 
 
@@ -143,7 +143,7 @@ converse.plugins.add('converse-chatboxviews', {
                 /* This method gets overridden in src/converse-controlbox.js if
                 /* This method gets overridden in src/converse-controlbox.js if
                  * the controlbox plugin is active.
                  * the controlbox plugin is active.
                  */
                  */
-                this.each(function (view) { view.close(); });
+                this.each(v => v.close());
                 return this;
                 return this;
             }
             }
         });
         });

+ 14 - 8
src/converse-chatview.js

@@ -163,7 +163,9 @@ converse.plugins.add('converse-chatview', {
                 this.model.on('change:status', this.onStatusMessageChanged, this);
                 this.model.on('change:status', this.onStatusMessageChanged, this);
 
 
                 this.debouncedRender = _.debounce(this.render, 50);
                 this.debouncedRender = _.debounce(this.render, 50);
-                this.model.vcard.on('change', this.debouncedRender, this);
+                if (this.model.vcard) {
+                    this.model.vcard.on('change', this.debouncedRender, this);
+                }
                 this.model.on('rosterContactAdded', () => {
                 this.model.on('rosterContactAdded', () => {
                     this.model.contact.on('change:nickname', this.debouncedRender, this);
                     this.model.contact.on('change:nickname', this.debouncedRender, this);
                     this.debouncedRender();
                     this.debouncedRender();
@@ -171,9 +173,11 @@ converse.plugins.add('converse-chatview', {
             },
             },
 
 
             render () {
             render () {
+                const vcard = _.get(this.model, 'vcard'),
+                      vcard_json = vcard ? vcard.toJSON() : {};
                 this.el.innerHTML = tpl_chatbox_head(
                 this.el.innerHTML = tpl_chatbox_head(
                     _.extend(
                     _.extend(
-                        this.model.vcard.toJSON(),
+                        vcard_json,
                         this.model.toJSON(),
                         this.model.toJSON(),
                         { '_converse': _converse,
                         { '_converse': _converse,
                           'info_close': __('Close this chat box'),
                           'info_close': __('Close this chat box'),
@@ -213,7 +217,7 @@ converse.plugins.add('converse-chatview', {
 
 
             initialize () {
             initialize () {
                 _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
                 _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
-                this.model.on('contactAdded', this.registerContactEventHandlers, this);
+                this.model.on('rosterContactAdded', this.registerContactEventHandlers, this);
                 this.model.on('change', this.render, this);
                 this.model.on('change', this.render, this);
                 this.registerContactEventHandlers();
                 this.registerContactEventHandlers();
                 /**
                 /**
@@ -226,9 +230,11 @@ converse.plugins.add('converse-chatview', {
             },
             },
 
 
             toHTML () {
             toHTML () {
+                const vcard = _.get(this.model, 'vcard'),
+                      vcard_json = vcard ? vcard.toJSON() : {};
                 return tpl_user_details_modal(_.extend(
                 return tpl_user_details_modal(_.extend(
                     this.model.toJSON(),
                     this.model.toJSON(),
-                    this.model.vcard.toJSON(), {
+                    vcard_json, {
                     '_': _,
                     '_': _,
                     '__': __,
                     '__': __,
                     'view': this,
                     'view': this,
@@ -1380,10 +1386,10 @@ converse.plugins.add('converse-chatview', {
         });
         });
 
 
         _converse.api.listen.on('chatBoxViewsInitialized', () => {
         _converse.api.listen.on('chatBoxViewsInitialized', () => {
-            const that = _converse.chatboxviews;
+            const views = _converse.chatboxviews;
             _converse.chatboxes.on('add', item => {
             _converse.chatboxes.on('add', item => {
-                if (!that.get(item.get('id')) && item.get('type') === _converse.PRIVATE_CHAT_TYPE) {
-                    that.add(item.get('id'), new _converse.ChatBoxView({model: item}));
+                if (!views.get(item.get('id')) && item.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+                    views.add(item.get('id'), new _converse.ChatBoxView({model: item}));
                 }
                 }
             });
             });
         });
         });
@@ -1421,7 +1427,7 @@ converse.plugins.add('converse-chatview', {
                 'get' (jids) {
                 'get' (jids) {
                     if (_.isUndefined(jids)) {
                     if (_.isUndefined(jids)) {
                         _converse.log(
                         _converse.log(
-                            "chats.create: You need to provide at least one JID",
+                            "chatviews.get: You need to provide at least one JID",
                             Strophe.LogLevel.ERROR
                             Strophe.LogLevel.ERROR
                         );
                         );
                         return null;
                         return null;

+ 11 - 11
src/converse-controlbox.js

@@ -188,18 +188,18 @@ converse.plugins.add('converse-controlbox', {
         const addControlBox = () => _converse.chatboxes.add({'id': 'controlbox'});
         const addControlBox = () => _converse.chatboxes.add({'id': 'controlbox'});
 
 
         _converse.ControlBox = _converse.ChatBox.extend({
         _converse.ControlBox = _converse.ChatBox.extend({
-            defaults: {
-                'bookmarked': false,
-                'box_id': 'controlbox',
-                'chat_state': undefined,
-                'closed': !_converse.show_controlbox_by_default,
-                'num_unread': 0,
-                'type': _converse.CONTROLBOX_TYPE,
-                'url': ''
-            },
 
 
-            initialize () {
-                u.safeSave(this, {'time_opened': this.get('time_opened') || moment().valueOf()});
+            defaults () {
+                return {
+                    'bookmarked': false,
+                    'box_id': 'controlbox',
+                    'chat_state': undefined,
+                    'closed': !_converse.show_controlbox_by_default,
+                    'num_unread': 0,
+                    'time_opened': this.get('time_opened') || moment().valueOf(),
+                    'type': _converse.CONTROLBOX_TYPE,
+                    'url': ''
+                }
             }
             }
         });
         });
 
 

+ 19 - 11
src/converse-headline.js

@@ -8,7 +8,7 @@ import "converse-chatview";
 import converse from "@converse/headless/converse-core";
 import converse from "@converse/headless/converse-core";
 import tpl_chatbox from "templates/chatbox.html";
 import tpl_chatbox from "templates/chatbox.html";
 
 
-const { _, utils } = converse.env;
+const { _, moment, utils } = converse.env;
 
 
 
 
 converse.plugins.add('converse-headline', {
 converse.plugins.add('converse-headline', {
@@ -52,13 +52,21 @@ converse.plugins.add('converse-headline', {
             { __ } = _converse;
             { __ } = _converse;
 
 
         _converse.HeadlinesBox = _converse.ChatBox.extend({
         _converse.HeadlinesBox = _converse.ChatBox.extend({
-            defaults: {
-                'type': _converse.HEADLINES_TYPE,
-                'bookmarked': false,
-                'chat_state': undefined,
-                'num_unread': 0,
-                'url': ''
+            defaults () {
+                return {
+                    'bookmarked': false,
+                    'hidden': _.includes(['mobile', 'fullscreen'], _converse.view_mode),
+                    'message_type': 'headline',
+                    'num_unread': 0,
+                    'time_opened': this.get('time_opened') || moment().valueOf(),
+                    'type': _converse.HEADLINES_TYPE
+                }
             },
             },
+
+            initialize () {
+                this.initMessages();
+                this.set({'box_id': `box-${btoa(this.get('jid'))}`});
+            }
         });
         });
 
 
 
 
@@ -110,7 +118,7 @@ converse.plugins.add('converse-headline', {
             if (utils.isHeadlineMessage(_converse, message)) {
             if (utils.isHeadlineMessage(_converse, message)) {
                 const from_jid = message.getAttribute('from');
                 const from_jid = message.getAttribute('from');
                 if (_.includes(from_jid, '@') && 
                 if (_.includes(from_jid, '@') && 
-                        !_converse.api.contacts.get(from_jid) &&
+                        !_converse.roster.get(from_jid) &&
                         !_converse.allow_non_roster_messaging) {
                         !_converse.allow_non_roster_messaging) {
                     return;
                     return;
                 }
                 }
@@ -142,10 +150,10 @@ converse.plugins.add('converse-headline', {
 
 
 
 
         _converse.api.listen.on('chatBoxViewsInitialized', () => {
         _converse.api.listen.on('chatBoxViewsInitialized', () => {
-            const that = _converse.chatboxviews;
+            const views = _converse.chatboxviews;
             _converse.chatboxes.on('add', item => {
             _converse.chatboxes.on('add', item => {
-                if (!that.get(item.get('id')) && item.get('type') === _converse.HEADLINES_TYPE) {
-                    that.add(item.get('id'), new _converse.HeadlinesBoxView({model: item}));
+                if (!views.get(item.get('id')) && item.get('type') === _converse.HEADLINES_TYPE) {
+                    views.add(item.get('id'), new _converse.HeadlinesBoxView({model: item}));
                 }
                 }
             });
             });
         });
         });

+ 4 - 3
src/converse-muc-views.js

@@ -108,10 +108,11 @@ converse.plugins.add('converse-muc-views', {
         // configuration settings.
         // configuration settings.
         _converse.api.settings.update({
         _converse.api.settings.update({
             'auto_list_rooms': false,
             'auto_list_rooms': false,
+            'cache_muc_messages': true,
+            'locked_muc_domain': false,
+            'locked_muc_nickname': false,
             'muc_disable_moderator_commands': false,
             'muc_disable_moderator_commands': false,
             'muc_domain': undefined,
             'muc_domain': undefined,
-            'locked_muc_nickname': false,
-            'locked_muc_domain': false,
             'muc_show_join_leave': true,
             'muc_show_join_leave': true,
             'roomconfig_whitelist': [],
             'roomconfig_whitelist': [],
             'visible_toolbar_buttons': {
             'visible_toolbar_buttons': {
@@ -2130,7 +2131,7 @@ converse.plugins.add('converse-muc-views', {
             /* Upon a reconnection event from converse, join again
             /* Upon a reconnection event from converse, join again
              * all the open groupchats.
              * all the open groupchats.
              */
              */
-            _converse.chatboxviews.each(function (view) {
+            _converse.chatboxviews.each(view => {
                 if (view.model.get('type') === _converse.CHATROOMS_TYPE) {
                 if (view.model.get('type') === _converse.CHATROOMS_TYPE) {
                     view.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
                     view.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
                     view.model.registerHandlers();
                     view.model.registerHandlers();

+ 53 - 61
src/headless/converse-chatboxes.js

@@ -9,7 +9,7 @@ import "./utils/form";
 import converse from "./converse-core";
 import converse from "./converse-core";
 import filesize from "filesize";
 import filesize from "filesize";
 
 
-const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, sizzle, utils, _ } = converse.env;
+const { $msg, Backbone, Promise, Strophe, moment, sizzle, utils, _ } = converse.env;
 const u = converse.env.utils;
 const u = converse.env.utils;
 
 
 Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
 Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
@@ -59,10 +59,10 @@ converse.plugins.add('converse-chatboxes', {
         const ModelWithContact = Backbone.Model.extend({
         const ModelWithContact = Backbone.Model.extend({
 
 
             async setRosterContact (jid) {
             async setRosterContact (jid) {
-                await _converse.api.waitUntil('rosterContactsFetched');
-                const contact = _converse.roster.get(jid);
+                const contact = await _converse.api.contacts.get(jid);
                 if (contact) {
                 if (contact) {
                     this.contact = contact;
                     this.contact = contact;
+                    this.set('nickname', contact.get('nickname'));
                     this.trigger('rosterContactAdded');
                     this.trigger('rosterContactAdded');
                 }
                 }
             }
             }
@@ -259,6 +259,7 @@ converse.plugins.add('converse-chatboxes', {
                     'message_type': 'chat',
                     'message_type': 'chat',
                     'nickname': undefined,
                     'nickname': undefined,
                     'num_unread': 0,
                     'num_unread': 0,
+                    'time_opened': this.get('time_opened') || moment().valueOf(),
                     'type': _converse.PRIVATE_CHAT_TYPE,
                     'type': _converse.PRIVATE_CHAT_TYPE,
                     'url': ''
                     'url': ''
                 }
                 }
@@ -276,23 +277,24 @@ converse.plugins.add('converse-chatboxes', {
                     // but we're in embedded mode.
                     // but we're in embedded mode.
                     return;
                     return;
                 }
                 }
-                // XXX: this creates a dependency on converse-roster, which we
-                // probably shouldn't have here, so we should probably move
-                // ChatBox out of converse-chatboxes
-                this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
+                this.set({'box_id': `box-${btoa(jid)}`});
 
 
                 if (_converse.vcards) {
                 if (_converse.vcards) {
                     this.vcard = _converse.vcards.findWhere({'jid': jid}) || _converse.vcards.create({'jid': jid});
                     this.vcard = _converse.vcards.findWhere({'jid': jid}) || _converse.vcards.create({'jid': jid});
                 }
                 }
-
                 if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
                 if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+                    this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
                     this.setRosterContact(jid);
                     this.setRosterContact(jid);
                 }
                 }
+                this.on('change:chat_state', this.sendChatState, this);
+                this.initMessages();
+            },
 
 
+            initMessages () {
                 this.messages = new _converse.Messages();
                 this.messages = new _converse.Messages();
                 const storage = _converse.config.get('storage');
                 const storage = _converse.config.get('storage');
                 this.messages.browserStorage = new Backbone.BrowserStorage[storage](
                 this.messages.browserStorage = new Backbone.BrowserStorage[storage](
-                    b64_sha1(`converse.messages${jid}${_converse.bare_jid}`));
+                    `converse.messages${this.get('jid')}${_converse.bare_jid}`);
                 this.messages.chatbox = this;
                 this.messages.chatbox = this;
 
 
                 this.messages.on('change:upload', (message) => {
                 this.messages.on('change:upload', (message) => {
@@ -300,19 +302,6 @@ converse.plugins.add('converse-chatboxes', {
                         _converse.api.send(this.createMessageStanza(message));
                         _converse.api.send(this.createMessageStanza(message));
                     }
                     }
                 });
                 });
-
-                this.on('change:chat_state', this.sendChatState, this);
-
-                // Models get saved immediately after creation, so no need to
-                // call `save` here.
-                this.set({
-                    // The chat_state will be set to ACTIVE once the chat box is opened
-                    // and we listen for change:chat_state, so shouldn't set it to ACTIVE here.
-                    'box_id' : b64_sha1(this.get('jid')),
-                    'time_opened': this.get('time_opened') || moment().valueOf(),
-                    'user_id' : Strophe.getNodeFromJid(this.get('jid')),
-                    'nickname':_.get(_converse.api.contacts.get(this.get('jid')), 'attributes.nickname')
-                });
             },
             },
 
 
             validate (attrs, options) {
             validate (attrs, options) {
@@ -956,8 +945,7 @@ converse.plugins.add('converse-chatboxes', {
                       from_resource = Strophe.getResourceFromJid(from_jid),
                       from_resource = Strophe.getResourceFromJid(from_jid),
                       is_me = from_bare_jid === _converse.bare_jid;
                       is_me = from_bare_jid === _converse.bare_jid;
 
 
-                let contact_jid,
-                    is_roster_contact = false;
+                let contact_jid; 
                 if (is_me) {
                 if (is_me) {
                     // I am the sender, so this must be a forwarded message...
                     // I am the sender, so this must be a forwarded message...
                     if (_.isNull(to_jid)) {
                     if (_.isNull(to_jid)) {
@@ -969,15 +957,17 @@ converse.plugins.add('converse-chatboxes', {
                     contact_jid = Strophe.getBareJidFromJid(to_jid);
                     contact_jid = Strophe.getBareJidFromJid(to_jid);
                 } else {
                 } else {
                     contact_jid = from_bare_jid;
                     contact_jid = from_bare_jid;
-                    await _converse.api.waitUntil('rosterContactsFetched');
-                    is_roster_contact = !_.isUndefined(_converse.roster.get(contact_jid));
-                    if (!is_roster_contact && !_converse.allow_non_roster_messaging) {
-                        return;
-                    }
                 }
                 }
+
+                const contact = await _converse.api.contacts.get(contact_jid);
+                const is_roster_contact = !_.isUndefined(contact);
+                if (!is_me && !is_roster_contact && !_converse.allow_non_roster_messaging) {
+                    return;
+                }
+
                 // Get chat box, but only create when the message has something to show to the user
                 // Get chat box, but only create when the message has something to show to the user
                 const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0,
                 const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0,
-                      roster_nick = _.get(_converse.api.contacts.get(contact_jid), 'attributes.nickname'),
+                      roster_nick = _.get(contact, 'attributes.nickname'),
                       chatbox = this.getChatBox(contact_jid, {'nickname': roster_nick}, has_body);
                       chatbox = this.getChatBox(contact_jid, {'nickname': roster_nick}, has_body);
 
 
                 if (chatbox) {
                 if (chatbox) {
@@ -1105,17 +1095,11 @@ converse.plugins.add('converse-chatboxes', {
                  * @param {string|string[]} jid|jids An jid or array of jids
                  * @param {string|string[]} jid|jids An jid or array of jids
                  * @param {object} [attrs] An object containing configuration attributes.
                  * @param {object} [attrs] An object containing configuration attributes.
                  */
                  */
-                'create' (jids, attrs) {
-                    if (_.isUndefined(jids)) {
-                        _converse.log(
-                            "chats.create: You need to provide at least one JID",
-                            Strophe.LogLevel.ERROR
-                        );
-                        return null;
-                    }
+                async create (jids, attrs) {
                     if (_.isString(jids)) {
                     if (_.isString(jids)) {
                         if (attrs && !_.get(attrs, 'fullname')) {
                         if (attrs && !_.get(attrs, 'fullname')) {
-                            attrs.fullname = _.get(_converse.api.contacts.get(jids), 'attributes.fullname');
+                            const contact = await _converse.api.contacts.get(jids);
+                            attrs.fullname = _.get(contact, 'attributes.fullname');
                         }
                         }
                         const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true);
                         const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true);
                         if (_.isNil(chatbox)) {
                         if (_.isNil(chatbox)) {
@@ -1124,10 +1108,18 @@ converse.plugins.add('converse-chatboxes', {
                         }
                         }
                         return chatbox;
                         return chatbox;
                     }
                     }
-                    return _.map(jids, (jid) => {
-                        attrs.fullname = _.get(_converse.api.contacts.get(jid), 'attributes.fullname');
-                        return _converse.chatboxes.getChatBox(jid, attrs, true).maybeShow();
-                    });
+                    if (_.isArray(jids)) {
+                        return Promise.all(jids.forEach(async jid => {
+                            const contact = await _converse.api.contacts.get(jids);
+                            attrs.fullname = _.get(contact, 'attributes.fullname');
+                            return _converse.chatboxes.getChatBox(jid, attrs, true).maybeShow();
+                        }));
+                    }
+                    _converse.log(
+                        "chats.create: You need to provide at least one JID",
+                        Strophe.LogLevel.ERROR
+                    );
+                    return null;
                 },
                 },
 
 
                 /**
                 /**
@@ -1136,6 +1128,8 @@ converse.plugins.add('converse-chatboxes', {
                  * @method _converse.api.chats.open
                  * @method _converse.api.chats.open
                  * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
                  * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
                  * @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
                  * @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
+                 * @param {Boolean} [attrs.minimized] - Should the chat be
+                 *   created in minimized state.
                  * @param {Boolean} [force=false] - By default, a minimized
                  * @param {Boolean} [force=false] - By default, a minimized
                  *   chat won't be maximized (in `overlayed` view mode) and in
                  *   chat won't be maximized (in `overlayed` view mode) and in
                  *   `fullscreen` view mode a newly opened chat won't replace
                  *   `fullscreen` view mode a newly opened chat won't replace
@@ -1169,23 +1163,21 @@ converse.plugins.add('converse-chatboxes', {
                  *     }
                  *     }
                  * });
                  * });
                  */
                  */
-                'open' (jids, attrs, force) {
-                    return new Promise((resolve, reject) => {
-                        Promise.all([
-                            _converse.api.waitUntil('rosterContactsFetched'),
-                            _converse.api.waitUntil('chatBoxesFetched')
-                        ]).then(() => {
-                            if (_.isUndefined(jids)) {
-                                const err_msg = "chats.open: You need to provide at least one JID";
-                                _converse.log(err_msg, Strophe.LogLevel.ERROR);
-                                reject(new Error(err_msg));
-                            } else if (_.isString(jids)) {
-                                resolve(_converse.api.chats.create(jids, attrs).maybeShow(force));
-                            } else {
-                                resolve(_.map(jids, jid => _converse.api.chats.create(jid, attrs).maybeShow(force)));
-                            }
-                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
-                    });
+                async open (jids, attrs, force) {
+                    await Promise.all([
+                        _converse.api.waitUntil('rosterContactsFetched'),
+                        _converse.api.waitUntil('chatBoxesFetched')
+                    ]);
+
+                    if (_.isString(jids)) {
+                        const chat = await _converse.api.chats.create(jids, attrs);
+                        return chat.maybeShow(force);
+                    } else if (_.isArray(jids)) {
+                        return Promise.all(jids.map(j => _converse.api.chats.create(j, attrs).then(c => c.maybeShow(force))));
+                    }
+                    const err_msg = "chats.open: You need to provide at least one JID";
+                    _converse.log(err_msg, Strophe.LogLevel.ERROR);
+                    throw new Error(err_msg);
                 },
                 },
 
 
                 /**
                 /**
@@ -1208,7 +1200,7 @@ converse.plugins.add('converse-chatboxes', {
                  * const models = _converse.api.chats.get();
                  * const models = _converse.api.chats.get();
                  *
                  *
                  */
                  */
-                'get' (jids) {
+                get (jids) {
                     if (_.isUndefined(jids)) {
                     if (_.isUndefined(jids)) {
                         const result = [];
                         const result = [];
                         _converse.chatboxes.each(function (chatbox) {
                         _converse.chatboxes.each(function (chatbox) {

+ 34 - 26
src/headless/converse-muc.js

@@ -19,7 +19,7 @@ const MUC_ROLE_WEIGHTS = {
     'none':         2,
     'none':         2,
 };
 };
 
 
-const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, b64_sha1, sizzle, f, moment, _ } = converse.env;
+const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, sizzle, f, moment, _ } = converse.env;
 
 
 // Add Strophe Namespaces
 // Add Strophe Namespaces
 Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
 Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
@@ -162,7 +162,6 @@ converse.plugins.add('converse-muc', {
              */
              */
             settings.type = _converse.CHATROOMS_TYPE;
             settings.type = _converse.CHATROOMS_TYPE;
             settings.id = jid;
             settings.id = jid;
-            settings.box_id = b64_sha1(jid)
             const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
             const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
             chatbox.maybeShow(true);
             chatbox.maybeShow(true);
             return chatbox;
             return chatbox;
@@ -178,32 +177,42 @@ converse.plugins.add('converse-muc', {
         _converse.ChatRoom = _converse.ChatBox.extend({
         _converse.ChatRoom = _converse.ChatBox.extend({
 
 
             defaults () {
             defaults () {
-                return _.assign(
-                    _.clone(_converse.ChatBox.prototype.defaults), {
-                      // For group chats, we distinguish between generally unread
-                      // messages and those ones that specifically mention the
-                      // user.
-                      //
-                      // To keep things simple, we reuse `num_unread` from
-                      // _converse.ChatBox to indicate unread messages which
-                      // mention the user and `num_unread_general` to indicate
-                      // generally unread messages (which *includes* mentions!).
-                      'num_unread_general': 0,
-
-                      'affiliation': null,
-                      'connection_status': converse.ROOMSTATUS.DISCONNECTED,
-                      'name': '',
-                      'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
-                      'description': '',
-                      'roomconfig': {},
-                      'type': _converse.CHATROOMS_TYPE,
-                      'message_type': 'groupchat'
-                    }
-                );
+                return {
+                    // For group chats, we distinguish between generally unread
+                    // messages and those ones that specifically mention the
+                    // user.
+                    //
+                    // To keep things simple, we reuse `num_unread` from
+                    // _converse.ChatBox to indicate unread messages which
+                    // mention the user and `num_unread_general` to indicate
+                    // generally unread messages (which *includes* mentions!).
+                    'num_unread_general': 0,
+
+                    'affiliation': null,
+                    'bookmarked': false,
+                    'chat_state': undefined,
+                    'connection_status': converse.ROOMSTATUS.DISCONNECTED,
+                    'description': '',
+                    'hidden': _.includes(['mobile', 'fullscreen'], _converse.view_mode),
+                    'message_type': 'groupchat',
+                    'name': '',
+                    'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
+                    'num_unread': 0,
+                    'roomconfig': {},
+                    'time_opened': this.get('time_opened') || moment().valueOf(),
+                    'type': _converse.CHATROOMS_TYPE
+                }
             },
             },
 
 
             initialize() {
             initialize() {
-                this.constructor.__super__.initialize.apply(this, arguments);
+                if (_converse.vcards) {
+                    this.vcard = _converse.vcards.findWhere({'jid': this.get('jid')}) ||
+                        _converse.vcards.create({'jid': this.get('jid')});
+                }
+                this.set('box_id', `box-${btoa(this.get('jid'))}`);
+
+                this.initMessages();
+                this.on('change:chat_state', this.sendChatState, this);
                 this.on('change:connection_status', this.onConnectionStatusChanged, this);
                 this.on('change:connection_status', this.onConnectionStatusChanged, this);
 
 
                 const storage = _converse.config.get('storage');
                 const storage = _converse.config.get('storage');
@@ -1391,7 +1400,6 @@ converse.plugins.add('converse-muc', {
             jid = jid.toLowerCase();
             jid = jid.toLowerCase();
             attrs.type = _converse.CHATROOMS_TYPE;
             attrs.type = _converse.CHATROOMS_TYPE;
             attrs.id = jid;
             attrs.id = jid;
-            attrs.box_id = b64_sha1(jid)
             return _converse.chatboxes.getChatBox(jid, attrs, create);
             return _converse.chatboxes.getChatBox(jid, attrs, create);
         };
         };
 
 

+ 15 - 35
src/headless/converse-roster.js

@@ -880,44 +880,25 @@ converse.plugins.add('converse-roster', {
 
 
 
 
         /********** Event Handlers *************/
         /********** Event Handlers *************/
-        function addRelatedContactToChatbox (chatbox, contact) {
-            if (!_.isUndefined(contact)) {
-                chatbox.contact = contact;
-                chatbox.trigger('contactAdded', contact);
-            }
-        }
-
-        _converse.api.waitUntil('rosterContactsFetched').then(() => {
-            _converse.roster.on('add', (contact) => {
-                /* When a new contact is added, check if we already have a
-                 * chatbox open for it, and if so attach it to the chatbox.
-                 */
-                const chatbox = _converse.chatboxes.findWhere({'jid': contact.get('jid')});
-                if (chatbox) {
-                    addRelatedContactToChatbox(chatbox, contact);
-                }
-            });
-        });
-
-
         function updateUnreadCounter (chatbox) {
         function updateUnreadCounter (chatbox) {
             const contact = _converse.roster.findWhere({'jid': chatbox.get('jid')});
             const contact = _converse.roster.findWhere({'jid': chatbox.get('jid')});
             if (!_.isUndefined(contact)) {
             if (!_.isUndefined(contact)) {
                 contact.save({'num_unread': chatbox.get('num_unread')});
                 contact.save({'num_unread': chatbox.get('num_unread')});
             }
             }
         }
         }
+
         _converse.api.listen.on('chatBoxesInitialized', () => {
         _converse.api.listen.on('chatBoxesInitialized', () => {
-            _converse.chatboxes.on('change:num_unread', updateUnreadCounter)
+            _converse.chatboxes.on('change:num_unread', updateUnreadCounter);
 
 
-            _converse.chatboxes.on('add', async chatbox => {
-                await _converse.api.waitUntil('rosterContactsFetched');
-                addRelatedContactToChatbox(chatbox, _converse.roster.findWhere({'jid': chatbox.get('jid')}));
+            _converse.chatboxes.on('add', chatbox => {
+                if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+                    chatbox.setRosterContact(chatbox.get('jid'));
+                }
             });
             });
         });
         });
 
 
         _converse.api.listen.on('beforeTearDown', _converse.unregisterPresenceHandler());
         _converse.api.listen.on('beforeTearDown', _converse.unregisterPresenceHandler());
 
 
-
         _converse.api.waitUntil('rosterContactsFetched').then(() => {
         _converse.api.waitUntil('rosterContactsFetched').then(() => {
             _converse.roster.on('add', (contact) => {
             _converse.roster.on('add', (contact) => {
                 /* When a new contact is added, check if we already have a
                 /* When a new contact is added, check if we already have a
@@ -925,7 +906,7 @@ converse.plugins.add('converse-roster', {
                  */
                  */
                 const chatbox = _converse.chatboxes.findWhere({'jid': contact.get('jid')});
                 const chatbox = _converse.chatboxes.findWhere({'jid': contact.get('jid')});
                 if (chatbox) {
                 if (chatbox) {
-                    addRelatedContactToChatbox(chatbox, contact);
+                    chatbox.setRosterContact(contact.get('jid'));
                 }
                 }
             });
             });
         });
         });
@@ -999,20 +980,20 @@ converse.plugins.add('converse-roster', {
                  * @method _converse.api.contacts.get
                  * @method _converse.api.contacts.get
                  * @params {(string[]|string)} jid|jids The JID or JIDs of
                  * @params {(string[]|string)} jid|jids The JID or JIDs of
                  *      the contacts to be returned.
                  *      the contacts to be returned.
-                 * @returns {(RosterContact[]|RosterContact)} [Backbone.Model](http://backbonejs.org/#Model)
-                 *      (or an array of them) representing the contact.
+                 * @returns {promise} Promise which resolves with the
+                 *  _converse.RosterContact (or an array of them) representing the contact.
                  *
                  *
                  * @example
                  * @example
                  * // Fetch a single contact
                  * // Fetch a single contact
                  * _converse.api.listen.on('rosterContactsFetched', function () {
                  * _converse.api.listen.on('rosterContactsFetched', function () {
-                 *     const contact = _converse.api.contacts.get('buddy@example.com')
+                 *     const contact = await _converse.api.contacts.get('buddy@example.com')
                  *     // ...
                  *     // ...
                  * });
                  * });
                  *
                  *
                  * @example
                  * @example
                  * // To get multiple contacts, pass in an array of JIDs:
                  * // To get multiple contacts, pass in an array of JIDs:
                  * _converse.api.listen.on('rosterContactsFetched', function () {
                  * _converse.api.listen.on('rosterContactsFetched', function () {
-                 *     const contacts = _converse.api.contacts.get(
+                 *     const contacts = await _converse.api.contacts.get(
                  *         ['buddy1@example.com', 'buddy2@example.com']
                  *         ['buddy1@example.com', 'buddy2@example.com']
                  *     )
                  *     )
                  *     // ...
                  *     // ...
@@ -1021,14 +1002,13 @@ converse.plugins.add('converse-roster', {
                  * @example
                  * @example
                  * // To return all contacts, simply call ``get`` without any parameters:
                  * // To return all contacts, simply call ``get`` without any parameters:
                  * _converse.api.listen.on('rosterContactsFetched', function () {
                  * _converse.api.listen.on('rosterContactsFetched', function () {
-                 *     const contacts = _converse.api.contacts.get();
+                 *     const contacts = await _converse.api.contacts.get();
                  *     // ...
                  *     // ...
                  * });
                  * });
                  */
                  */
-                'get' (jids) {
-                    const _getter = function (jid) {
-                        return _converse.roster.get(Strophe.getBareJidFromJid(jid)) || null;
-                    };
+                async get (jids) {
+                    await _converse.api.waitUntil('rosterContactsFetched');
+                    const _getter = jid => _converse.roster.get(Strophe.getBareJidFromJid(jid));
                     if (_.isUndefined(jids)) {
                     if (_.isUndefined(jids)) {
                         jids = _converse.roster.pluck('jid');
                         jids = _converse.roster.pluck('jid');
                     } else if (_.isString(jids)) {
                     } else if (_.isString(jids)) {

+ 92 - 107
src/headless/dist/converse-headless.js

@@ -38790,6 +38790,7 @@ Strophe.Websocket.prototype = {
 });
 });
 //# sourceMappingURL=strophe.js.map
 //# sourceMappingURL=strophe.js.map
 
 
+
 /***/ }),
 /***/ }),
 
 
 /***/ "./node_modules/strophejs-plugin-ping/strophe.ping.js":
 /***/ "./node_modules/strophejs-plugin-ping/strophe.ping.js":
@@ -40248,7 +40249,6 @@ const _converse$env = _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].env
       Backbone = _converse$env.Backbone,
       Backbone = _converse$env.Backbone,
       Promise = _converse$env.Promise,
       Promise = _converse$env.Promise,
       Strophe = _converse$env.Strophe,
       Strophe = _converse$env.Strophe,
-      b64_sha1 = _converse$env.b64_sha1,
       moment = _converse$env.moment,
       moment = _converse$env.moment,
       sizzle = _converse$env.sizzle,
       sizzle = _converse$env.sizzle,
       utils = _converse$env.utils,
       utils = _converse$env.utils,
@@ -40291,12 +40291,11 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
 
 
     const ModelWithContact = Backbone.Model.extend({
     const ModelWithContact = Backbone.Model.extend({
       async setRosterContact(jid) {
       async setRosterContact(jid) {
-        await _converse.api.waitUntil('rosterContactsFetched');
-
-        const contact = _converse.roster.get(jid);
+        const contact = await _converse.api.contacts.get(jid);
 
 
         if (contact) {
         if (contact) {
           this.contact = contact;
           this.contact = contact;
+          this.set('nickname', contact.get('nickname'));
           this.trigger('rosterContactAdded');
           this.trigger('rosterContactAdded');
         }
         }
       }
       }
@@ -40524,6 +40523,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           'message_type': 'chat',
           'message_type': 'chat',
           'nickname': undefined,
           'nickname': undefined,
           'num_unread': 0,
           'num_unread': 0,
+          'time_opened': this.get('time_opened') || moment().valueOf(),
           'type': _converse.PRIVATE_CHAT_TYPE,
           'type': _converse.PRIVATE_CHAT_TYPE,
           'url': ''
           'url': ''
         };
         };
@@ -40541,15 +40541,10 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           // This happens when the controlbox is in browser storage,
           // This happens when the controlbox is in browser storage,
           // but we're in embedded mode.
           // but we're in embedded mode.
           return;
           return;
-        } // XXX: this creates a dependency on converse-roster, which we
-        // probably shouldn't have here, so we should probably move
-        // ChatBox out of converse-chatboxes
-
+        }
 
 
-        this.presence = _converse.presences.findWhere({
-          'jid': jid
-        }) || _converse.presences.create({
-          'jid': jid
+        this.set({
+          'box_id': `box-${btoa(jid)}`
         });
         });
 
 
         if (_converse.vcards) {
         if (_converse.vcards) {
@@ -40561,31 +40556,30 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         }
         }
 
 
         if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
         if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+          this.presence = _converse.presences.findWhere({
+            'jid': jid
+          }) || _converse.presences.create({
+            'jid': jid
+          });
           this.setRosterContact(jid);
           this.setRosterContact(jid);
         }
         }
 
 
+        this.on('change:chat_state', this.sendChatState, this);
+        this.initMessages();
+      },
+
+      initMessages() {
         this.messages = new _converse.Messages();
         this.messages = new _converse.Messages();
 
 
         const storage = _converse.config.get('storage');
         const storage = _converse.config.get('storage');
 
 
-        this.messages.browserStorage = new Backbone.BrowserStorage[storage](b64_sha1(`converse.messages${jid}${_converse.bare_jid}`));
+        this.messages.browserStorage = new Backbone.BrowserStorage[storage](`converse.messages${this.get('jid')}${_converse.bare_jid}`);
         this.messages.chatbox = this;
         this.messages.chatbox = this;
         this.messages.on('change:upload', message => {
         this.messages.on('change:upload', message => {
           if (message.get('upload') === _converse.SUCCESS) {
           if (message.get('upload') === _converse.SUCCESS) {
             _converse.api.send(this.createMessageStanza(message));
             _converse.api.send(this.createMessageStanza(message));
           }
           }
         });
         });
-        this.on('change:chat_state', this.sendChatState, this); // Models get saved immediately after creation, so no need to
-        // call `save` here.
-
-        this.set({
-          // The chat_state will be set to ACTIVE once the chat box is opened
-          // and we listen for change:chat_state, so shouldn't set it to ACTIVE here.
-          'box_id': b64_sha1(this.get('jid')),
-          'time_opened': this.get('time_opened') || moment().valueOf(),
-          'user_id': Strophe.getNodeFromJid(this.get('jid')),
-          'nickname': _.get(_converse.api.contacts.get(this.get('jid')), 'attributes.nickname')
-        });
       },
       },
 
 
       validate(attrs, options) {
       validate(attrs, options) {
@@ -41317,8 +41311,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
         const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
               from_resource = Strophe.getResourceFromJid(from_jid),
               from_resource = Strophe.getResourceFromJid(from_jid),
               is_me = from_bare_jid === _converse.bare_jid;
               is_me = from_bare_jid === _converse.bare_jid;
-        let contact_jid,
-            is_roster_contact = false;
+        let contact_jid;
 
 
         if (is_me) {
         if (is_me) {
           // I am the sender, so this must be a forwarded message...
           // I am the sender, so this must be a forwarded message...
@@ -41329,17 +41322,18 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           contact_jid = Strophe.getBareJidFromJid(to_jid);
           contact_jid = Strophe.getBareJidFromJid(to_jid);
         } else {
         } else {
           contact_jid = from_bare_jid;
           contact_jid = from_bare_jid;
-          await _converse.api.waitUntil('rosterContactsFetched');
-          is_roster_contact = !_.isUndefined(_converse.roster.get(contact_jid));
+        }
 
 
-          if (!is_roster_contact && !_converse.allow_non_roster_messaging) {
-            return;
-          }
+        const contact = await _converse.api.contacts.get(contact_jid);
+        const is_roster_contact = !_.isUndefined(contact);
+
+        if (!is_me && !is_roster_contact && !_converse.allow_non_roster_messaging) {
+          return;
         } // Get chat box, but only create when the message has something to show to the user
         } // Get chat box, but only create when the message has something to show to the user
 
 
 
 
         const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0,
         const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0,
-              roster_nick = _.get(_converse.api.contacts.get(contact_jid), 'attributes.nickname'),
+              roster_nick = _.get(contact, 'attributes.nickname'),
               chatbox = this.getChatBox(contact_jid, {
               chatbox = this.getChatBox(contact_jid, {
           'nickname': roster_nick
           'nickname': roster_nick
         }, has_body);
         }, has_body);
@@ -41490,16 +41484,11 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
          * @param {string|string[]} jid|jids An jid or array of jids
          * @param {string|string[]} jid|jids An jid or array of jids
          * @param {object} [attrs] An object containing configuration attributes.
          * @param {object} [attrs] An object containing configuration attributes.
          */
          */
-        'create'(jids, attrs) {
-          if (_.isUndefined(jids)) {
-            _converse.log("chats.create: You need to provide at least one JID", Strophe.LogLevel.ERROR);
-
-            return null;
-          }
-
+        async create(jids, attrs) {
           if (_.isString(jids)) {
           if (_.isString(jids)) {
             if (attrs && !_.get(attrs, 'fullname')) {
             if (attrs && !_.get(attrs, 'fullname')) {
-              attrs.fullname = _.get(_converse.api.contacts.get(jids), 'attributes.fullname');
+              const contact = await _converse.api.contacts.get(jids);
+              attrs.fullname = _.get(contact, 'attributes.fullname');
             }
             }
 
 
             const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true);
             const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true);
@@ -41513,10 +41502,17 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
             return chatbox;
             return chatbox;
           }
           }
 
 
-          return _.map(jids, jid => {
-            attrs.fullname = _.get(_converse.api.contacts.get(jid), 'attributes.fullname');
-            return _converse.chatboxes.getChatBox(jid, attrs, true).maybeShow();
-          });
+          if (_.isArray(jids)) {
+            return Promise.all(jids.forEach(async jid => {
+              const contact = await _converse.api.contacts.get(jids);
+              attrs.fullname = _.get(contact, 'attributes.fullname');
+              return _converse.chatboxes.getChatBox(jid, attrs, true).maybeShow();
+            }));
+          }
+
+          _converse.log("chats.create: You need to provide at least one JID", Strophe.LogLevel.ERROR);
+
+          return null;
         },
         },
 
 
         /**
         /**
@@ -41525,6 +41521,8 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
          * @method _converse.api.chats.open
          * @method _converse.api.chats.open
          * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
          * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
          * @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
          * @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
+         * @param {Boolean} [attrs.minimized] - Should the chat be
+         *   created in minimized state.
          * @param {Boolean} [force=false] - By default, a minimized
          * @param {Boolean} [force=false] - By default, a minimized
          *   chat won't be maximized (in `overlayed` view mode) and in
          *   chat won't be maximized (in `overlayed` view mode) and in
          *   `fullscreen` view mode a newly opened chat won't replace
          *   `fullscreen` view mode a newly opened chat won't replace
@@ -41558,22 +41556,21 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
          *     }
          *     }
          * });
          * });
          */
          */
-        'open'(jids, attrs, force) {
-          return new Promise((resolve, reject) => {
-            Promise.all([_converse.api.waitUntil('rosterContactsFetched'), _converse.api.waitUntil('chatBoxesFetched')]).then(() => {
-              if (_.isUndefined(jids)) {
-                const err_msg = "chats.open: You need to provide at least one JID";
-
-                _converse.log(err_msg, Strophe.LogLevel.ERROR);
-
-                reject(new Error(err_msg));
-              } else if (_.isString(jids)) {
-                resolve(_converse.api.chats.create(jids, attrs).maybeShow(force));
-              } else {
-                resolve(_.map(jids, jid => _converse.api.chats.create(jid, attrs).maybeShow(force)));
-              }
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
-          });
+        async open(jids, attrs, force) {
+          await Promise.all([_converse.api.waitUntil('rosterContactsFetched'), _converse.api.waitUntil('chatBoxesFetched')]);
+
+          if (_.isString(jids)) {
+            const chat = await _converse.api.chats.create(jids, attrs);
+            return chat.maybeShow(force);
+          } else if (_.isArray(jids)) {
+            return Promise.all(jids.map(j => _converse.api.chats.create(j, attrs).then(c => c.maybeShow(force))));
+          }
+
+          const err_msg = "chats.open: You need to provide at least one JID";
+
+          _converse.log(err_msg, Strophe.LogLevel.ERROR);
+
+          throw new Error(err_msg);
         },
         },
 
 
         /**
         /**
@@ -41596,7 +41593,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
          * const models = _converse.api.chats.get();
          * const models = _converse.api.chats.get();
          *
          *
          */
          */
-        'get'(jids) {
+        get(jids) {
           if (_.isUndefined(jids)) {
           if (_.isUndefined(jids)) {
             const result = [];
             const result = [];
 
 
@@ -45011,7 +45008,6 @@ const _converse$env = _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].env
       $build = _converse$env.$build,
       $build = _converse$env.$build,
       $msg = _converse$env.$msg,
       $msg = _converse$env.$msg,
       $pres = _converse$env.$pres,
       $pres = _converse$env.$pres,
-      b64_sha1 = _converse$env.b64_sha1,
       sizzle = _converse$env.sizzle,
       sizzle = _converse$env.sizzle,
       f = _converse$env.f,
       f = _converse$env.f,
       moment = _converse$env.moment,
       moment = _converse$env.moment,
@@ -45152,7 +45148,6 @@ _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins.add('converse-muc
        */
        */
       settings.type = _converse.CHATROOMS_TYPE;
       settings.type = _converse.CHATROOMS_TYPE;
       settings.id = jid;
       settings.id = jid;
-      settings.box_id = b64_sha1(jid);
 
 
       const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
       const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
 
 
@@ -45170,7 +45165,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins.add('converse-muc
 
 
     _converse.ChatRoom = _converse.ChatBox.extend({
     _converse.ChatRoom = _converse.ChatBox.extend({
       defaults() {
       defaults() {
-        return _.assign(_.clone(_converse.ChatBox.prototype.defaults), {
+        return {
           // For group chats, we distinguish between generally unread
           // For group chats, we distinguish between generally unread
           // messages and those ones that specifically mention the
           // messages and those ones that specifically mention the
           // user.
           // user.
@@ -45181,19 +45176,33 @@ _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins.add('converse-muc
           // generally unread messages (which *includes* mentions!).
           // generally unread messages (which *includes* mentions!).
           'num_unread_general': 0,
           'num_unread_general': 0,
           'affiliation': null,
           'affiliation': null,
+          'bookmarked': false,
+          'chat_state': undefined,
           'connection_status': _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].ROOMSTATUS.DISCONNECTED,
           'connection_status': _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].ROOMSTATUS.DISCONNECTED,
+          'description': '',
+          'hidden': _.includes(['mobile', 'fullscreen'], _converse.view_mode),
+          'message_type': 'groupchat',
           'name': '',
           'name': '',
           'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
           'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
-          'description': '',
+          'num_unread': 0,
           'roomconfig': {},
           'roomconfig': {},
-          'type': _converse.CHATROOMS_TYPE,
-          'message_type': 'groupchat'
-        });
+          'time_opened': this.get('time_opened') || moment().valueOf(),
+          'type': _converse.CHATROOMS_TYPE
+        };
       },
       },
 
 
       initialize() {
       initialize() {
-        this.constructor.__super__.initialize.apply(this, arguments);
+        if (_converse.vcards) {
+          this.vcard = _converse.vcards.findWhere({
+            'jid': this.get('jid')
+          }) || _converse.vcards.create({
+            'jid': this.get('jid')
+          });
+        }
 
 
+        this.set('box_id', `box-${btoa(this.get('jid'))}`);
+        this.initMessages();
+        this.on('change:chat_state', this.sendChatState, this);
         this.on('change:connection_status', this.onConnectionStatusChanged, this);
         this.on('change:connection_status', this.onConnectionStatusChanged, this);
 
 
         const storage = _converse.config.get('storage');
         const storage = _converse.config.get('storage');
@@ -46561,7 +46570,6 @@ _converse_core__WEBPACK_IMPORTED_MODULE_3__["default"].plugins.add('converse-muc
       jid = jid.toLowerCase();
       jid = jid.toLowerCase();
       attrs.type = _converse.CHATROOMS_TYPE;
       attrs.type = _converse.CHATROOMS_TYPE;
       attrs.id = jid;
       attrs.id = jid;
-      attrs.box_id = b64_sha1(jid);
       return _converse.chatboxes.getChatBox(jid, attrs, create);
       return _converse.chatboxes.getChatBox(jid, attrs, create);
     };
     };
 
 
@@ -48112,28 +48120,6 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
     /********** Event Handlers *************/
     /********** Event Handlers *************/
 
 
 
 
-    function addRelatedContactToChatbox(chatbox, contact) {
-      if (!_.isUndefined(contact)) {
-        chatbox.contact = contact;
-        chatbox.trigger('contactAdded', contact);
-      }
-    }
-
-    _converse.api.waitUntil('rosterContactsFetched').then(() => {
-      _converse.roster.on('add', contact => {
-        /* When a new contact is added, check if we already have a
-         * chatbox open for it, and if so attach it to the chatbox.
-         */
-        const chatbox = _converse.chatboxes.findWhere({
-          'jid': contact.get('jid')
-        });
-
-        if (chatbox) {
-          addRelatedContactToChatbox(chatbox, contact);
-        }
-      });
-    });
-
     function updateUnreadCounter(chatbox) {
     function updateUnreadCounter(chatbox) {
       const contact = _converse.roster.findWhere({
       const contact = _converse.roster.findWhere({
         'jid': chatbox.get('jid')
         'jid': chatbox.get('jid')
@@ -48149,11 +48135,10 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
     _converse.api.listen.on('chatBoxesInitialized', () => {
     _converse.api.listen.on('chatBoxesInitialized', () => {
       _converse.chatboxes.on('change:num_unread', updateUnreadCounter);
       _converse.chatboxes.on('change:num_unread', updateUnreadCounter);
 
 
-      _converse.chatboxes.on('add', async chatbox => {
-        await _converse.api.waitUntil('rosterContactsFetched');
-        addRelatedContactToChatbox(chatbox, _converse.roster.findWhere({
-          'jid': chatbox.get('jid')
-        }));
+      _converse.chatboxes.on('add', chatbox => {
+        if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+          chatbox.setRosterContact(chatbox.get('jid'));
+        }
       });
       });
     });
     });
 
 
@@ -48169,7 +48154,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
         });
         });
 
 
         if (chatbox) {
         if (chatbox) {
-          addRelatedContactToChatbox(chatbox, contact);
+          chatbox.setRosterContact(contact.get('jid'));
         }
         }
       });
       });
     });
     });
@@ -48256,20 +48241,20 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
          * @method _converse.api.contacts.get
          * @method _converse.api.contacts.get
          * @params {(string[]|string)} jid|jids The JID or JIDs of
          * @params {(string[]|string)} jid|jids The JID or JIDs of
          *      the contacts to be returned.
          *      the contacts to be returned.
-         * @returns {(RosterContact[]|RosterContact)} [Backbone.Model](http://backbonejs.org/#Model)
-         *      (or an array of them) representing the contact.
+         * @returns {promise} Promise which resolves with the
+         *  _converse.RosterContact (or an array of them) representing the contact.
          *
          *
          * @example
          * @example
          * // Fetch a single contact
          * // Fetch a single contact
          * _converse.api.listen.on('rosterContactsFetched', function () {
          * _converse.api.listen.on('rosterContactsFetched', function () {
-         *     const contact = _converse.api.contacts.get('buddy@example.com')
+         *     const contact = await _converse.api.contacts.get('buddy@example.com')
          *     // ...
          *     // ...
          * });
          * });
          *
          *
          * @example
          * @example
          * // To get multiple contacts, pass in an array of JIDs:
          * // To get multiple contacts, pass in an array of JIDs:
          * _converse.api.listen.on('rosterContactsFetched', function () {
          * _converse.api.listen.on('rosterContactsFetched', function () {
-         *     const contacts = _converse.api.contacts.get(
+         *     const contacts = await _converse.api.contacts.get(
          *         ['buddy1@example.com', 'buddy2@example.com']
          *         ['buddy1@example.com', 'buddy2@example.com']
          *     )
          *     )
          *     // ...
          *     // ...
@@ -48278,14 +48263,14 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
          * @example
          * @example
          * // To return all contacts, simply call ``get`` without any parameters:
          * // To return all contacts, simply call ``get`` without any parameters:
          * _converse.api.listen.on('rosterContactsFetched', function () {
          * _converse.api.listen.on('rosterContactsFetched', function () {
-         *     const contacts = _converse.api.contacts.get();
+         *     const contacts = await _converse.api.contacts.get();
          *     // ...
          *     // ...
          * });
          * });
          */
          */
-        'get'(jids) {
-          const _getter = function _getter(jid) {
-            return _converse.roster.get(Strophe.getBareJidFromJid(jid)) || null;
-          };
+        async get(jids) {
+          await _converse.api.waitUntil('rosterContactsFetched');
+
+          const _getter = jid => _converse.roster.get(Strophe.getBareJidFromJid(jid));
 
 
           if (_.isUndefined(jids)) {
           if (_.isUndefined(jids)) {
             jids = _converse.roster.pluck('jid');
             jids = _converse.roster.pluck('jid');

+ 3 - 1
src/templates/chatbox_head.html

@@ -2,7 +2,9 @@
     <div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>
     <div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>
     <div class="chatbox-title">
     <div class="chatbox-title">
         <div class="row no-gutters">
         <div class="row no-gutters">
-            <canvas class="avatar" height="36" width="36"></canvas>
+            {[ if (o.type !== o._converse.HEADLINES_TYPE) { ]}
+                <canvas class="avatar" height="36" width="36"></canvas>
+            {[ } ]}
             <div class="col chat-title" title="{{{o.jid}}}">
             <div class="col chat-title" title="{{{o.jid}}}">
                 {[ if (o.url) { ]}
                 {[ if (o.url) { ]}
                     <a href="{{{o.url}}}" target="_blank" rel="noopener" class="user">
                     <a href="{{{o.url}}}" target="_blank" rel="noopener" class="user">