2
0
Эх сурвалжийг харах

Initial work on using IndexedDB. updates #1105

Depend on latest backbone.browserStorage which has support for IndexedDB
via localforage.

Storage operations are now asynchronous and transactional.

Bugs fixed (mostly by waiting for operations to complete):

* Rooms are now fetched asynchronously, so wait before triggering `show`
  or when closing.
* Make sure chat create/update transactions complete before firing events
* Make sure chats and messages have been fetched before creating new ones.
* When doing a `fetch` with `wait: false` on a collection and then
  creating a model in that collection, then once the read
  operation finishes (after creating the model), the collection is emptied again.
* Patch and wait when saving.
  Otherwise we have previously set attributes overriding later ones.
JC Brand 6 жил өмнө
parent
commit
46aeca6070

+ 37 - 76
css/converse.css

@@ -9321,8 +9321,6 @@ readers do not read off random characters that represent icons */
   --message-text-color: #555;
   --message-receipt-color: #3AA569;
   --save-button-color: #3AA569;
-  --message-avatar-width: 36px;
-  --message-avatar-height: 36px;
   --chat-textarea-color: #666;
   --chat-textarea-background-color: white;
   --chat-textarea-height: 60px;
@@ -9383,8 +9381,6 @@ readers do not read off random characters that represent icons */
   --chatroom-head-border-bottom: 0px;
   --chatroom-width: 400px;
   --chatroom-correcting-color: #fadfd7;
-  --chatroom-badge-color: var(--chatroom-head-color);
-  --chatroom-badge-hover-color: var(--chatroom-head-color-dark);
   --headline-head-color: #E7A151;
   --headline-message-color: #D2842B;
   --chatbox-button-size: 14px;
@@ -9465,8 +9461,6 @@ readers do not read off random characters that represent icons */
   --chatroom-head-description-padding-left: 12px;
   --chatroom-head-border-bottom: 1px solid #EEE;
   --chatroom-correcting-color: #FFFFC0;
-  --chatroom-badge-color: #E77051;
-  --chatroom-badge-hover-color: #D24E2B;
   --occupants-background-color: #F3F3F3;
   /* TODO: find a way to allow that and reflow the chat-area properly.
      * --occupants-max-width: 240px; */
@@ -9570,9 +9564,8 @@ body.converse-fullscreen {
     margin-bottom: 2em;
     margin-left: -2.7em;
     word-spacing: 5px; }
-
-#conversejs-bg .subdued, #conversejs .subdued {
-  opacity: 0.35; }
+  #conversejs-bg .converse-brand__text .subdued {
+    opacity: 0.25; }
 
 #conversejs {
   bottom: 0;
@@ -9581,7 +9574,7 @@ body.converse-fullscreen {
   padding-left: env(safe-area-inset-left);
   padding-right: env(safe-area-inset-right);
   color: var(--text-color);
-  font-family: var(--normal-font);
+  font-family: "Helvetica", "Arial", sans-serif;
   font-size: var(--font-size);
   direction: ltr;
   z-index: 1031; }
@@ -9599,26 +9592,15 @@ body.converse-fullscreen {
   #conversejs .brand-heading-container {
     text-align: center; }
   #conversejs .brand-heading {
+    font-size: 200%;
     display: inline-flex;
-    flex-direction: row;
-    align-items: flex-start;
+    align-items: flex-end;
     font-family: var(--branding-font);
-    color: var(--link-color);
-    margin-bottom: 1em; }
-    #conversejs .brand-heading .brand-name {
-      color: var(--link-color);
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      margin-top: -0.5em; }
-    #conversejs .brand-heading .brand-name__text {
-      font-size: 120%;
-      vertical-align: text-bottom; }
+    color: var(--link-color); }
     #conversejs .brand-heading .converse-svg-logo {
-      color: var(--link-color);
-      height: 1.5em;
+      height: 1.2em;
       margin-right: 0.25em;
-      margin-bottom: -0.25em; }
+      margin-bottom: -0.3em; }
       #conversejs .brand-heading .converse-svg-logo .cls-1 {
         isolation: isolate; }
       #conversejs .brand-heading .converse-svg-logo .cls-2 {
@@ -9629,15 +9611,7 @@ body.converse-fullscreen {
       #conversejs .brand-heading .converse-svg-logo .cls-4 {
         fill: var(--link-color); }
   #conversejs .brand-heading--inverse .converse-svg-logo {
-    margin-bottom: 0em;
-    margin-top: -0.2em; }
-  #conversejs .brand-heading--inverse .byline {
-    margin: 0;
-    font-family: var(--heading-font);
-    font-size: 0.25em;
-    opacity: 0.55;
-    margin-left: -7em;
-    word-spacing: 5px; }
+    margin-bottom: 0em; }
   #conversejs .popover {
     position: fixed; }
   #conversejs .converse-chatboxes {
@@ -9956,13 +9930,13 @@ body.converse-fullscreen {
       background-color: var(--primary-color-dark) !important;
       border-color: transparent !important; }
   #conversejs .badge-groupchat {
-    background-color: var(--chatroom-badge-color);
+    background-color: var(--chatroom-head-color);
     border-color: transparent; }
     #conversejs .badge-groupchat:hover {
-      background-color: var(--chatroom-badge-hover-color);
+      background-color: var(--chatroom-head-color-dark);
       border-color: transparent; }
     #conversejs .badge-groupchat.active {
-      background-color: var(--chatroom-badge-hover-color) !important;
+      background-color: var(--chatroom-head-color-dark) !important;
       border-color: transparent !important; }
   #conversejs .btn-info, #conversejs .badge-info {
     background-color: var(--primary-color);
@@ -10087,7 +10061,7 @@ body.converse-fullscreen {
 
 #conversejs form.converse-form {
   background: white;
-  padding: 1.2rem; }
+  padding: 1.5em; }
   #conversejs form.converse-form legend {
     color: var(--text-color);
     font-size: 125%;
@@ -10810,6 +10784,10 @@ body.converse-fullscreen {
       padding-bottom: 1em; }
       #conversejs #controlbox .conn-feedback p.feedback-subject.error {
         font-weight: bold; }
+  #conversejs #controlbox .brand-heading-container .brand-heading {
+    text-align: center; }
+  #conversejs #controlbox .brand-heading-container .brand-name {
+    font-size: 120%; }
   #conversejs #controlbox #converse-login-panel, #conversejs #controlbox #converse-register-panel {
     padding-top: 0;
     padding-bottom: 0; }
@@ -11029,11 +11007,12 @@ body.converse-fullscreen {
   #conversejs.converse-mobile #controlbox .brand-heading-container {
     flex: 0 0 100%;
     max-width: 100%;
-    margin-top: 5em;
-    margin-bottom: 1em; }
+    margin-bottom: 1em;
+    text-align: center; }
     #conversejs.converse-fullscreen #controlbox .brand-heading-container .brand-heading,
     #conversejs.converse-mobile #controlbox .brand-heading-container .brand-heading {
-      font-size: 500%;
+      font-size: 150%;
+      font-size: 600%;
       padding: 0.7em 0 0 0;
       opacity: 0.8;
       color: var(--brand-heading-color); }
@@ -11152,18 +11131,9 @@ body.converse-fullscreen {
       #conversejs:not(.converse-embedded) .converse-chatboxes.sidebar-open .chatbox:not(#controlbox) {
         display: none; }
       #conversejs:not(.converse-embedded) .converse-chatboxes.sidebar-open #controlbox .controlbox-pane {
-        display: block; } }
-
-#conversejs.converse-overlayed .brand-heading {
-  padding-top: 0.8rem;
-  padding-left: 0.8rem;
-  width: 100%; }
-
-#conversejs.converse-overlayed .converse-svg-logo {
-  height: 1em; }
-
-#conversejs.converse-overlayed #controlbox #converse-login-panel {
-  height: 100%; }
+        display: block; }
+  #conversejs.converse-overlayed .converse-chatboxes .chatbox .box-flyout {
+    margin-left: 30px; } }
 
 #conversejs #converse-modals .set-xmpp-status {
   margin: 1em; }
@@ -11867,8 +11837,7 @@ body.converse-fullscreen {
   font-style: italic; }
 
 #conversejs .message.chat-msg {
-  display: inline-flex;
-  width: 100%;
+  display: flex;
   flex-direction: row;
   overflow: auto;
   padding: 0.25rem 1rem; }
@@ -11901,7 +11870,7 @@ body.converse-fullscreen {
     justify-content: space-between;
     align-items: stretch;
     margin-left: 0.5rem;
-    width: calc(100% - var(--message-avatar-width)); }
+    width: 100%; }
   #conversejs .message.chat-msg .chat-msg__content--action {
     margin-left: 0; }
   #conversejs .message.chat-msg .chat-msg__body {
@@ -11910,18 +11879,14 @@ body.converse-fullscreen {
     justify-content: space-between;
     width: 100%; }
   #conversejs .message.chat-msg .chat-msg__message {
-    display: inline-flex;
+    display: flex;
     flex-direction: column;
-    width: 100%;
-    overflow-wrap: break-word; }
+    width: 100%; }
   #conversejs .message.chat-msg .chat-msg__edit-modal {
     cursor: pointer;
     padding-right: 0.5em; }
   #conversejs .message.chat-msg.headline .chat-msg__body {
     margin-left: 0; }
-  #conversejs .message.chat-msg .chat-msg__subject {
-    font-weight: bold;
-    clear: right; }
   #conversejs .message.chat-msg .chat-msg__text {
     padding: 0;
     color: var(--message-text-color);
@@ -11958,30 +11923,28 @@ body.converse-fullscreen {
   #conversejs .message.chat-msg .chat-msg__avatar {
     margin-top: 0.5em;
     vertical-align: middle;
-    height: var(--message-avatar-height);
-    width: var(--message-avatar-width);
-    min-height: var(--message-avatar-height);
-    min-width: var(--message-avatar-width); }
+    height: 36px;
+    width: 36px;
+    min-height: 36px;
+    min-width: 36px; }
   #conversejs .message.chat-msg .chat-msg__heading {
     width: 100%;
     margin-top: 0.5em;
     padding-right: 0.25rem;
     padding-bottom: 0.25rem;
-    display: flex; }
+    display: block; }
     #conversejs .message.chat-msg .chat-msg__heading .chat-msg__author {
       overflow: hidden;
       text-overflow: ellipsis;
       white-space: nowrap;
       font-family: var(--heading-font);
       font-size: 115%;
-      font-weight: bold;
-      padding-bottom: 1px; }
-    #conversejs .message.chat-msg .chat-msg__heading .badge {
-      margin-left: 0.5em;
-      font-family: var(--normal_font); }
+      font-weight: bold; }
+      #conversejs .message.chat-msg .chat-msg__heading .chat-msg__author .badge {
+        font-size: 80%;
+        font-family: var(--normal_font); }
     #conversejs .message.chat-msg .chat-msg__heading .chat-msg__time {
       padding-left: 0.25em;
-      padding-right: 0.25em;
       color: var(--text-color-lighten-15-percent); }
   #conversejs .message.chat-msg.chat-msg--action .chat-msg__content {
     flex-wrap: wrap;
@@ -11993,8 +11956,6 @@ body.converse-fullscreen {
     margin-top: 0;
     padding-bottom: 0;
     width: auto; }
-    #conversejs .message.chat-msg.chat-msg--action .chat-msg__heading .fa {
-      margin-left: 0.5em; }
   #conversejs .message.chat-msg.chat-msg--action .chat-msg__author {
     font-size: var(--message-font-size); }
   #conversejs .message.chat-msg.chat-msg--action .chat-msg__time {

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 98 - 41
dist/converse.js


+ 2 - 7
package-lock.json

@@ -3482,13 +3482,8 @@
       }
     },
     "backbone.browserStorage": {
-      "version": "0.0.5",
-      "resolved": "https://registry.npmjs.org/backbone.browserStorage/-/backbone.browserStorage-0.0.5.tgz",
-      "integrity": "sha512-Cf8B90EIWyHMm/ReS5yFmFMOXPVNda6QcTFcdyp1RW/1zM3LZF2Nf4U601/seIaEu/X8cRVEKqTINpPKql3sxA==",
-      "requires": {
-        "backbone": "~1.x.x",
-        "underscore": ">=1.4.0"
-      }
+      "version": "github:conversejs/backbone.browserStorage#4514f39fd8ea5f13108eaedce42c5652bb790f61",
+      "from": "github:conversejs/backbone.browserStorage#4514f39fd8ea5f13108eaedce42c5652bb790f61"
     },
     "backbone.nativeview": {
       "version": "0.3.3",

+ 69 - 64
spec/messages.js

@@ -23,7 +23,7 @@
             test_utils.openControlBox();
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             await test_utils.openChatBoxFor(_converse, contact_jid);
-            const view = _converse.chatboxviews.get(contact_jid);
+            const view = await _converse.api.chatviews.get(contact_jid);
             const textarea = view.el.querySelector('textarea.chat-textarea');
 
             textarea.value = 'But soft, what light through yonder airlock breaks?';
@@ -50,8 +50,7 @@
             expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
             expect(view.model.messages.at(0).get('correcting')).toBe(true);
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-            await new Promise((resolve, reject) => view.model.messages.once('rendered', resolve));
-            expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(true);
+            await test_utils.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')));
 
             spyOn(_converse.connection, 'send');
             textarea.value = 'But soft, what light through yonder window breaks?';
@@ -314,7 +313,9 @@
                     .tree();
             await _converse.chatboxes.onMessage(msg);
             await test_utils.waitUntil(() => _converse.api.chats.get().length);
-            const view = _converse.chatboxviews.get(sender_jid);
+            const view = _converse.api.chatviews.get(sender_jid);
+            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+
             msg = $msg({'id': 'aeb214', 'to': _converse.bare_jid})
                 .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
                     .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'}).up()
@@ -522,8 +523,9 @@
                         'to': _converse.bare_jid+'/another-resource',
                         'type': 'chat'
                 }).c('body').t(msgtext).tree();
+
             await _converse.chatboxes.onMessage(msg);
-            await test_utils.waitUntil(() => _converse.api.chats.get().length)
+            await test_utils.waitUntil(() => (_converse.api.chats.get().length > 1))
             const chatbox = _converse.chatboxes.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
                 
@@ -1010,11 +1012,12 @@
             _converse.time_format = 'hh:mm';
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             await test_utils.openChatBoxFor(_converse, contact_jid)
-            const view = _converse.chatboxviews.get(contact_jid);
+            const view = await _converse.api.chatviews.get(contact_jid);
             const message = 'This message is sent from this chatbox';
             await test_utils.sendMessage(view, message);
+            await new Promise((resolve, reject) => view.model.messages.once('rendered', resolve));
 
-            const chatbox = _converse.chatboxes.get(contact_jid);
+            const chatbox = await _converse.api.chats.get(contact_jid);
             expect(chatbox.messages.models.length, 1);
             const msg_object = chatbox.messages.models[0];
 
@@ -1054,8 +1057,9 @@
                     'id': (new Date()).getTime()
                 }).c('body').t('A message').up()
                 .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+
             await new Promise(resolve => _converse.on('chatBoxOpened', resolve));
-            const view = await _converse.chatboxviews.get(sender_jid);
+            const view = _converse.api.chatviews.get(sender_jid);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             jasmine.clock().tick(3*ONE_MINUTE_LATER);
@@ -1066,6 +1070,7 @@
                     'id': (new Date()).getTime()
                 }).c('body').t("Another message 3 minutes later").up()
                 .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             jasmine.clock().tick(11*ONE_MINUTE_LATER);
@@ -1076,6 +1081,7 @@
                     'id': (new Date()).getTime()
                 }).c('body').t("Another message 14 minutes since we started").up()
                 .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             jasmine.clock().tick(1000);
@@ -1352,7 +1358,7 @@
                 _converse.emit('rosterContactsFetched');
                 test_utils.openControlBox();
                 await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300);
-                spyOn(_converse, 'emit');
+                spyOn(_converse, 'emit').and.callThrough();
                 const message = 'This is a received message';
                 const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
                 // We don't already have an open chatbox for this user
@@ -1366,10 +1372,10 @@
                     }).c('body').t(message).up()
                     .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
                 );
-                await test_utils.waitUntil(() => _converse.api.chats.get().length === 2);
+                await test_utils.waitUntil(() => (_converse.api.chats.get().length === 2));
                 const chatbox = _converse.chatboxes.get(sender_jid);
                 expect(chatbox).toBeDefined();
-                const view = _converse.chatboxviews.get(sender_jid);
+                const view = await _converse.api.chatviews.get(sender_jid);
                 expect(view).toBeDefined();
 
                 expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
@@ -1490,6 +1496,7 @@
                     // Check that the chatbox and its view now exist
                     const chatbox = _converse.chatboxes.get(sender_jid);
                     const view = _converse.chatboxviews.get(sender_jid);
+                    await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
                     expect(chatbox).toBeDefined();
                     expect(view).toBeDefined();
@@ -1533,7 +1540,7 @@
                     // We don't already have an open chatbox for this user
                     expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
 
-                    let chatbox = _converse.chatboxes.get(sender_jid);
+                    let chatbox = await _converse.api.chats.get(sender_jid);
                     expect(chatbox).not.toBeDefined();
                     // onMessage is a handler for received XMPP messages
                     await _converse.chatboxes.onMessage(msg);
@@ -1602,7 +1609,7 @@
                     fullname = _.isEmpty(fullname)? _converse.bare_jid: fullname;
                     await _converse.api.chats.open(sender_jid)
                     var msg_text = 'This message will not be sent, due to an error';
-                    const view = _converse.chatboxviews.get(sender_jid);
+                    const view = await _converse.api.chatviews.get(sender_jid);
                     let message = view.model.messages.create({
                         'msgid': '82bc02ce-9651-4336-baf0-fa04762ed8d2',
                         'fullname': fullname,
@@ -1769,7 +1776,7 @@
                 const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
                 const message = 'This message is received while the chat area is scrolled up';
                 await test_utils.openChatBoxFor(_converse, sender_jid)
-                const view = _converse.chatboxviews.get(sender_jid);
+                const view = await _converse.api.chatviews.get(sender_jid);
                 spyOn(view, 'onScrolledDown').and.callThrough();
                 // Create enough messages so that there's a scrollbar.
                 const promises = [];
@@ -1851,6 +1858,7 @@
                 await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => _converse.chatboxviews.keys().length > 1, 1000);
                 const view = _converse.chatboxviews.get(sender_jid);
+                await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 await test_utils.waitUntil(() => view.model.messages.length);
                 expect(_converse.chatboxes.getChatBox).toHaveBeenCalled();
                 var chat_content = $(view.el).find('.chat-content:last')[0];
@@ -1872,7 +1880,7 @@
                 _converse.emit('rosterContactsFetched');
                 const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
                 await test_utils.openChatBoxFor(_converse, contact_jid);
-                const view = _converse.chatboxviews.get(contact_jid);
+                const view = await _converse.api.chatviews.get(contact_jid);
                 spyOn(view.model, 'sendMessage').and.callThrough();
 
                 let stanza = u.toStanza(`
@@ -1923,7 +1931,7 @@
                 _converse.emit('rosterContactsFetched');
                 const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
                 await test_utils.openChatBoxFor(_converse, contact_jid)
-                const view = _converse.chatboxviews.get(contact_jid);
+                const view = await _converse.api.chatviews.get(contact_jid);
                 spyOn(view.model, 'sendMessage').and.callThrough();
 
                 let stanza = u.toStanza(`
@@ -1972,7 +1980,7 @@
                 _converse.emit('rosterContactsFetched');
                 const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
                 await test_utils.openChatBoxFor(_converse, contact_jid);
-                const view = _converse.chatboxviews.get(contact_jid);
+                const view = await _converse.api.chatviews.get(contact_jid);
                 spyOn(view.model, 'sendMessage').and.callThrough();
                 const stanza = u.toStanza(`
                     <message from="${contact_jid}"
@@ -2279,7 +2287,7 @@
             await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
             const jid = 'lounge@localhost';
             const room = _converse.api.rooms.get(jid);
-            const view = _converse.api.chatviews.get(jid);
+            const view = await _converse.api.chatviews.get(jid);
             const stanza = $pres({
                     to: 'dummy@localhost/_converse.js-29092160',
                     from: 'coven@chat.shakespeare.lit/newguy'
@@ -2628,7 +2636,7 @@
                     async function (done, _converse) {
 
                 await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'tom');
-                const view = _converse.chatboxviews.get('lounge@localhost');
+                const view = await _converse.chatviews.get('lounge@localhost');
                 ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => {
                     _converse.connection._dataRecv(test_utils.createRequest(
                         $pres({
@@ -2671,7 +2679,7 @@
                     async function (done, _converse) {
 
                 await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'tom');
-                const view = _converse.chatboxviews.get('lounge@localhost');
+                const view = await _converse.api.chatviews.get('lounge@localhost');
                 ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh', 'Link Mauve'].forEach((nick) => {
                     _converse.connection._dataRecv(test_utils.createRequest(
                         $pres({
@@ -2732,7 +2740,7 @@
                     async function (done, _converse) {
 
                 await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'tom');
-                const view = _converse.chatboxviews.get('lounge@localhost');
+                const view = await _converse.api.chatviews.get('lounge@localhost');
                 ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => {
                     _converse.connection._dataRecv(test_utils.createRequest(
                         $pres({
@@ -2803,50 +2811,47 @@
             it("includes XEP-0372 references to that person",
                 mock.initConverse(
                     null, ['rosterGroupsFetched'], {},
-                        function (done, _converse) {
-
-                test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'tom')
-                .then(() => {
-                    const view = _converse.chatboxviews.get('lounge@localhost');
-                    ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => {
-                        _converse.connection._dataRecv(test_utils.createRequest(
-                            $pres({
-                                'to': 'tom@localhost/resource',
-                                'from': `lounge@localhost/${nick}`
-                            })
-                            .c('x', {xmlns: Strophe.NS.MUC_USER})
-                            .c('item', {
-                                'affiliation': 'none',
-                                'jid': `${nick}@localhost/resource`,
-                                'role': 'participant'
-                            })));
-                    });
+                        async function (done, _converse) {
 
-                    spyOn(_converse.connection, 'send');
-                    const textarea = view.el.querySelector('textarea.chat-textarea');
-                    textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?'
-                    const enter_event = {
-                        'target': textarea,
-                        'preventDefault': _.noop,
-                        'stopPropagation': _.noop,
-                        'keyCode': 13 // Enter
-                    }
-                    view.keyPressed(enter_event);
-
-                    const msg = _converse.connection.send.calls.all()[0].args[0];
-                    expect(msg.toLocaleString())
-                        .toBe(`<message from="dummy@localhost/resource" id="${msg.nodeTree.getAttribute("id")}" `+
-                                `to="lounge@localhost" type="groupchat" `+
-                                `xmlns="jabber:client">`+
-                                    `<body>hello z3r0 gibson mr.robot, how are you?</body>`+
-                                    `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
-                                    `<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@localhost" xmlns="urn:xmpp:reference:0"/>`+
-                                    `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@localhost" xmlns="urn:xmpp:reference:0"/>`+
-                                    `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@localhost" xmlns="urn:xmpp:reference:0"/>`+
-                                    `<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
-                              `</message>`);
-                    done();
-                }).catch(_.partial(console.error, _));
+                await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'tom');
+                const view = await _converse.api.chatviews.get('lounge@localhost');
+                ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => {
+                    _converse.connection._dataRecv(test_utils.createRequest(
+                        $pres({
+                            'to': 'tom@localhost/resource',
+                            'from': `lounge@localhost/${nick}`
+                        })
+                        .c('x', {xmlns: Strophe.NS.MUC_USER})
+                        .c('item', {
+                            'affiliation': 'none',
+                            'jid': `${nick}@localhost/resource`,
+                            'role': 'participant'
+                        })));
+                });
+
+                spyOn(_converse.connection, 'send');
+                const textarea = view.el.querySelector('textarea.chat-textarea');
+                textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?'
+                const enter_event = {
+                    'target': textarea,
+                    'preventDefault': _.noop,
+                    'stopPropagation': _.noop,
+                    'keyCode': 13 // Enter
+                }
+                view.keyPressed(enter_event);
+
+                const msg = _converse.connection.send.calls.all()[0].args[0];
+                expect(msg.toLocaleString())
+                    .toBe(`<message from="dummy@localhost/resource" id="${msg.nodeTree.getAttribute("id")}" `+
+                            `to="lounge@localhost" type="groupchat" `+
+                            `xmlns="jabber:client">`+
+                                `<body>hello z3r0 gibson mr.robot, how are you?</body>`+
+                                `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
+                                `<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@localhost" xmlns="urn:xmpp:reference:0"/>`+
+                                `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@localhost" xmlns="urn:xmpp:reference:0"/>`+
+                                `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@localhost" xmlns="urn:xmpp:reference:0"/>`+
+                            `</message>`);
+                done();
             }));
         });
     });

+ 13 - 19
src/converse-bookmarks.js

@@ -237,19 +237,19 @@ converse.plugins.add('converse-bookmarks', {
             comparator: (item) => item.get('name').toLowerCase(),
 
             initialize () {
-                this.on('add', _.flow(this.openBookmarkedRoom, this.markRoomAsBookmarked));
+                this.on('add', b => this.openBookmarkedRoom(b).then(b => this.markRoomAsBookmarked(b)));
                 this.on('remove', this.markRoomAsUnbookmarked, this);
                 this.on('remove', this.sendBookmarkStanza, this);
 
                 const storage = _converse.config.get('storage'),
-                      cache_key = `converse.room-bookmarks${_converse.bare_jid}`;
-                this.fetched_flag = cache_key+'fetched';
-                this.browserStorage = new Backbone.BrowserStorage[storage](cache_key);
+                      cache_key = `converse.room-bookmarks-${_converse.bare_jid}`;
+                this.bookmarks_cached = `${cache_key}--cached`;
+                this.browserStorage = new Backbone.BrowserStorage(cache_key, storage);
             },
 
-            openBookmarkedRoom (bookmark) {
+            async openBookmarkedRoom (bookmark) {
                 if (bookmark.get('autojoin')) {
-                    const groupchat = _converse.api.rooms.create(bookmark.get('jid'), bookmark.get('nick'));
+                    const groupchat = await _converse.api.rooms.create(bookmark.get('jid'), bookmark.get('nick'));
                     if (!groupchat.get('hidden')) {
                         groupchat.trigger('show');
                     }
@@ -259,19 +259,13 @@ converse.plugins.add('converse-bookmarks', {
 
             fetchBookmarks () {
                 const deferred = u.getResolveablePromise();
-                if (this.browserStorage.records.length > 0) {
+                if (sessionStorage.getItem(this.bookmarks_cached)) {
                     this.fetch({
                         'success': _.bind(this.onCachedBookmarksFetched, this, deferred),
                         'error':  _.bind(this.onCachedBookmarksFetched, this, deferred)
                     });
-                } else if (! window.sessionStorage.getItem(this.fetched_flag)) {
-                    // There aren't any cached bookmarks and the
-                    // `fetched_flag` is off, so we query the XMPP server.
-                    // If nothing is returned from the XMPP server, we set
-                    // the `fetched_flag` to avoid calling the server again.
-                    this.fetchBookmarksFromServer(deferred);
                 } else {
-                    deferred.resolve();
+                    this.fetchBookmarksFromServer(deferred);
                 }
                 return deferred;
             },
@@ -367,13 +361,14 @@ converse.plugins.add('converse-bookmarks', {
 
             onBookmarksReceived (deferred, iq) {
                 this.createBookmarksFromStanza(iq);
+                sessionStorage.setItem(this.bookmarks_cached, true);
                 if (!_.isUndefined(deferred)) {
                     return deferred.resolve();
                 }
             },
 
             onBookmarksReceivedError (deferred, iq) {
-                window.sessionStorage.setItem(this.fetched_flag, true);
+                sessionStorage.setItem(this.bookmarks_cached, true);
                 _converse.log('Error while fetching bookmarks', Strophe.LogLevel.WARN);
                 _converse.log(iq.outerHTML, Strophe.LogLevel.DEBUG);
                 if (!_.isNil(deferred)) {
@@ -433,10 +428,9 @@ converse.plugins.add('converse-bookmarks', {
                 _converse.chatboxes.on('add', this.renderBookmarkListElement, this);
                 _converse.chatboxes.on('remove', this.renderBookmarkListElement, this);
 
-                const storage = _converse.config.get('storage'),
-                      id = `converse.room-bookmarks${_converse.bare_jid}-list-model`;
+                const id = `converse.room-bookmarks${_converse.bare_jid}-list-model`;
                 this.list_model = new _converse.BookmarksList({'id': id});
-                this.list_model.browserStorage = new Backbone.BrowserStorage[storage](id);
+                this.list_model.browserStorage = new _converse.BrowserStorage(id);
                 this.list_model.fetch();
                 this.render();
                 this.sortAndPositionAllItems();
@@ -545,7 +539,7 @@ converse.plugins.add('converse-bookmarks', {
         _converse.on('clearSession', () => {
             if (!_.isUndefined(_converse.bookmarks)) {
                 _converse.bookmarks.browserStorage._clear();
-                window.sessionStorage.removeItem(_converse.bookmarks.fetched_flag);
+                window.sessionStorage.removeItem(_converse.bookmarks.bookmarks_cached);
             }
         });
 

+ 13 - 3
src/converse-chatboxviews.js

@@ -61,9 +61,7 @@ converse.plugins.add('converse-chatboxviews', {
         const { _converse } = this,
               { __ } = _converse;
 
-        _converse.api.promises.add([
-            'chatBoxViewsInitialized'
-        ]);
+        _converse.api.promises.add(['chatBoxViewsInitialized']);
 
         // Configuration values for this plugin
         // ====================================
@@ -152,6 +150,18 @@ converse.plugins.add('converse-chatboxviews', {
 
 
         /************************ BEGIN Event Handlers ************************/
+        _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) {
+                    chatbox.addRelatedContact(contact);
+                }
+            });
+        });
+
         _converse.api.listen.on('chatBoxesInitialized', () => {
             _converse.chatboxviews = new _converse.ChatBoxViews({
                 'model': _converse.chatboxes

+ 61 - 39
src/converse-chatview.js

@@ -28,7 +28,7 @@ import tpl_user_details_modal from "templates/user_details_modal.html";
 import u from "@converse/headless/utils/emoji";
 import xss from "xss";
 
-const { $msg, Backbone, Promise, Strophe, _, b64_sha1, f, sizzle, moment } = converse.env;
+const { $msg, Backbone, Promise, Strophe, _, f, sizzle, moment } = converse.env;
 
 
 converse.plugins.add('converse-chatview', {
@@ -301,7 +301,7 @@ converse.plugins.add('converse-chatview', {
                 'drop .chat-textarea': 'onDrop',
             },
 
-            initialize () {
+            async initialize () {
                 this.initDebounced();
 
                 this.model.messages.on('add', this.onMessageAdded, this);
@@ -314,7 +314,8 @@ converse.plugins.add('converse-chatview', {
                 this.model.on('showHelpMessages', this.showHelpMessages, this);
                 this.render();
 
-                this.fetchMessages();
+                await this.model.fetchMessages();
+                this.afterMessagesFetched();
                 _converse.emit('chatBoxOpened', this);
                 _converse.emit('chatBoxInitialized', this);
             },
@@ -487,15 +488,6 @@ converse.plugins.add('converse-chatview', {
                 _converse.emit('afterMessagesFetched', this);
             },
 
-            fetchMessages () {
-                this.model.messages.fetch({
-                    'add': true,
-                    'success': this.afterMessagesFetched.bind(this),
-                    'error': this.afterMessagesFetched.bind(this),
-                });
-                return this;
-            },
-
             insertIntoDOM () {
                 /* This method gets overridden in src/converse-controlbox.js
                  * as well as src/converse-muc.js (if those plugins are
@@ -1049,7 +1041,7 @@ converse.plugins.add('converse-chatview', {
                     const storage = _converse.config.get('storage'),
                           id = `converse.emoji-${_converse.bare_jid}`;
                     _converse.emojipicker = new _converse.EmojiPicker({'id': id});
-                    _converse.emojipicker.browserStorage = new Backbone.BrowserStorage[storage](id);
+                    _converse.emojipicker.browserStorage = new _converse.BrowserStorage(id);
                     _converse.emojipicker.fetch();
                 }
                 this.emoji_picker_view = new _converse.EmojiPickerView({
@@ -1144,7 +1136,7 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
-            close (ev) {
+            async close (ev) {
                 if (ev && ev.preventDefault) { ev.preventDefault(); }
                 if (Backbone.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) {
                     _converse.router.navigate('');
@@ -1156,7 +1148,7 @@ converse.plugins.add('converse-chatview', {
                     this.model.sendChatState();
                 }
                 try {
-                    this.model.destroy();
+                    await new Promise((success, error) => this.model.destroy({success, error}));
                 } catch (e) {
                     _converse.log(e, Strophe.LogLevel.ERROR);
                 }
@@ -1306,33 +1298,63 @@ converse.plugins.add('converse-chatview', {
              * @memberOf _converse.api
              */
             'chatviews': {
-                 /**
-                  * Get the view of an already open chat.
-                  *
-                  * @method _converse.api.chatviews.get
-                  * @returns {ChatBoxView} A [Backbone.View](http://backbonejs.org/#View) instance.
-                  *     The chat should already be open, otherwise `undefined` will be returned.
-                  *
-                  * @example
-                  * // To return a single view, provide the JID of the contact:
-                  * _converse.api.chatviews.get('buddy@example.com')
-                  *
-                  * @example
-                  * // To return an array of views, provide an array of JIDs:
-                  * _converse.api.chatviews.get(['buddy1@example.com', 'buddy2@example.com'])
-                  */
-                'get' (jids) {
-                    if (_.isUndefined(jids)) {
-                        _converse.log(
-                            "chats.create: You need to provide at least one JID",
-                            Strophe.LogLevel.ERROR
-                        );
+                /**
+                 * Retrieves a chat view. The chat should already be open.
+                 *
+                 * @method _converse.api.chatviews.get
+                 * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
+                 * @returns {(Backbone.View|Array)} The Backbone.View representing the chatview, or an array of views.
+                 *
+                 * @example
+                 * // To return a single view, provide the JID of the contact you're chatting with in that chat:
+                 * const view = _converse.api.chatviews.get('buddy@example.com');
+                 *
+                 * @example
+                 * // To return an array of views, provide an array of JIDs:
+                 * const views = _converse.api.chatviews.get(['buddy1@example.com', 'buddy2@example.com']);
+                 *
+                 * @example
+                 * // To return all open views, call the method without any parameters::
+                 * const views = _converse.api.chatviews.get();
+                 */
+                get (jids) {
+                    const chats = _converse.api.chats.get(jids);
+                    if (_.isUndefined(chats)) {
                         return null;
                     }
-                    if (_.isString(jids)) {
-                        return _converse.chatboxviews.get(jids);
+                    if (_.isArray(chats)) {
+                        return chats.map(chat => _converse.chatboxviews.get(chat.get('jid')));
+                    } else {
+                        return _converse.chatboxviews.get(chats.get('jid'));
+                    }
+                },
+
+                /**
+                 * Opens a chat view.
+                 *
+                 * @method _converse.api.chatviews.open
+                 * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
+                 * @returns {Promise} A Promise which resolves with the view or array of views.
+                 *
+                 * @example
+                 * // To return a single view, provide the JID of the contact you're chatting with in that chat:
+                 * const view = _converse.api.chatviews.open('buddy@example.com');
+                 *
+                 * @example
+                 * // To return an array of views, provide an array of JIDs:
+                 * const views = _converse.api.chatviews.open(['buddy1@example.com', 'buddy2@example.com']);
+                 *
+                 * @example
+                 * // To return all open views, call the method without any parameters::
+                 * const views = _converse.api.chatviews.open();
+                 */
+                async open (jids) {
+                    const chats = await _converse.api.chats.open(jids);
+                    if (_.isArray(chats)) {
+                        return chats.map(chat => _converse.chatboxviews.get(chat.get('jid')));
+                    } else {
+                        return _converse.chatboxviews.get(chats.get('jid'));
                     }
-                    return _.map(jids, (jid) => _converse.chatboxviews.get(jids));
                 }
             }
         });

+ 1 - 1
src/converse-controlbox.js

@@ -627,7 +627,7 @@ converse.plugins.add('converse-controlbox', {
 
         _converse.on('chatBoxesFetched', () => {
             const controlbox = _converse.chatboxes.get('controlbox') || addControlBox();
-            controlbox.save({connected:true});
+            controlbox.save({'connected': true}, {'patch': true});
         });
 
         const disconnect =  function () {

+ 5 - 2
src/converse-headline.js

@@ -71,7 +71,7 @@ converse.plugins.add('converse-headline', {
                 'keypress textarea.chat-textarea': 'keyPressed'
             },
 
-            initialize () {
+            async initialize () {
                 this.initDebounced();
 
                 this.disable_mam = true; // Don't do MAM queries for this box
@@ -80,7 +80,10 @@ converse.plugins.add('converse-headline', {
                 this.model.on('destroy', this.hide, this);
                 this.model.on('change:minimized', this.onMinimizedChanged, this);
 
-                this.render().insertHeading().fetchMessages().insertIntoDOM().hide();
+                this.render().insertHeading()
+                await this.model.fetchMessages()
+                this.hide();
+                this.insertIntoDOM();
                 _converse.emit('chatBoxOpened', this);
                 _converse.emit('chatBoxInitialized', this);
             },

+ 2 - 2
src/converse-minimize.js

@@ -414,11 +414,11 @@ converse.plugins.add('converse-minimize', {
 
             initToggle () {
                 const storage = _converse.config.get('storage'),
-                      id = `converse.minchatstoggle${_converse.bare_jid}`;
+                      id = `converse.minchatstoggle-${_converse.bare_jid}`;
                 this.toggleview = new _converse.MinimizedChatsToggleView({
                     'model': new _converse.MinimizedChatsToggle({'id': id})
                 });
-                this.toggleview.model.browserStorage = new Backbone.BrowserStorage[storage](id);
+                this.toggleview.model.browserStorage = new _converse.BrowserStorage(id);
                 this.toggleview.model.fetch();
             },
 

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

@@ -61,11 +61,11 @@ converse.plugins.add('converse-muc-views', {
                 if (this.roomspanel && u.isVisible(this.roomspanel.el)) {
                     return;
                 }
+                const id = `converse.roomspanel-${_converse.bare_jid}`;
                 this.roomspanel = new _converse.RoomsPanel({
                     'model': new (_converse.RoomsPanelModel.extend({
-                        'id': `converse.roomspanel${_converse.bare_jid}`, // Required by web storage
-                        'browserStorage': new Backbone.BrowserStorage[_converse.config.get('storage')](
-                            `converse.roomspanel${_converse.bare_jid}`)
+                        'id': id,
+                        'browserStorage': new _converse.BrowserStorage(id)
                     }))()
                 });
                 this.roomspanel.model.fetch();
@@ -512,23 +512,20 @@ converse.plugins.add('converse-muc-views', {
                 this.enterRoom();
             },
 
-            enterRoom (ev) {
+            async enterRoom (ev) {
                 if (ev) { ev.preventDefault(); }
                 if (this.model.get('connection_status') !==  converse.ROOMSTATUS.ENTERED) {
-                    const handler = () => {
-                        if (!u.isPersistableModel(this.model)) {
-                            // Happens during tests, nothing to do if this
-                            // is a hanging chatbox (i.e. not in the collection anymore).
-                            return;
-                        }
-                        this.populateAndJoin();
-                        _converse.emit('chatRoomOpened', this);
+                    await this.model.getRoomFeatures()
+                    if (!u.isPersistableModel(this.model)) {
+                        // XXX: Happens during tests, nothing to do if this
+                        // is a hanging chatbox (i.e. not in the collection anymore).
+                        return;
                     }
-                    this.model.getRoomFeatures().then(handler, handler);
+                    this.populateAndJoin();
                 } else {
-                    this.fetchMessages();
-                    _converse.emit('chatRoomOpened', this);
+                    this.model.fetchMessages();
                 }
+                _converse.emit('chatRoomOpened', this);
             },
 
             render () {
@@ -713,7 +710,7 @@ converse.plugins.add('converse-muc-views', {
                 );
             },
 
-            close (ev) {
+            async close (ev) {
                 /* Close this chat box, which implies leaving the groupchat as
                  * well.
                  */
@@ -721,8 +718,8 @@ converse.plugins.add('converse-muc-views', {
                 if (Backbone.history.getFragment() === "converse/room?jid="+this.model.get('jid')) {
                     _converse.router.navigate('');
                 }
-                this.model.leave();
-                _converse.ChatBoxView.prototype.close.apply(this, arguments);
+                await this.model.leave();
+                return _converse.ChatBoxView.prototype.close.apply(this, arguments);
             },
 
             setOccupantsVisibility () {
@@ -1082,7 +1079,7 @@ converse.plugins.add('converse-muc-views', {
             populateAndJoin () {
                 this.model.occupants.fetchMembers();
                 this.join();
-                this.fetchMessages();
+                this.model.fetchMessages();
             },
 
             join (nick, password) {

+ 3 - 3
src/converse-omemo.js

@@ -945,7 +945,7 @@ converse.plugins.add('converse-omemo', {
                 this.devices = new _converse.Devices();
                 const id = `converse.devicelist-${_converse.bare_jid}-${this.get('jid')}`;
                 const storage = _converse.config.get('storage');
-                this.devices.browserStorage = new Backbone.BrowserStorage[storage](id);
+                this.devices.browserStorage = new _converse.BrowserStorage(id, _converse.storage[storage]);
                 this.fetchDevices();
             },
 
@@ -1116,7 +1116,7 @@ converse.plugins.add('converse-omemo', {
                 const storage = _converse.config.get('storage'),
                       id = `converse.omemosession-${_converse.bare_jid}`;
                 _converse.omemo_store = new _converse.OMEMOStore({'id': id});
-                _converse.omemo_store.browserStorage = new Backbone.BrowserStorage[storage](id);
+                _converse.omemo_store.browserStorage = new _converse.BrowserStorage(id);
             }
             return _converse.omemo_store.fetchSession();
         }
@@ -1128,7 +1128,7 @@ converse.plugins.add('converse-omemo', {
             _converse.devicelists = new _converse.DeviceLists();
             const storage = _converse.config.get('storage'),
                   id = `converse.devicelists-${_converse.bare_jid}`;
-            _converse.devicelists.browserStorage = new Backbone.BrowserStorage[storage](id);
+            _converse.devicelists.browserStorage = new _converse.BrowserStorage(id);
 
             await fetchOwnDevices();
             await restoreOMEMOSession();

+ 4 - 7
src/converse-roomslist.js

@@ -169,11 +169,9 @@ converse.plugins.add('converse-roomslist', {
                 this.model.on('add', this.showOrHide, this);
                 this.model.on('remove', this.showOrHide, this);
 
-                const storage = _converse.config.get('storage'),
-                      id = `converse.roomslist${_converse.bare_jid}`;
-
+                const id = `converse.roomslist${_converse.bare_jid}`;
                 this.list_model = new _converse.RoomsList({'id': id});
-                this.list_model.browserStorage = new Backbone.BrowserStorage[storage](id);
+                this.list_model.browserStorage = new _converse.BrowserStorage(id);
                 this.list_model.fetch();
                 this.render();
                 this.sortAndPositionAllItems();
@@ -264,11 +262,10 @@ converse.plugins.add('converse-roomslist', {
         });
 
         const initRoomsListView = function () {
-            const storage = _converse.config.get('storage'),
-                  id = `converse.open-rooms-{_converse.bare_jid}`,
+            const id = `converse.open-rooms-{_converse.bare_jid}`,
                   model = new _converse.OpenRooms();
 
-            model.browserStorage = new Backbone.BrowserStorage[storage](id);
+            model.browserStorage = new _converse.BrowserStorage(id);
             _converse.rooms_list_view = new _converse.RoomsListView({'model': model});
             _converse.api.emit('roomsListInitialized');
         };

+ 2 - 2
src/converse-rosterview.js

@@ -803,8 +803,8 @@ converse.plugins.add('converse-rosterview', {
             createRosterFilter () {
                 // Create a model on which we can store filter properties
                 const model = new _converse.RosterFilter();
-                model.id = `_converse.rosterfilter${_converse.bare_jid}`;
-                model.browserStorage = new Backbone.BrowserStorage.local(this.filter.id);
+                model.id = `converse.rosterfilter-${_converse.bare_jid}`;
+                model.browserStorage = new _converse.BrowserStorage(this.filter.id, 'local');
                 this.filter_view = new _converse.RosterFilterView({'model': model});
                 this.filter_view.model.on('change', this.updateFilter, this);
                 this.filter_view.model.fetch();

+ 79 - 54
src/headless/converse-chatboxes.js

@@ -254,8 +254,8 @@ converse.plugins.add('converse-chatboxes', {
 
                 this.messages = new _converse.Messages();
                 const storage = _converse.config.get('storage');
-                this.messages.browserStorage = new Backbone.BrowserStorage[storage](
-                    b64_sha1(`converse.messages${jid}${_converse.bare_jid}`));
+                const id = `converse.messages-${this.get('jid')}-${_converse.bare_jid}`;
+                this.messages.browserStorage = new _converse.BrowserStorage(id, storage);
                 this.messages.chatbox = this;
 
                 this.messages.on('change:upload', (message) => {
@@ -274,7 +274,16 @@ converse.plugins.add('converse-chatboxes', {
                     'box_id' : b64_sha1(this.get('jid')),
                     'time_opened': this.get('time_opened') || moment().valueOf(),
                     'user_id' : Strophe.getNodeFromJid(this.get('jid'))
-                });
+                }, {'wait': true});
+            },
+
+            fetchMessages () {
+                this.messagesFetchedPromise = new Promise((resolve, reject) => this.messages.fetch({
+                        'add': true,
+                        'success': resolve,
+                        'error': reject
+                    })
+                );
             },
 
             validate (attrs, options) {
@@ -698,6 +707,27 @@ converse.plugins.add('converse-chatboxes', {
                 return attrs;
             },
 
+            async createMessage (message, original_stanza) {
+                /* Create a Backbone.Message object inside this chat box
+                 * based on the identified message stanza.
+                 */
+                await this.messagesFetchedPromise;
+                const attrs = await this.getMessageAttributesFromStanza(message, original_stanza),
+                      is_csn = u.isOnlyChatStateNotification(attrs);
+
+                if (is_csn && (attrs.is_delayed ||
+                        (attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick')))) {
+                    // XXX: MUC leakage
+                    // No need showing delayed or our own CSN messages
+                    return;
+                } else if (!is_csn && !attrs.file && !attrs.plaintext && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
+                    // TODO: handle <subject> messages (currently being done by ChatRoom)
+                    return;
+                } else {
+                    return this.messages.create(attrs);
+                }
+            },
+
             isHidden () {
                 /* Returns a boolean to indicate whether a newly received
                  * message will be visible to the user or not.
@@ -777,8 +807,8 @@ converse.plugins.add('converse-chatboxes', {
             },
 
             onConnected () {
-                this.browserStorage = new Backbone.BrowserStorage.session(
-                    `converse.chatboxes-${_converse.bare_jid}`);
+                const id = `converse.chatboxes-${_converse.bare_jid}`;
+                this.browserStorage = new _converse.BrowserStorage(id, 'session');
                 this.registerMessageHandler();
                 this.fetch({
                     'add': true,
@@ -793,7 +823,7 @@ converse.plugins.add('converse-chatboxes', {
                 if (utils.isSameBareJID(from_jid, _converse.bare_jid)) {
                     return true;
                 }
-                const chatbox = this.getChatBox(from_jid);
+                const chatbox = await this.getChatBox(from_jid);
                 if (!chatbox) {
                     return true;
                 }
@@ -923,7 +953,7 @@ converse.plugins.add('converse-chatboxes', {
                 _converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});
             },
 
-            getChatBox (jid, attrs={}, create) {
+            async getChatBox (jid, attrs={}, create) {
                 /* Returns a chat box or optionally return a newly
                  * created one if one doesn't exist.
                  *
@@ -939,14 +969,16 @@ converse.plugins.add('converse-chatboxes', {
                 }
                 jid = Strophe.getBareJidFromJid(jid.toLowerCase());
 
-                let  chatbox = this.get(Strophe.getBareJidFromJid(jid));
+                await _converse.api.waitUntil('chatBoxesFetched');
+                let chatbox = this.get(Strophe.getBareJidFromJid(jid));
                 if (!chatbox && create) {
                     _.extend(attrs, {'jid': jid, 'id': jid});
-                    chatbox = this.create(attrs, {
-                        'error' (model, response) {
-                            _converse.log(response.responseText);
-                        }
-                    });
+                    try {
+                        chatbox = await new Promise((success, error) => this.create(attrs, {success, error}));
+                    } catch (e) {
+                        _converse.log(e, Strophe.LogLevel.ERROR);
+                        throw e;
+                    }
                 }
                 return chatbox;
             }
@@ -1005,8 +1037,9 @@ converse.plugins.add('converse-chatboxes', {
                  * @method _converse.api.chats.create
                  * @param {string|string[]} jid|jids An jid or array of jids
                  * @param {object} attrs An object containing configuration attributes.
+                 * @returns {Promise} Promise which resolves with the Backbone.Model representing the chat.
                  */
-                'create' (jids, attrs) {
+                async create (jids, attrs) {
                     if (_.isUndefined(jids)) {
                         _converse.log(
                             "chats.create: You need to provide at least one JID",
@@ -1018,7 +1051,7 @@ converse.plugins.add('converse-chatboxes', {
                         if (attrs && !_.get(attrs, 'fullname')) {
                             attrs.fullname = _.get(_converse.api.contacts.get(jids), 'attributes.fullname');
                         }
-                        const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true);
+                        const chatbox = await _converse.chatboxes.getChatBox(jids, attrs, true);
                         if (_.isNil(chatbox)) {
                             _converse.log("Could not open chatbox for JID: "+jids, Strophe.LogLevel.ERROR);
                             return;
@@ -1042,11 +1075,10 @@ converse.plugins.add('converse-chatboxes', {
                  * // To open a single chat, provide the JID of the contact you're chatting with in that chat:
                  * converse.plugins.add('myplugin', {
                  *     initialize: function() {
-                 *         var _converse = this._converse;
+                 *         const _converse = this._converse;
                  *         // Note, buddy@example.org must be in your contacts roster!
-                 *         _converse.api.chats.open('buddy@example.com').then((chat) => {
-                 *             // Now you can do something with the chat model
-                 *         });
+                 *         const chat = await _converse.api.chats.open('buddy@example.com');
+                 *         // Now you can do something with the chat model
                  *     }
                  * });
                  *
@@ -1054,40 +1086,41 @@ converse.plugins.add('converse-chatboxes', {
                  * // To open an array of chats, provide an array of JIDs:
                  * converse.plugins.add('myplugin', {
                  *     initialize: function () {
-                 *         var _converse = this._converse;
+                 *         const _converse = this._converse;
                  *         // Note, these users must first be in your contacts roster!
-                 *         _converse.api.chats.open(['buddy1@example.com', 'buddy2@example.com']).then((chats) => {
-                 *             // Now you can do something with the chat models
-                 *         });
+                 *         const chat = await _converse.api.chats.open(['buddy1@example.com', 'buddy2@example.com']);
+                 *         // Now you can do something with the chat models
                  *     }
                  * });
-                 *
                  */
-                'open' (jids, attrs) {
-                    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).trigger('show'));
-                            } else {
-                                resolve(_.map(jids, (jid) => _converse.api.chats.create(jid, attrs).trigger('show')));
-                            }
-                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
-                    });
+                async open (jids, attrs) {
+                    await Promise.all([
+                        _converse.api.waitUntil('rosterContactsFetched'),
+                        _converse.api.waitUntil('chatBoxesFetched')
+                    ]);
+                    if (_.isUndefined(jids)) {
+                        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));
+                    } else if (_.isString(jids)) {
+                        const chat = await _converse.api.chats.create(jids, attrs)
+                        chat.trigger('show');
+                        return chat;
+                    } else {
+                        return Promise.all(_.map(jids, async (jid) => {
+                            const chat = await _converse.api.chats.create(jid, attrs)
+                            chat.trigger('show');
+                            return chat;
+                        }));
+                    }
                 },
 
                 /**
-                 * Returns a chat model. The chat should already be open.
+                 * Retrieves a chat model. The chat should already be open.
                  *
                  * @method _converse.api.chats.get
                  * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
-                 * @returns {Backbone.Model}
+                 * @returns {(Backbone.Model|Array)} The Backbone.Model representing the chat or an array of models.
                  *
                  * @example
                  * // To return a single chat, provide the JID of the contact you're chatting with in that chat:
@@ -1104,19 +1137,11 @@ converse.plugins.add('converse-chatboxes', {
                  */
                 'get' (jids) {
                     if (_.isUndefined(jids)) {
-                        const result = [];
-                        _converse.chatboxes.each(function (chatbox) {
-                            // FIXME: Leaky abstraction from MUC. We need to add a
-                            // base type for chat boxes, and check for that.
-                            if (chatbox.get('type') !== _converse.CHATROOMS_TYPE) {
-                                result.push(chatbox);
-                            }
-                        });
-                        return result;
+                        return _converse.chatboxes.filter(chatbox => (chatbox.get('type') !== _converse.CHATROOMS_TYPE));
                     } else if (_.isString(jids)) {
-                        return _converse.chatboxes.getChatBox(jids);
+                        return _converse.chatboxes.get(jids);
                     }
-                    return _.map(jids, _.partial(_converse.chatboxes.getChatBox.bind(_converse.chatboxes), _, {}, true));
+                    return _.map(jids, _.partial(_converse.chatboxes.get.bind(_converse.chatboxes), _, {}, true));
                 }
             }
         });

+ 45 - 10
src/headless/converse-core.js

@@ -18,9 +18,9 @@ import sizzle from "sizzle";
 import u from "@converse/headless/utils/core";
 
 Backbone = Backbone.noConflict();
+BrowserStorage.patch(Backbone);
 
 // Strophe globals
-const b64_sha1 = SHA1.b64_sha1;
 
 // Add Strophe Namespaces
 Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2');
@@ -321,6 +321,38 @@ _converse.isUniView = function () {
     return _.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode);
 }
 
+
+_converse.initStorage = async function () {
+    /* Set up Backbone.BrowserStorage and localForage for the 3 different stores.
+     */
+    _converse.localStorage = BrowserStorage.localForage.createInstance({
+        'name': 'local',
+        'driver': BrowserStorage.localForage.LOCALSTORAGE
+    });
+    _converse.indexedDB = BrowserStorage.localForage.createInstance({
+        'name': 'indexed',
+        'driver': BrowserStorage.localForage.INDEXEDDB
+    });
+    _converse.sessionStorage = BrowserStorage.localForage.createInstance({
+        'name': 'session'
+    });
+    _converse.storage = {
+        'session': _converse.sessionStorage,
+        'local': _converse.localStorage,
+        'indexed': _converse.indexedDB
+    }
+    await BrowserStorage.sessionStorageInitialized;
+    _converse.sessionStorage.setDriver('sessionStorageWrapper');
+};
+_converse.initStorage();
+
+
+_converse.BrowserStorage = function (id, storage) {
+    const s = storage ? storage : _converse.storage[_converse.config.get('storage')];
+    return new Backbone.BrowserStorage(id, s);
+}
+
+
 _converse.router = new Backbone.Router();
 
 
@@ -361,6 +393,7 @@ function initPlugins() {
     _converse.emit('pluginsInitialized');
 }
 
+
 function initClientConfig () {
     /* The client config refers to configuration of the client which is
      * independent of any particular user.
@@ -373,11 +406,12 @@ function initClientConfig () {
         'trusted': _converse.trusted && true || false,
         'storage': _converse.trusted ? 'local' : 'session'
     });
-    _converse.config.browserStorage = new Backbone.BrowserStorage.session(id);
+    _converse.config.browserStorage = new _converse.BrowserStorage(id, 'session');
     _converse.config.fetch();
     _converse.emit('clientConfigInitialized');
 }
 
+
 _converse.initConnection = function () {
     /* Creates a new Strophe.Connection instance if we don't already have one.
      */
@@ -438,12 +472,13 @@ function unregisterGlobalEventHandlers () {
     _converse.emit('unregisteredGlobalEventHandlers');
 }
 
-function cleanup () {
+
+async function cleanup () {
     // Looks like _converse.initialized was called again without logging
     // out or disconnecting in the previous session.
     // This happens in tests. We therefore first clean up.
     Backbone.history.stop();
-    _converse.chatboxviews.closeAllChatBoxes();
+    await _converse.chatboxviews.closeAllChatBoxes();
     unregisterGlobalEventHandlers();
     window.localStorage.clear();
     window.sessionStorage.clear();
@@ -468,7 +503,7 @@ _converse.initialize = async function (settings, callback) {
     const init_promise = u.getResolveablePromise();
     _.each(PROMISES, addPromise);
     if (!_.isUndefined(_converse.connection)) {
-        cleanup();
+        await cleanup();
     }
 
     if ('onpagehide' in window) {
@@ -784,7 +819,7 @@ _converse.initialize = async function (settings, callback) {
         } else {
             const id = `converse.xmppstatus-${_converse.bare_jid}`;
             this.xmppstatus = new this.XMPPStatus({'id': id});
-            this.xmppstatus.browserStorage = new Backbone.BrowserStorage.session(id);
+            this.xmppstatus.browserStorage = new _converse.BrowserStorage(id, 'session');
             this.xmppstatus.fetch({
                 'success': _.partial(_converse.onStatusInitialized, reconnecting),
                 'error': _.partial(_converse.onStatusInitialized, reconnecting)
@@ -794,9 +829,9 @@ _converse.initialize = async function (settings, callback) {
 
 
     this.initSession = function () {
-        const id = 'converse.bosh-session';
+        const id = `converse.bosh-session-${_converse.bare_jid}`;
         _converse.session = new Backbone.Model({id});
-        _converse.session.browserStorage = new Backbone.BrowserStorage.session(id);
+        _converse.session.browserStorage = new _converse.BrowserStorage(id, 'session');
         _converse.session.fetch();
         _converse.emit('sessionInitialized');
     };
@@ -1156,7 +1191,7 @@ _converse.initialize = async function (settings, callback) {
             const password = _.isNil(credentials) ? (_converse.connection.pass || this.password) : credentials.password;
             if (!password) {
                 if (this.auto_login) {
-                    throw new Error("initConnection: If you use auto_login and "+
+                    throw new Error("If you use auto_login and "+
                         "authentication='login' then you also need to provide a password.");
                 }
                 _converse.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true);
@@ -1768,7 +1803,7 @@ const converse = {
         'Strophe': Strophe,
         '_': _,
         'f': f,
-        'b64_sha1':  b64_sha1,
+        'b64_sha1':  SHA1.b64_sha1,
         'moment': moment,
         'sizzle': sizzle,
         'utils': u

+ 23 - 26
src/headless/converse-disco.js

@@ -34,33 +34,29 @@ converse.plugins.add('converse-disco', {
             initialize () {
                 this.waitUntilFeaturesDiscovered = utils.getResolveablePromise();
 
+                let id = `converse.dataforms-{this.get('jid')}`;
                 this.dataforms = new Backbone.Collection();
-                this.dataforms.browserStorage = new Backbone.BrowserStorage.session(
-                    `converse.dataforms-${this.get('jid')}`
-                );
+                this.dataforms.browserStorage = new _converse.BrowserStorage(id, 'session');
 
+                id = `converse.features-${this.get('jid')}`
                 this.features = new Backbone.Collection();
-                this.features.browserStorage = new Backbone.BrowserStorage.session(
-                    `converse.features-${this.get('jid')}`
-                );
+                this.features.browserStorage = new _converse.BrowserStorage(id, 'session');
                 this.features.on('add', this.onFeatureAdded, this);
+                this.features_cached = `${id}--cached`;
 
+                id = `converse.fields-${this.get('jid')}`;
                 this.fields = new Backbone.Collection();
-                this.fields.browserStorage = new Backbone.BrowserStorage.session(
-                    `converse.fields-${this.get('jid')}`
-                );
+                this.fields.browserStorage = new _converse.BrowserStorage(id, 'session');
                 this.fields.on('add', this.onFieldAdded, this);
 
+                id = `converse.identities-${this.get('jid')}`;
                 this.identities = new Backbone.Collection();
-                this.identities.browserStorage = new Backbone.BrowserStorage.session(
-                    `converse.identities-${this.get('jid')}`
-                );
+                this.identities.browserStorage = new _converse.BrowserStorage(id, 'session');
                 this.fetchFeatures();
 
+                id = `converse.disco-items-${this.get('jid')}`;
                 this.items = new _converse.DiscoEntities();
-                this.items.browserStorage = new Backbone.BrowserStorage.session(
-                    `converse.disco-items-${this.get('jid')}`
-                );
+                this.items.browserStorage = new _converse.BrowserStorage(id, 'session');
                 this.items.fetch();
             },
 
@@ -103,17 +99,17 @@ converse.plugins.add('converse-disco', {
             },
 
             fetchFeatures () {
-                if (this.features.browserStorage.records.length === 0) {
-                    this.queryInfo();
-                } else {
+                if (sessionStorage.getItem(this.features_cached)) {
                     this.features.fetch({
-                        add: true,
+                        'add': true,
                         success: () => {
                             this.waitUntilFeaturesDiscovered.resolve(this);
                             this.trigger('featuresDiscovered');
                         }
                     });
-                    this.identities.fetch({add: true});
+                    this.identities.fetch({'add': true});
+                } else {
+                    this.queryInfo();
                 }
             },
 
@@ -122,6 +118,7 @@ converse.plugins.add('converse-disco', {
                     const stanza = await _converse.api.disco.info(this.get('jid'), null);
                     this.onInfo(stanza);
                 } catch(iq) {
+                    sessionStorage.setItem(this.features_cached, true);
                     this.waitUntilFeaturesDiscovered.resolve(this);
                     _converse.log(iq, Strophe.LogLevel.ERROR);
                 }
@@ -157,6 +154,7 @@ converse.plugins.add('converse-disco', {
             },
 
             onInfo (stanza) {
+                sessionStorage.setItem(this.features_cached, true);
                 _.forEach(stanza.querySelectorAll('identity'), (identity) => {
                     this.identities.create({
                         'category': identity.getAttribute('category'),
@@ -232,10 +230,9 @@ converse.plugins.add('converse-disco', {
         }
 
         function initStreamFeatures () {
+            const id = `converse.stream-features-${_converse.bare_jid}`;
             _converse.stream_features = new Backbone.Collection();
-            _converse.stream_features.browserStorage = new Backbone.BrowserStorage.session(
-                `converse.stream-features-${_converse.bare_jid}`
-            );
+            _converse.stream_features.browserStorage = new _converse.BrowserStorage(id, 'session');
             _converse.stream_features.fetch({
                 success (collection) {
                     if (collection.length === 0 && _converse.connection.features) {
@@ -257,10 +254,9 @@ converse.plugins.add('converse-disco', {
             addClientFeatures();
             _converse.connection.addHandler(onDiscoInfoRequest, Strophe.NS.DISCO_INFO, 'iq', 'get', null, null);
 
+            const id = `converse.disco-entities-${_converse.bare_jid}`;
             _converse.disco_entities = new _converse.DiscoEntities();
-            _converse.disco_entities.browserStorage = new Backbone.BrowserStorage.session(
-                `converse.disco-entities-${_converse.bare_jid}`
-            );
+            _converse.disco_entities.browserStorage = new _converse.BrowserStorage(id, 'session');
 
             const collection = await _converse.disco_entities.fetchEntities();
             if (collection.length === 0 || !collection.get(_converse.domain)) {
@@ -280,6 +276,7 @@ converse.plugins.add('converse-disco', {
                 _converse.disco_entities.each((entity) => {
                     entity.features.reset();
                     entity.features.browserStorage._clear();
+                    sessionStorage.removeItem(entity.features_cached);
                 });
                 _converse.disco_entities.reset();
                 _converse.disco_entities.browserStorage._clear();

+ 38 - 23
src/headless/converse-muc.js

@@ -142,7 +142,7 @@ converse.plugins.add('converse-muc', {
         _converse.router.route('converse/room?jid=:jid', openRoom);
 
 
-        _converse.openChatRoom = function (jid, settings, bring_to_foreground) {
+        _converse.openChatRoom = async function (jid, settings, bring_to_foreground) {
             /* Opens a groupchat, making sure that certain attributes
              * are correct, for example that the "type" is set to
              * "chatroom".
@@ -150,7 +150,7 @@ converse.plugins.add('converse-muc', {
             settings.type = _converse.CHATROOMS_TYPE;
             settings.id = jid;
             settings.box_id = b64_sha1(jid)
-            const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
+            const chatbox = await _converse.chatboxes.getChatBox(jid, settings, true);
             chatbox.trigger('show', true);
             return chatbox;
         }
@@ -187,17 +187,16 @@ converse.plugins.add('converse-muc', {
                 this.on('change:connection_status', this.onConnectionStatusChanged, this);
 
                 const storage = _converse.config.get('storage');
-                const id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
+                let id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
                 this.features = new Backbone.Model(
                     _.assign({id}, _.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)))
                 );
-                this.features.browserStorage = new Backbone.BrowserStorage.session(id);
+                this.features.browserStorage = new _converse.BrowserStorage(id, 'session');
                 this.features.fetch();
 
+                id = `converse.occupants-${_converse.bare_jid}${this.get('jid')}`;
                 this.occupants = new _converse.ChatRoomOccupants();
-                this.occupants.browserStorage = new Backbone.BrowserStorage.session(
-                    `converse.occupants-${_converse.bare_jid}${this.get('jid')}`
-                );
+                this.occupants.browserStorage = new _converse.BrowserStorage(id, 'session');
                 this.occupants.chatroom  = this;
                 this.registerHandlers();
             },
@@ -302,7 +301,7 @@ converse.plugins.add('converse-muc', {
                 return this;
             },
 
-            leave (exit_msg) {
+            async leave (exit_msg) {
                 /* Leave the groupchat.
                  *
                  * Parameters:
@@ -310,12 +309,12 @@ converse.plugins.add('converse-muc', {
                  *      reason for leaving.
                  */
                 this.features.destroy();
-                this.occupants.browserStorage._clear();
+                await this.occupants.browserStorage._clear();
                 this.occupants.reset();
                 if (_converse.disco_entities) {
                     const disco_entity = _converse.disco_entities.get(this.get('jid'));
                     if (disco_entity) {
-                        disco_entity.destroy();
+                        await new Promise((success, error) => disco_entity.destroy({success, error}));
                     }
                 }
                 if (_converse.connection.connected) {
@@ -695,10 +694,14 @@ converse.plugins.add('converse-muc', {
                     const affiliation = item.getAttribute('affiliation');
                     const role = item.getAttribute('role');
                     if (affiliation) {
-                        this.save({'affiliation': affiliation});
+                        this.save(
+                            {'affiliation': affiliation},
+                            {'wait': true, 'patch': true});
                     }
                     if (role) {
-                        this.save({'role': role});
+                        this.save(
+                            {'role': role},
+                            {'wait': true, 'patch': true});
                     }
                 }
             },
@@ -1066,7 +1069,7 @@ converse.plugins.add('converse-muc', {
                 _converse.emit('message', {'stanza': original_stanza, 'chatbox': this});
             },
 
-            onPresence (pres) {
+            async onPresence (pres) {
                 /* Handles all MUC presence stanzas.
                  *
                  * Parameters:
@@ -1078,15 +1081,19 @@ converse.plugins.add('converse-muc', {
                 }
                 const is_self = pres.querySelector("status[code='110']");
                 if (is_self && pres.getAttribute('type') !== 'unavailable') {
-                    this.onOwnPresence(pres);
+                    await this.onOwnPresence(pres);
                 }
                 this.updateOccupantsOnPresence(pres);
+
                 if (this.get('role') !== 'none' && this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
-                    this.save('connection_status', converse.ROOMSTATUS.CONNECTED);
+                    await new Promise((success, error) => this.save(
+                        {'connection_status': converse.ROOMSTATUS.CONNECTED},
+                        {'patch': true, 'wait': true, success, error}
+                    ));
                 }
             },
 
-            onOwnPresence (pres) {
+            async onOwnPresence (pres) {
                 /* Handles a received presence relating to the current
                  * user.
                  *
@@ -1127,7 +1134,10 @@ converse.plugins.add('converse-muc', {
                         this.getRoomFeatures();
                     }
                 }
-                this.save('connection_status', converse.ROOMSTATUS.ENTERED);
+                await new Promise((success, error) => this.save(
+                    {'connection_status': converse.ROOMSTATUS.ENTERED},
+                    {'patch': true, 'wait': true, success, error}
+                ));
             },
 
             isUserMentioned (message) {
@@ -1290,7 +1300,7 @@ converse.plugins.add('converse-muc', {
         });
 
 
-        _converse.onDirectMUCInvitation = function (message) {
+        _converse.onDirectMUCInvitation = async function (message) {
             /* A direct MUC invitation to join a groupchat has been received
              * See XEP-0249: Direct MUC invitations.
              *
@@ -1323,8 +1333,7 @@ converse.plugins.add('converse-muc', {
                 }
             }
             if (result === true) {
-                const chatroom = _converse.openChatRoom(
-                    room_jid, {'password': x_el.getAttribute('password') });
+                const chatroom = await _converse.openChatRoom(room_jid, {'password': x_el.getAttribute('password') });
 
                 if (chatroom.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
                     _converse.chatboxviews.get(room_jid).join();
@@ -1461,6 +1470,7 @@ converse.plugins.add('converse-muc', {
                  * @param {(string[]|string)} jid|jids The JID or array of
                  *     JIDs of the chatroom(s) to create
                  * @param {object} [attrs] attrs The room attributes
+                 * @returns {Promise} Promise which resolves with the Backbone.Model representing the chat.
                  */
                 'create' (jids, attrs) {
                     if (_.isString(attrs)) {
@@ -1543,14 +1553,18 @@ converse.plugins.add('converse-muc', {
                         _converse.log(err_msg, Strophe.LogLevel.ERROR);
                         throw(new TypeError(err_msg));
                     } else if (_.isString(jids)) {
-                        return _converse.api.rooms.create(jids, attrs).trigger('show');
+                        const room = await _converse.api.rooms.create(jids, attrs);
+                        room.trigger('show');
+                        return room;
                     } else {
-                        return _.map(jids, (jid) => _converse.api.rooms.create(jid, attrs).trigger('show'));
+                        const rooms = await Promise.all(_.map(jids, (jid) => _converse.api.rooms.create(jid, attrs)));
+                        rooms.forEach(r =>  r.trigger('show'));
+                        return rooms;
                     }
                 },
 
                 /**
-                 * Returns an object representing a MUC chatroom (aka groupchat)
+                 * Fetches the object representing a MUC chatroom (aka groupchat)
                  *
                  * @method _converse.api.rooms.get
                  * @param {string} [jid] The room JID (if not specified, all rooms will be returned).
@@ -1562,6 +1576,7 @@ converse.plugins.add('converse-muc', {
                  *     the user's JID will be used.
                  * @param {boolean} create A boolean indicating whether the room should be created
                  *     if not found (default: `false`)
+                 * @returns {Promise} Promise which resolves with the Backbone.Model representing the chat.
                  * @example
                  * _converse.api.waitUntil('roomsAutoJoined').then(() => {
                  *     const create_if_not_found = true;

+ 9 - 7
src/headless/converse-roster.js

@@ -49,20 +49,19 @@ converse.plugins.add('converse-roster', {
             /* Initialize the Bakcbone collections that represent the contats
              * roster and the roster groups.
              */
-            const storage = _converse.config.get('storage');
+            let id = `converse.contacts-${_converse.bare_jid}`
             _converse.roster = new _converse.RosterContacts();
-            _converse.roster.browserStorage = new Backbone.BrowserStorage[storage](
-                `converse.contacts-${_converse.bare_jid}`);
+            _converse.roster.browserStorage = new _converse.BrowserStorage(id);
 
             _converse.roster.data = new Backbone.Model();
-            const id = `converse-roster-model-${_converse.bare_jid}`;
+            id = `converse-roster-model-${_converse.bare_jid}`;
             _converse.roster.data.id = id;
-            _converse.roster.data.browserStorage = new Backbone.BrowserStorage[storage](id);
+            _converse.roster.data.browserStorage = new _converse.BrowserStorage(id);
             _converse.roster.data.fetch();
 
+            id = `converse.roster-groups-${_converse.bare_jid}`;
             _converse.rostergroups = new _converse.RosterGroups();
-            _converse.rostergroups.browserStorage = new Backbone.BrowserStorage[storage](
-                `converse.roster.groups${_converse.bare_jid}`);
+            _converse.rostergroups.browserStorage = new _converse.BrowserStorage(id);
             _converse.emit('rosterInitialized');
         };
 
@@ -852,6 +851,9 @@ converse.plugins.add('converse-roster', {
         _converse.api.listen.on('statusInitialized', (reconnecting) => {
             if (!reconnecting) {
                 _converse.presences = new _converse.Presences();
+                _converse.presences.browserStorage = 
+                    new _converse.BrowserStorage(`converse.presences-${_converse.bare_jid}`, 'session');
+                _converse.presences.fetch();
             }
             _converse.presences.browserStorage =
                 new Backbone.BrowserStorage.session(`converse.presences-${_converse.bare_jid}`);

+ 4 - 6
src/headless/converse-vcard.js

@@ -120,20 +120,18 @@ converse.plugins.add('converse-vcard', {
             return onVCardData(jid, iq);
         }
 
-        /* Event handlers */
+        /************************ Event Handlers ************************/
         _converse.initVCardCollection = function () {
             _converse.vcards = new _converse.VCards();
             const id = `${_converse.bare_jid}-converse.vcards`;
-            _converse.vcards.browserStorage = new Backbone.BrowserStorage[_converse.config.get('storage')](id);
+            _converse.vcards.browserStorage = new _converse.BrowserStorage(id);
             _converse.vcards.fetch();
         }
         _converse.api.listen.on('sessionInitialized', _converse.initVCardCollection);
+        _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.VCARD));
 
 
-        _converse.on('addClientFeatures', () => {
-            _converse.api.disco.own.features.add(Strophe.NS.VCARD);
-        });
-
+        /***************************** API ******************************/
         _.extend(_converse.api, {
             /**
              * The XEP-0054 VCard API

+ 1 - 1
src/headless/package.json

@@ -23,7 +23,7 @@
   "gitHead": "9641dcdc820e029b05930479c242d2b707bbe8e2",
   "devDependencies": {
     "backbone": "1.3.3",
-    "backbone.browserStorage": "0.0.5",
+    "backbone.browserStorage": "conversejs/backbone.browserStorage#4514f39fd8ea5f13108eaedce42c5652bb790f61",
     "es6-promise": "^4.1.0",
     "filesize": "^3.6.1",
     "lodash": "^4.17.11",

+ 28 - 24
tests/utils.js

@@ -211,13 +211,31 @@
         view.model.messages.browserStorage._clear();
     };
 
-    utils.createContacts = function (converse, type, length) {
+    utils.createContact = async function (_converse, name, ask, requesting, subscription) {
+        const jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
+        if (_converse.roster.get(jid)) {
+            return Promise.resolve();
+        }
+        const contact = await new Promise((success, error) => {
+            _converse.roster.create({
+                'ask': ask,
+                'fullname': name,
+                'jid': jid,
+                'requesting': requesting,
+                'subscription': subscription
+            }, {success, error});
+        });
+        return contact;
+    };
+
+    utils.createContacts = async function (_converse, type, length) {
         /* Create current (as opposed to requesting or pending) contacts
          * for the user's roster.
          *
          * These contacts are not grouped. See below.
          */
-        var names, jid, subscription, requesting, ask;
+        await _converse.api.waitUntil('rosterContactsFetched');
+        let names, jid, subscription, requesting, ask;
         if (type === 'requesting') {
             names = mock.req_names;
             subscription = 'none';
@@ -234,40 +252,26 @@
             requesting = false;
             ask = null;
         } else if (type === 'all') {
-            this.createContacts(converse, 'current')
-                .createContacts(converse, 'requesting')
-                .createContacts(converse, 'pending');
+            this.createContacts(_converse, 'current')
+                .createContacts(_converse, 'requesting')
+                .createContacts(_converse, 'pending');
             return this;
         } else {
             throw Error("Need to specify the type of contact to create");
         }
-
-        if (typeof length === 'undefined') {
-            length = names.length;
-        }
-        for (var i=0; i<length; i++) {
-            jid = names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-            if (!converse.roster.get(jid)) {
-                converse.roster.create({
-                    'ask': ask,
-                    'fullname': names[i],
-                    'jid': jid,
-                    'requesting': requesting,
-                    'subscription': subscription
-                });
-            }
-        }
-        return this;
+        const promises = names.slice(0, length).map(n => this.createContact(_converse, n, ask, requesting, subscription));
+        await Promise.all(promises);
+        return this.waitUntil(() => _converse.roster.length);
     };
 
-    utils.createGroupedContacts = function (converse) {
+    utils.createGroupedContacts = function (_converse) {
         /* Create grouped contacts
          */
         var i=0, j=0;
         _.each(_.keys(mock.groups), function (name) {
             j = i;
             for (i=j; i<j+mock.groups[name]; i++) {
-                converse.roster.create({
+                _converse.roster.create({
                     jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
                     subscription: 'both',
                     ask: null,

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно