소스 검색

Turn roster into a custom element

JC Brand 4 년 전
부모
커밋
9f5dbad589
61개의 변경된 파일1175개의 추가작업 그리고 1673개의 파일을 삭제
  1. 1 0
      CHANGES.md
  2. 41 42
      karma.conf.js
  3. 95 102
      sass/_roster.scss
  4. 6 6
      spec/autocomplete.js
  5. 24 22
      spec/bookmarks.js
  6. 88 151
      spec/chatbox.js
  7. 10 10
      spec/controlbox.js
  8. 1 2
      spec/converse.js
  9. 6 6
      spec/corrections.js
  10. 14 22
      spec/emojis.js
  11. 1 1
      spec/hats.js
  12. 10 6
      spec/headline.js
  13. 5 5
      spec/http-file-upload.js
  14. 4 4
      spec/markers.js
  15. 2 2
      spec/me-messages.js
  16. 9 9
      spec/mentions.js
  17. 31 31
      spec/messages.js
  18. 12 12
      spec/minchats.js
  19. 7 7
      spec/modtools.js
  20. 1 1
      spec/muc-mentions.js
  21. 81 81
      spec/muc.js
  22. 14 14
      spec/muc_messages.js
  23. 10 5
      spec/muclist.js
  24. 8 8
      spec/notification.js
  25. 14 38
      spec/omemo.js
  26. 4 9
      spec/presence.js
  27. 26 26
      spec/protocol.js
  28. 7 15
      spec/push.js
  29. 3 3
      spec/rai.js
  30. 4 4
      spec/receipts.js
  31. 15 15
      spec/retractions.js
  32. 2 2
      spec/room_registration.js
  33. 225 235
      spec/roster.js
  34. 0 5
      spec/smacks.js
  35. 4 12
      spec/spoilers.js
  36. 6 6
      spec/styling.js
  37. 2 4
      spec/user-details-modal.js
  38. 6 6
      spec/xss.js
  39. 1 1
      src/components/message.js
  40. 6 14
      src/headless/plugins/roster/contacts.js
  41. 0 18
      src/headless/plugins/roster/group.js
  42. 0 58
      src/headless/plugins/roster/groups.js
  43. 0 15
      src/headless/plugins/roster/index.js
  44. 40 5
      src/headless/plugins/roster/utils.js
  45. 1 1
      src/modals/user-details.js
  46. 5 1
      src/plugins/controlbox/templates/controlbox.js
  47. 3 5
      src/plugins/muc-views/index.js
  48. 0 17
      src/plugins/muc-views/rooms-panel.js
  49. 72 133
      src/plugins/rosterview/contactview.js
  50. 1 1
      src/plugins/rosterview/filterview.js
  51. 0 206
      src/plugins/rosterview/groupview.js
  52. 3 21
      src/plugins/rosterview/index.js
  53. 42 197
      src/plugins/rosterview/rosterview.js
  54. 60 0
      src/plugins/rosterview/templates/group.js
  55. 0 9
      src/plugins/rosterview/templates/group_header.js
  56. 2 2
      src/plugins/rosterview/templates/pending_contact.js
  57. 3 1
      src/plugins/rosterview/templates/requesting_contact.js
  58. 69 14
      src/plugins/rosterview/templates/roster.js
  59. 4 3
      src/plugins/rosterview/templates/roster_item.js
  60. 63 21
      src/plugins/rosterview/utils.js
  61. 1 1
      src/templates/form_captcha.js

+ 1 - 0
CHANGES.md

@@ -27,6 +27,7 @@ OMEMO messages are gone and cannot be recovered on that device. See [muc_clear_m
 Removed events:
 Removed events:
 * `chatBoxInsertedIntoDOM`
 * `chatBoxInsertedIntoDOM`
 * `bookmarkViewsInitialized`
 * `bookmarkViewsInitialized`
+* `rosterGroupsFetched`
 
 
 ## 7.0.2 (2020-11-23)
 ## 7.0.2 (2020-11-23)
 
 

+ 41 - 42
karma.conf.js

@@ -25,49 +25,48 @@ module.exports = function(config) {
       { pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' },
       { pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' },
       { pattern: "spec/mock.js", type: 'module' },
       { pattern: "spec/mock.js", type: 'module' },
 
 
-      { pattern: "spec/user-details-modal.js", type: 'module' },
-      { pattern: "spec/spoilers.js", type: 'module' },
-      { pattern: "spec/emojis.js", type: 'module' },
-      { pattern: "spec/muclist.js", type: 'module' },
-      { pattern: "spec/utils.js", type: 'module' },
-      { pattern: "spec/converse.js", type: 'module' },
-      { pattern: "spec/bookmarks.js", type: 'module' },
-      { pattern: "spec/headline.js", type: 'module' },
-      { pattern: "spec/disco.js", type: 'module' },
-      { pattern: "spec/protocol.js", type: 'module' },
-      { pattern: "spec/presence.js", type: 'module' },
-      { pattern: "spec/eventemitter.js", type: 'module' },
-      { pattern: "spec/smacks.js", type: 'module' },
-      { pattern: "spec/ping.js", type: 'module' },
-      { pattern: "spec/push.js", type: 'module' },
-      { pattern: "spec/xmppstatus.js", type: 'module' },
-      { pattern: "spec/mam.js", type: 'module' },
-      { pattern: "spec/omemo.js", type: 'module' },
-      { pattern: "spec/controlbox.js", type: 'module' },
-      { pattern: "spec/roster.js", type: 'module' },
+      // { pattern: "spec/user-details-modal.js", type: 'module' },
+      // { pattern: "spec/spoilers.js", type: 'module' },
+      // { pattern: "spec/emojis.js", type: 'module' },
+      // { pattern: "spec/muclist.js", type: 'module' },
+      // { pattern: "spec/converse.js", type: 'module' },
+      // { pattern: "spec/bookmarks.js", type: 'module' },
+      // { pattern: "spec/headline.js", type: 'module' },
+      // { pattern: "spec/disco.js", type: 'module' },
+      // { pattern: "spec/protocol.js", type: 'module' },
+      // { pattern: "spec/presence.js", type: 'module' },
+      // { pattern: "spec/eventemitter.js", type: 'module' },
+      // { pattern: "spec/smacks.js", type: 'module' },
+      // { pattern: "spec/ping.js", type: 'module' },
+      // { pattern: "spec/push.js", type: 'module' },
+      // { pattern: "spec/xmppstatus.js", type: 'module' },
+      // { pattern: "spec/mam.js", type: 'module' },
+      // { pattern: "spec/omemo.js", type: 'module' },
+      // { pattern: "spec/controlbox.js", type: 'module' },
+      // { pattern: "spec/roster.js", type: 'module' },
       { pattern: "spec/chatbox.js", type: 'module' },
       { pattern: "spec/chatbox.js", type: 'module' },
-      { pattern: "spec/messages.js", type: 'module' },
-      { pattern: "spec/corrections.js", type: 'module' },
-      { pattern: "spec/styling.js", type: 'module' },
-      { pattern: "spec/receipts.js", type: 'module' },
-      { pattern: "spec/markers.js", type: 'module' },
-      { pattern: "spec/rai.js", type: 'module' },
-      { pattern: "spec/muc_messages.js", type: 'module' },
-      { pattern: "spec/muc-mentions.js", type: 'module' },
-      { pattern: "spec/me-messages.js", type: 'module' },
-      { pattern: "spec/mentions.js", type: 'module' },
-      { pattern: "spec/retractions.js", type: 'module' },
-      { pattern: "spec/muc.js", type: 'module' },
-      { pattern: "spec/modtools.js", type: 'module' },
-      { pattern: "spec/room_registration.js", type: 'module' },
-      { pattern: "spec/autocomplete.js", type: 'module' },
-      { pattern: "spec/minchats.js", type: 'module' },
-      { pattern: "spec/notification.js", type: 'module' },
-      { pattern: "spec/login.js", type: 'module' },
-      { pattern: "spec/register.js", type: 'module' },
-      { pattern: "spec/hats.js", type: 'module' },
-      { pattern: "spec/http-file-upload.js", type: 'module' },
-      { pattern: "spec/xss.js", type: 'module' }
+      // { pattern: "spec/messages.js", type: 'module' },
+      // { pattern: "spec/corrections.js", type: 'module' },
+      // { pattern: "spec/styling.js", type: 'module' },
+      // { pattern: "spec/receipts.js", type: 'module' },
+      // { pattern: "spec/markers.js", type: 'module' },
+      // { pattern: "spec/rai.js", type: 'module' },
+      // { pattern: "spec/muc_messages.js", type: 'module' },
+      // { pattern: "spec/muc-mentions.js", type: 'module' },
+      // { pattern: "spec/me-messages.js", type: 'module' },
+      // { pattern: "spec/mentions.js", type: 'module' },
+      // { pattern: "spec/retractions.js", type: 'module' },
+      // { pattern: "spec/muc.js", type: 'module' },
+      // { pattern: "spec/modtools.js", type: 'module' },
+      // { pattern: "spec/room_registration.js", type: 'module' },
+      // { pattern: "spec/autocomplete.js", type: 'module' },
+      // { pattern: "spec/minchats.js", type: 'module' },
+      // { pattern: "spec/notification.js", type: 'module' },
+      // { pattern: "spec/login.js", type: 'module' },
+      // { pattern: "spec/register.js", type: 'module' },
+      // { pattern: "spec/hats.js", type: 'module' },
+      // { pattern: "spec/http-file-upload.js", type: 'module' },
+      // { pattern: "spec/xss.js", type: 'module' }
     ],
     ],
 
 
     proxies: {
     proxies: {

+ 95 - 102
sass/_roster.scss

@@ -53,126 +53,119 @@
         height: 100%;
         height: 100%;
         overflow-x: hidden;
         overflow-x: hidden;
         overflow-y: auto;
         overflow-y: auto;
+        color: var(--text-color);
 
 
-        .roster-group {
-            border: none;
-            color: var(--text-color);
-            font-weight: normal;
-            text-shadow: 0 1px 0 var(--text-shadow-color);
-            margin: 0.75em 0 0.75em 0;
+        .group-toggle {
+            font-family: var(--heading-font);
+            display: block;
+            width: 100%;
+            margin: 0.75em 0 0.25em 0;
+        }
 
 
-            .group-toggle {
-                font-family: var(--heading-font);
-                display: block;
-                width: 100%;
-                padding-top: 0;
-                padding-bottom: 0.3rem;
+        .group-toggle, .group-toggle .fa {
+            color: var(--chat-head-color-dark) !important;
+            &:hover {
+                color: var(--chat-head-color-darker) !important;
             }
             }
+        }
 
 
-            .group-toggle, .group-toggle .fa {
-                color: var(--chat-head-color-dark) !important;
-                &:hover {
-                    color: var(--chat-head-color-darker) !important;
-                }
+        .current-xmpp-contact {
+            margin: 0.25em 0;
+
+            .chat-status {
+                vertical-align: middle;
+                font-size: 0.6em;
+                margin-right: 0;
+                margin-left: -0.7em;
+                margin-bottom: -1.5em;
+                border-radius: 50%;
+                border: 2px solid var(--occupants-background-color);
             }
             }
+            .chat-status--offline {
+                margin-right: 0.8em;
+            }
+            .chat-status--online {
+                color: var(--chat-status-online);
+            }
+            .chat-status--busy {
+                color: var(--chat-status-busy);
+            }
+            .chat-status--away {
+                color: var(--chat-status-away);
+            }
+            .chat-status--offline {
+                display: none;
+            }
+            .far.fa-circle,
+            .fa-times-circle {
+                color: var(--subdued-color);
+            }
+        }
 
 
-            .current-xmpp-contact {
-                margin: 0.25em 0;
-
-                .chat-status {
-                    vertical-align: middle;
-                    font-size: 0.6em;
-                    margin-right: 0;
-                    margin-left: -0.7em;
-                    margin-bottom: -1.5em;
-                    border-radius: 50%;
-                    border: 2px solid var(--occupants-background-color);
-                }
-                .chat-status--offline {
-                    margin-right: 0.8em;
-                }
-                .chat-status--online {
-                    color: var(--chat-status-online);
-                }
-                .chat-status--busy {
-                    color: var(--chat-status-busy);
-                }
-                .chat-status--away {
-                    color: var(--chat-status-away);
-                }
-                .chat-status--offline {
-                    display: none;
+        li {
+            &.requesting-xmpp-contact {
+                a {
+                    line-height: var(--line-height);
                 }
                 }
-                .far.fa-circle,
-                .fa-times-circle {
-                    color: var(--subdued-color);
+                .req-contact-name {
+                    padding: 0 0.2em 0 0;
                 }
                 }
             }
             }
 
 
-            li {
-                &.requesting-xmpp-contact {
-                    a {
-                        line-height: var(--line-height);
-                    }
-                    .req-contact-name {
-                        padding: 0 0.2em 0 0;
-                    }
-                }
-
-                .open-chat {
-                    margin: 0;
-                    padding: 0;
-                    &.unread-msgs {
-                        font-weight: bold;
-                        .contact-name {
-                            width: 70%;
-                        }
-                    }
-
-                    .msgs-indicator {
-                        color: white;
-                        background-color: var(--chat-head-color);
-                        opacity: 1;
-                        border-radius: 10%;
-                        padding: 0.2em 0.4em;
-                        font-size: var(--font-size-small);
-                    }
-
+            .open-chat {
+                margin: 0;
+                padding: 0;
+                &.unread-msgs {
+                    font-weight: bold;
                     .contact-name {
                     .contact-name {
-                        overflow: hidden;
-                        white-space: nowrap;
-                        text-overflow: ellipsis;
-                        padding: 0;
-                        margin: 0;
-                        max-width: 85%;
-                        float: none;
-                        height: 100%;
-                        &.unread-msgs {
-                            max-width: 60%;
-                        }
-                        &.contact-name--offline {
-                            margin-left: 0.7em;
-                        }
+                        width: 70%;
                     }
                     }
                 }
                 }
-                &.odd {
-                    background-color: #DCEAC5;
-                    /* Make this difference */
+
+                .msgs-indicator {
+                    color: white;
+                    background-color: var(--chat-head-color);
+                    opacity: 1;
+                    border-radius: 10%;
+                    padding: 0.2em 0.4em;
+                    font-size: var(--font-size-small);
+                    margin-right: 0;
                 }
                 }
-                a, span {
+
+                .contact-name {
                     overflow: hidden;
                     overflow: hidden;
                     white-space: nowrap;
                     white-space: nowrap;
                     text-overflow: ellipsis;
                     text-overflow: ellipsis;
+                    padding: 0;
+                    margin: 0;
+                    max-width: 85%;
+                    float: none;
+                    height: 100%;
+                    &.unread-msgs {
+                        max-width: 60%;
+                    }
+                    &.contact-name--offline {
+                        margin-left: 0.25em;
+                    }
                 }
                 }
-                .span {
-                    display: inline-block;
-                }
-                .decline-xmpp-request {
-                    margin-left: 5px;
-                }
-                &:hover {
-                    background-color: var(controlbox-pane-bg-hover-color);
-                }
+            }
+            &.odd {
+                background-color: #DCEAC5;
+                /* Make this difference */
+            }
+            a, span {
+                overflow: hidden;
+                white-space: nowrap;
+                text-overflow: ellipsis;
+            }
+            .span {
+                display: inline-block;
+            }
+            .decline-xmpp-request {
+                margin-left: 5px;
+            }
+            &:hover {
+                background-color: var(controlbox-pane-bg-hover-color);
             }
             }
         }
         }
     }
     }

+ 6 - 6
spec/autocomplete.js

@@ -9,7 +9,7 @@ describe("The nickname autocomplete feature", function () {
 
 
     it("shows all autocompletion options when the user presses @",
     it("shows all autocompletion options when the user presses @",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
@@ -63,7 +63,7 @@ describe("The nickname autocomplete feature", function () {
 
 
     it("shows all autocompletion options when the user presses @ right after a new line",
     it("shows all autocompletion options when the user presses @ right after a new line",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
@@ -118,7 +118,7 @@ describe("The nickname autocomplete feature", function () {
 
 
     it("shows all autocompletion options when the user presses @ right after an allowed character",
     it("shows all autocompletion options when the user presses @ right after an allowed character",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {'opening_mention_characters':['(']},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {'opening_mention_characters':['(']},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
@@ -172,7 +172,7 @@ describe("The nickname autocomplete feature", function () {
     }));
     }));
 
 
     it("should order by query index position and length", mock.initConverse(
     it("should order by query index position and length", mock.initConverse(
-        ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
+        ['rosterContactsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
             const view = _converse.chatboxviews.get('lounge@montague.lit');
             const view = _converse.chatboxviews.get('lounge@montague.lit');
 
 
@@ -220,7 +220,7 @@ describe("The nickname autocomplete feature", function () {
 
 
     it("autocompletes when the user presses tab",
     it("autocompletes when the user presses tab",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -332,7 +332,7 @@ describe("The nickname autocomplete feature", function () {
 
 
     it("autocompletes when the user presses backspace",
     it("autocompletes when the user presses backspace",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');

+ 24 - 22
spec/bookmarks.js

@@ -6,8 +6,9 @@ const { Strophe, u, sizzle, $iq } = converse.env;
 describe("A chat room", function () {
 describe("A chat room", function () {
 
 
     it("can be bookmarked", mock.initConverse(
     it("can be bookmarked", mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
+            ['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilDiscoConfirmed(
         await mock.waitUntilDiscoConfirmed(
             _converse, _converse.bare_jid,
             _converse, _converse.bare_jid,
             [{'category': 'pubsub', 'type': 'pep'}],
             [{'category': 'pubsub', 'type': 'pep'}],
@@ -133,10 +134,10 @@ describe("A chat room", function () {
 
 
 
 
     it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverse(
     it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async function (done, _converse) {
+            [], {}, async function (done, _converse) {
 
 
         const { u } = converse.env;
         const { u } = converse.env;
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilDiscoConfirmed(
         await mock.waitUntilDiscoConfirmed(
             _converse, _converse.bare_jid,
             _converse, _converse.bare_jid,
             [{'category': 'pubsub', 'type': 'pep'}],
             [{'category': 'pubsub', 'type': 'pep'}],
@@ -178,10 +179,9 @@ describe("A chat room", function () {
 
 
     describe("when bookmarked", function () {
     describe("when bookmarked", function () {
 
 
-        it("will use the nickname from the bookmark", mock.initConverse(
-                ['rosterGroupsFetched'], {}, async function (done, _converse) {
-
+        it("will use the nickname from the bookmark", mock.initConverse([], {}, async function (done, _converse) {
             const { u } = converse.env;
             const { u } = converse.env;
+            await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitUntilBookmarksReturned(_converse);
             await mock.waitUntilBookmarksReturned(_converse);
             const muc_jid = 'coven@chat.shakespeare.lit';
             const muc_jid = 'coven@chat.shakespeare.lit';
             _converse.bookmarks.create({
             _converse.bookmarks.create({
@@ -199,11 +199,10 @@ describe("A chat room", function () {
             done();
             done();
         }));
         }));
 
 
-        it("displays that it's bookmarked through its bookmark icon", mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async function (done, _converse) {
+        it("displays that it's bookmarked through its bookmark icon", mock.initConverse([], {}, async function (done, _converse) {
 
 
             const { u } = converse.env;
             const { u } = converse.env;
+            await mock.waitForRoster(_converse, 'current', 0);
             mock.waitUntilDiscoConfirmed(
             mock.waitUntilDiscoConfirmed(
                 _converse, _converse.bare_jid,
                 _converse, _converse.bare_jid,
                 [{'category': 'pubsub', 'type': 'pep'}],
                 [{'category': 'pubsub', 'type': 'pep'}],
@@ -225,10 +224,10 @@ describe("A chat room", function () {
             done();
             done();
         }));
         }));
 
 
-        it("can be unbookmarked", mock.initConverse(
-                ['rosterGroupsFetched'], {}, async function (done, _converse) {
+        it("can be unbookmarked", mock.initConverse([], {}, async function (done, _converse) {
 
 
             const { u, Strophe } = converse.env;
             const { u, Strophe } = converse.env;
+            await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitUntilBookmarksReturned(_converse);
             await mock.waitUntilBookmarksReturned(_converse);
             const muc_jid = 'theplay@conference.shakespeare.lit';
             const muc_jid = 'theplay@conference.shakespeare.lit';
             await _converse.api.rooms.open(muc_jid);
             await _converse.api.rooms.open(muc_jid);
@@ -292,9 +291,9 @@ describe("A chat room", function () {
     describe("and when autojoin is set", function () {
     describe("and when autojoin is set", function () {
 
 
         it("will be be opened and joined automatically upon login", mock.initConverse(
         it("will be be opened and joined automatically upon login", mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async function (done, _converse) {
+                [], {}, async function (done, _converse) {
 
 
+            await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitUntilBookmarksReturned(_converse);
             await mock.waitUntilBookmarksReturned(_converse);
             spyOn(_converse.api.rooms, 'create').and.callThrough();
             spyOn(_converse.api.rooms, 'create').and.callThrough();
             const jid = 'theplay@conference.shakespeare.lit';
             const jid = 'theplay@conference.shakespeare.lit';
@@ -321,9 +320,10 @@ describe("A chat room", function () {
 describe("Bookmarks", function () {
 describe("Bookmarks", function () {
 
 
     it("can be pushed from the XMPP server", mock.initConverse(
     it("can be pushed from the XMPP server", mock.initConverse(
-            ['connected', 'rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
+            ['connected', 'chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         const { $msg, u } = converse.env;
         const { $msg, u } = converse.env;
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilBookmarksReturned(_converse);
         await mock.waitUntilBookmarksReturned(_converse);
 
 
         /* The stored data is automatically pushed to all of the user's
         /* The stored data is automatically pushed to all of the user's
@@ -410,10 +410,11 @@ describe("Bookmarks", function () {
 
 
 
 
     it("can be retrieved from the XMPP server", mock.initConverse(
     it("can be retrieved from the XMPP server", mock.initConverse(
-            ['chatBoxesFetched', 'rosterGroupsFetched'], {},
+            ['chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const { Strophe, sizzle, u, $iq } = converse.env;
         const { Strophe, sizzle, u, $iq } = converse.env;
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilDiscoConfirmed(
         await mock.waitUntilDiscoConfirmed(
             _converse, _converse.bare_jid,
             _converse, _converse.bare_jid,
             [{'category': 'pubsub', 'type': 'pep'}],
             [{'category': 'pubsub', 'type': 'pep'}],
@@ -490,14 +491,14 @@ describe("Bookmarks", function () {
     describe("The bookmarks list", function () {
     describe("The bookmarks list", function () {
 
 
         it("shows a list of bookmarks", mock.initConverse(
         it("shows a list of bookmarks", mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async function (done, _converse) {
+                [], {}, async function (done, _converse) {
 
 
             await mock.waitUntilDiscoConfirmed(
             await mock.waitUntilDiscoConfirmed(
                 _converse, _converse.bare_jid,
                 _converse, _converse.bare_jid,
                 [{'category': 'pubsub', 'type': 'pep'}],
                 [{'category': 'pubsub', 'type': 'pep'}],
                 ['http://jabber.org/protocol/pubsub#publish-options']
                 ['http://jabber.org/protocol/pubsub#publish-options']
             );
             );
+            await mock.waitForRoster(_converse, 'current', 0);
             mock.openControlBox(_converse);
             mock.openControlBox(_converse);
 
 
             const IQ_stanzas = _converse.connection.IQ_stanzas;
             const IQ_stanzas = _converse.connection.IQ_stanzas;
@@ -565,7 +566,7 @@ describe("Bookmarks", function () {
         }));
         }));
 
 
         it("can be used to open a MUC from a bookmark", mock.initConverse(
         it("can be used to open a MUC from a bookmark", mock.initConverse(
-                ['rosterGroupsFetched'], {'view_mode': 'fullscreen'}, async function (done, _converse) {
+                [], {'view_mode': 'fullscreen'}, async function (done, _converse) {
 
 
             const api = _converse.api;
             const api = _converse.api;
             await mock.waitUntilDiscoConfirmed(
             await mock.waitUntilDiscoConfirmed(
@@ -573,6 +574,7 @@ describe("Bookmarks", function () {
                 [{'category': 'pubsub', 'type': 'pep'}],
                 [{'category': 'pubsub', 'type': 'pep'}],
                 ['http://jabber.org/protocol/pubsub#publish-options']
                 ['http://jabber.org/protocol/pubsub#publish-options']
             );
             );
+            await mock.waitForRoster(_converse, 'current', 0);
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
             const view = await _converse.chatboxviews.get('controlbox');
             const view = await _converse.chatboxviews.get('controlbox');
             const IQ_stanzas = _converse.connection.IQ_stanzas;
             const IQ_stanzas = _converse.connection.IQ_stanzas;
@@ -614,8 +616,9 @@ describe("Bookmarks", function () {
         }));
         }));
 
 
         it("remembers the toggle state of the bookmarks list", mock.initConverse(
         it("remembers the toggle state of the bookmarks list", mock.initConverse(
-                ['rosterGroupsFetched'], {}, async function (done, _converse) {
+                [], {}, async function (done, _converse) {
 
 
+            await mock.waitForRoster(_converse, 'current', 0);
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
             await mock.waitUntilDiscoConfirmed(
             await mock.waitUntilDiscoConfirmed(
                 _converse, _converse.bare_jid,
                 _converse, _converse.bare_jid,
@@ -672,10 +675,9 @@ describe("Bookmarks", function () {
 describe("When hide_open_bookmarks is true and a bookmarked room is opened", function () {
 describe("When hide_open_bookmarks is true and a bookmarked room is opened", function () {
 
 
     it("can be closed", mock.initConverse(
     it("can be closed", mock.initConverse(
-        ['rosterGroupsFetched'],
-        { hide_open_bookmarks: true },
-        async function (done, _converse) {
+            [], { hide_open_bookmarks: true }, async function (done, _converse) {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.openControlBox(_converse);
         await mock.openControlBox(_converse);
         await mock.waitUntilBookmarksReturned(_converse);
         await mock.waitUntilBookmarksReturned(_converse);
 
 

+ 88 - 151
spec/chatbox.js

@@ -13,7 +13,7 @@ describe("Chatboxes", function () {
 
 
     describe("A Chatbox", function () {
     describe("A Chatbox", function () {
 
 
-        it("has a /help command to show the available commands", mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
+        it("has a /help command to show the available commands", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -72,8 +72,7 @@ describe("Chatboxes", function () {
 
 
 
 
         it("is created when you click on a roster item", mock.initConverse(
         it("is created when you click on a roster item", mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                ['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -84,8 +83,9 @@ describe("Chatboxes", function () {
             spyOn(_converse.minimize, 'trimChats');
             spyOn(_converse.minimize, 'trimChats');
             expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
             expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
 
 
-            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group li').length, 700);
-            const online_contacts = _converse.rosterview.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
+            const rosterview = document.querySelector('converse-roster');
+            await u.waitUntil(() => rosterview.querySelectorAll('.roster-group li').length, 700);
+            const online_contacts = rosterview.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
             expect(online_contacts.length).toBe(17);
             expect(online_contacts.length).toBe(17);
             let el = online_contacts[0];
             let el = online_contacts[0];
             el.click();
             el.click();
@@ -102,8 +102,8 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("opens when a new message is received", mock.initConverse(
         it("opens when a new message is received", mock.initConverse(
-            ['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
-            async function (done, _converse) {
+                [], {'allow_non_roster_messaging': true},
+                async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitForRoster(_converse, 'current', 0);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -122,10 +122,7 @@ describe("Chatboxes", function () {
             done();
             done();
         }));
         }));
 
 
-        it("doesn't open when a message without body is received", mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async function (done, _converse) {
-
+        it("doesn't open when a message without body is received", mock.initConverse([], {}, async function (done, _converse) {
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const stanza = u.toStanza(`
             const stanza = u.toStanza(`
@@ -142,8 +139,7 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("is focused if its already open and you click on its corresponding roster item",
         it("is focused if its already open and you click on its corresponding roster item",
-            mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -151,7 +147,8 @@ describe("Chatboxes", function () {
 
 
             const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const view = await mock.openChatBoxFor(_converse, contact_jid);
             const view = await mock.openChatBoxFor(_converse, contact_jid);
-            const el = sizzle('a.open-chat:contains("'+view.model.getDisplayName()+'")', _converse.rosterview.el).pop();
+            const rosterview = document.querySelector('converse-roster');
+            const el = sizzle('a.open-chat:contains("'+view.model.getDisplayName()+'")', rosterview.el).pop();
             await u.waitUntil(() => u.isVisible(el));
             await u.waitUntil(() => u.isVisible(el));
             const textarea = view.querySelector('.chat-textarea');
             const textarea = view.querySelector('.chat-textarea');
             await u.waitUntil(() => u.isVisible(textarea));
             await u.waitUntil(() => u.isVisible(textarea));
@@ -167,9 +164,7 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("can be saved to, and retrieved from, browserStorage",
         it("can be saved to, and retrieved from, browserStorage",
-            mock.initConverse(
-                ['rosterGroupsFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse([], {}, async function (done, _converse) {
 
 
             spyOn(_converse.minimize, 'trimChats');
             spyOn(_converse.minimize, 'trimChats');
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -193,23 +188,21 @@ describe("Chatboxes", function () {
             // have the same attributes values as the original ones.
             // have the same attributes values as the original ones.
             const attrs = ['id', 'box_id', 'visible'];
             const attrs = ['id', 'box_id', 'visible'];
             let new_attrs, old_attrs;
             let new_attrs, old_attrs;
-            for (var i=0; i<attrs.length; i++) {
+            for (let i=0; i<attrs.length; i++) {
                 new_attrs = _.map(_.map(newchatboxes.models, 'attributes'), attrs[i]);
                 new_attrs = _.map(_.map(newchatboxes.models, 'attributes'), attrs[i]);
                 old_attrs = _.map(_.map(_converse.chatboxes.models, 'attributes'), attrs[i]);
                 old_attrs = _.map(_.map(_converse.chatboxes.models, 'attributes'), attrs[i]);
                 expect(_.isEqual(new_attrs, old_attrs)).toEqual(true);
                 expect(_.isEqual(new_attrs, old_attrs)).toEqual(true);
             }
             }
-            _converse.rosterview.render();
             done();
             done();
         }));
         }));
 
 
         it("can be closed by clicking a DOM element with class 'close-chatbox-button'",
         it("can be closed by clicking a DOM element with class 'close-chatbox-button'",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
             const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+            const rosterview = document.querySelector('converse-roster');
+            await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
             await mock.openChatBoxFor(_converse, contact_jid);
             await mock.openChatBoxFor(_converse, contact_jid);
             const chatview = _converse.chatboxviews.get(contact_jid);
             const chatview = _converse.chatboxviews.get(contact_jid);
             spyOn(chatview, 'close').and.callThrough();
             spyOn(chatview, 'close').and.callThrough();
@@ -224,14 +217,13 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("will be removed from browserStorage when closed",
         it("will be removed from browserStorage when closed",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             spyOn(_converse.minimize, 'trimChats');
             spyOn(_converse.minimize, 'trimChats');
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
-            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+            const rosterview = document.querySelector('converse-roster');
+            await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
             spyOn(_converse.api, "trigger").and.callThrough();
             spyOn(_converse.api, "trigger").and.callThrough();
 
 
             mock.closeControlBox();
             mock.closeControlBox();
@@ -265,9 +257,7 @@ describe("Chatboxes", function () {
         describe("A chat toolbar", function () {
         describe("A chat toolbar", function () {
 
 
             it("shows the remaining character count if a message_limit is configured",
             it("shows the remaining character count if a message_limit is configured",
-                mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 200},
-                    async function (done, _converse) {
+                    mock.initConverse(['chatBoxesFetched'], {'message_limit': 200}, async function (done, _converse) {
 
 
                 await mock.waitForRoster(_converse, 'current', 3);
                 await mock.waitForRoster(_converse, 'current', 3);
                 await mock.openControlBox(_converse);
                 await mock.openControlBox(_converse);
@@ -305,9 +295,7 @@ describe("Chatboxes", function () {
 
 
 
 
             it("does not show a remaining character count if message_limit is zero",
             it("does not show a remaining character count if message_limit is zero",
-                mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 0},
-                    async function (done, _converse) {
+                    mock.initConverse(['chatBoxesFetched'], {'message_limit': 0}, async function (done, _converse) {
 
 
                 await mock.waitForRoster(_converse, 'current', 3);
                 await mock.waitForRoster(_converse, 'current', 3);
                 await mock.openControlBox(_converse);
                 await mock.openControlBox(_converse);
@@ -321,9 +309,7 @@ describe("Chatboxes", function () {
 
 
 
 
             it("can contain a button for starting a call",
             it("can contain a button for starting a call",
-                mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                    async function (done, _converse) {
+                    mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                 await mock.waitForRoster(_converse, 'current');
                 await mock.waitForRoster(_converse, 'current');
                 await mock.openControlBox(_converse);
                 await mock.openControlBox(_converse);
@@ -355,11 +341,7 @@ describe("Chatboxes", function () {
 
 
         describe("A Chat Status Notification", function () {
         describe("A Chat Status Notification", function () {
 
 
-            it("does not open a new chatbox",
-                mock.initConverse(
-                    ['rosterGroupsFetched'], {},
-                    async function (done, _converse) {
-
+            it("does not open a new chatbox", mock.initConverse([], {}, async function (done, _converse) {
                 await mock.waitForRoster(_converse, 'current');
                 await mock.waitForRoster(_converse, 'current');
                 await mock.openControlBox(_converse);
                 await mock.openControlBox(_converse);
 
 
@@ -383,14 +365,13 @@ describe("Chatboxes", function () {
             describe("An active notification", function () {
             describe("An active notification", function () {
 
 
                 it("is sent when the user opens a chat box",
                 it("is sent when the user opens a chat box",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        async function (done, _converse) {
+                        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
-                    u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+                    const rosterview = document.querySelector('converse-roster');
+                    u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
                     spyOn(_converse.connection, 'send');
                     spyOn(_converse.connection, 'send');
                     await mock.openChatBoxFor(_converse, contact_jid);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
@@ -406,14 +387,14 @@ describe("Chatboxes", function () {
                 }));
                 }));
 
 
                 it("is sent when the user maximizes a minimized a chat box", mock.initConverse(
                 it("is sent when the user maximizes a minimized a chat box", mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                    async function (done, _converse) {
+                        ['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     await mock.waitForRoster(_converse, 'current', 1);
                     await mock.waitForRoster(_converse, 'current', 1);
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
 
-                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+                    const rosterview = document.querySelector('converse-roster');
+                    await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
                     view.model.minimize();
                     view.model.minimize();
@@ -439,15 +420,14 @@ describe("Chatboxes", function () {
             describe("A composing notification", function () {
             describe("A composing notification", function () {
 
 
                 it("is sent as soon as the user starts typing a message which is not a command",
                 it("is sent as soon as the user starts typing a message which is not a command",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        async function (done, _converse) {
+                        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
 
-                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+                    const rosterview = document.querySelector('converse-roster');
+                    await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     var view = _converse.chatboxviews.get(contact_jid);
                     var view = _converse.chatboxviews.get(contact_jid);
                     expect(view.model.get('chat_state')).toBe('active');
                     expect(view.model.get('chat_state')).toBe('active');
@@ -478,15 +458,15 @@ describe("Chatboxes", function () {
                 }));
                 }));
 
 
                 it("is NOT sent out if send_chat_state_notifications doesn't allow it",
                 it("is NOT sent out if send_chat_state_notifications doesn't allow it",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {'send_chat_state_notifications': []},
+                    mock.initConverse(['chatBoxesFetched'], {'send_chat_state_notifications': []},
                         async function (done, _converse) {
                         async function (done, _converse) {
 
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
 
-                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+                    const rosterview = document.querySelector('converse-roster');
+                    await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     var view = _converse.chatboxviews.get(contact_jid);
                     var view = _converse.chatboxviews.get(contact_jid);
                     expect(view.model.get('chat_state')).toBe('active');
                     expect(view.model.get('chat_state')).toBe('active');
@@ -501,17 +481,14 @@ describe("Chatboxes", function () {
                     done();
                     done();
                 }));
                 }));
 
 
-                it("will be shown if received",
-                    mock.initConverse(
-                        ['rosterGroupsFetched'], {},
-                        async function (done, _converse) {
-
+                it("will be shown if received", mock.initConverse([], {}, async function (done, _converse) {
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
 
 
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+                    const rosterview = document.querySelector('converse-roster');
+                    await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, sender_jid);
                     await mock.openChatBoxFor(_converse, sender_jid);
 
 
                     // <composing> state
                     // <composing> state
@@ -553,9 +530,7 @@ describe("Chatboxes", function () {
                 }));
                 }));
 
 
                 it("is ignored if it's a composing carbon message sent by this user from a different client",
                 it("is ignored if it's a composing carbon message sent by this user from a different client",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        async function (done, _converse) {
+                        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
                     await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
                     await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
                     await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
@@ -593,14 +568,13 @@ describe("Chatboxes", function () {
             describe("A paused notification", function () {
             describe("A paused notification", function () {
 
 
                 it("is sent if the user has stopped typing since 30 seconds",
                 it("is sent if the user has stopped typing since 30 seconds",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        async function (done, _converse) {
+                        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
-                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group li').length, 700);
+                    const rosterview = document.querySelector('converse-roster');
+                    await u.waitUntil(() => rosterview.querySelectorAll('.roster-group li').length, 700);
                     _converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test
                     _converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test
                     await mock.openChatBoxFor(_converse, contact_jid);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
@@ -646,14 +620,11 @@ describe("Chatboxes", function () {
                     done();
                     done();
                 }));
                 }));
 
 
-                it("will be shown if received",
-                        mock.initConverse(
-                            ['rosterGroupsFetched'], {},
-                            async function (done, _converse) {
-
+                it("will be shown if received", mock.initConverse([], {}, async function (done, _converse) {
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
-                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+                    const rosterview = document.querySelector('converse-roster');
+                    await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
                     // TODO: only show paused state if the previous state was composing
                     // TODO: only show paused state if the previous state was composing
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     spyOn(_converse.api, "trigger").and.callThrough();
                     spyOn(_converse.api, "trigger").and.callThrough();
@@ -675,9 +646,7 @@ describe("Chatboxes", function () {
                 }));
                 }));
 
 
                 it("will not be shown if it's a paused carbon message that this user sent from a different client",
                 it("will not be shown if it's a paused carbon message that this user sent from a different client",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        async function (done, _converse) {
+                        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
                     await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
                     await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
                     await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
@@ -713,9 +682,7 @@ describe("Chatboxes", function () {
             describe("An inactive notification", function () {
             describe("An inactive notification", function () {
 
 
                 it("is sent if the user has stopped typing since 2 minutes",
                 it("is sent if the user has stopped typing since 2 minutes",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        async function (done, _converse) {
+                        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     const sent_stanzas = _converse.connection.sent_stanzas;
                     const sent_stanzas = _converse.connection.sent_stanzas;
                     // Make the timeouts shorter so that we can test
                     // Make the timeouts shorter so that we can test
@@ -725,7 +692,8 @@ describe("Chatboxes", function () {
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
-                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 1000);
+                    const rosterview = document.querySelector('converse-roster');
+                    await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 1000);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
                     await u.waitUntil(() => view.model.get('chat_state') === 'active');
                     await u.waitUntil(() => view.model.get('chat_state') === 'active');
@@ -776,9 +744,7 @@ describe("Chatboxes", function () {
                 }));
                 }));
 
 
                 it("is sent when the user a minimizes a chat box",
                 it("is sent when the user a minimizes a chat box",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        async function (done, _converse) {
+                    mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
@@ -797,14 +763,13 @@ describe("Chatboxes", function () {
                 }));
                 }));
 
 
                 it("is sent if the user closes a chat box",
                 it("is sent if the user closes a chat box",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        async function (done, _converse) {
+                        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
-                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+                    const rosterview = document.querySelector('converse-roster');
+                    await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
                     const view = await mock.openChatBoxFor(_converse, contact_jid);
                     const view = await mock.openChatBoxFor(_converse, contact_jid);
                     expect(view.model.get('chat_state')).toBe('active');
                     expect(view.model.get('chat_state')).toBe('active');
                     spyOn(_converse.connection, 'send');
                     spyOn(_converse.connection, 'send');
@@ -821,9 +786,7 @@ describe("Chatboxes", function () {
                 }));
                 }));
 
 
                 it("will clear any other chat status notifications",
                 it("will clear any other chat status notifications",
-                    mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        async function (done, _converse) {
+                        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
@@ -862,11 +825,7 @@ describe("Chatboxes", function () {
 
 
             describe("A gone notification", function () {
             describe("A gone notification", function () {
 
 
-                it("will be shown if received",
-                    mock.initConverse(
-                        ['rosterGroupsFetched'], {},
-                        async function (done, _converse) {
-
+                it("will be shown if received", mock.initConverse([], {}, async function (done, _converse) {
                     await mock.waitForRoster(_converse, 'current', 3);
                     await mock.waitForRoster(_converse, 'current', 3);
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
                     const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -889,17 +848,14 @@ describe("Chatboxes", function () {
 
 
             describe("On receiving a message correction", function () {
             describe("On receiving a message correction", function () {
 
 
-                it("will be removed",
-                    mock.initConverse(
-                        ['rosterGroupsFetched'], {},
-                        async function (done, _converse) {
-
+                it("will be removed", mock.initConverse([], {}, async function (done, _converse) {
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openControlBox(_converse);
                     await mock.openControlBox(_converse);
 
 
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
+                    const rosterview = document.querySelector('converse-roster');
+                    await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, sender_jid);
                     await mock.openChatBoxFor(_converse, sender_jid);
 
 
                     // Original message
                     // Original message
@@ -953,9 +909,7 @@ describe("Chatboxes", function () {
     describe("Special Messages", function () {
     describe("Special Messages", function () {
 
 
         it("'/clear' can be used to clear messages in a conversation",
         it("'/clear' can be used to clear messages in a conversation",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -998,9 +952,7 @@ describe("Chatboxes", function () {
     describe("A ChatBox's Unread Message Count", function () {
     describe("A ChatBox's Unread Message Count", function () {
 
 
         it("is incremented when the message is received and ChatBoxView is scrolled up",
         it("is incremented when the message is received and ChatBoxView is scrolled up",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
@@ -1021,9 +973,7 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("is not incremented when the message is received and ChatBoxView is scrolled down",
         it("is not incremented when the message is received and ChatBoxView is scrolled down",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -1040,11 +990,9 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("is incremented when message is received, chatbox is scrolled down and the window is not focused",
         it("is incremented when message is received, chatbox is scrolled down and the window is not focused",
-            mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
-
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const msgFactory = function () {
             const msgFactory = function () {
                 return mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
                 return mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
@@ -1066,9 +1014,7 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("is incremented when message is received, chatbox is scrolled up and the window is not focused",
         it("is incremented when message is received, chatbox is scrolled up and the window is not focused",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -1091,9 +1037,7 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("is cleared when ChatBoxView was scrolled down and the window become focused",
         it("is cleared when ChatBoxView was scrolled down and the window become focused",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -1119,9 +1063,7 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("is not cleared when ChatBoxView was scrolled up and the windows become focused",
         it("is not cleared when ChatBoxView was scrolled up and the windows become focused",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -1151,14 +1093,13 @@ describe("Chatboxes", function () {
     describe("A RosterView's Unread Message Count", function () {
     describe("A RosterView's Unread Message Count", function () {
 
 
         it("is updated when message is received and chatbox is scrolled up",
         it("is updated when message is received and chatbox is scrolled up",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             let msg, indicator_el;
             let msg, indicator_el;
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
+            const rosterview = document.querySelector('converse-roster');
+            await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             chatbox.save('scrolled', true);
             chatbox.save('scrolled', true);
@@ -1166,26 +1107,25 @@ describe("Chatboxes", function () {
             await _converse.handleMessageStanza(msg);
             await _converse.handleMessageStanza(msg);
             await u.waitUntil(() => chatbox.messages.length);
             await u.waitUntil(() => chatbox.messages.length);
             const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
             const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
-            indicator_el = sizzle(selector, _converse.rosterview.el).pop();
+            indicator_el = sizzle(selector, rosterview.el).pop();
             expect(indicator_el.textContent).toBe('1');
             expect(indicator_el.textContent).toBe('1');
             msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too');
             msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too');
             await _converse.handleMessageStanza(msg);
             await _converse.handleMessageStanza(msg);
             await u.waitUntil(() => chatbox.messages.length > 1);
             await u.waitUntil(() => chatbox.messages.length > 1);
-            indicator_el = sizzle(selector, _converse.rosterview.el).pop();
+            indicator_el = sizzle(selector, rosterview.el).pop();
             expect(indicator_el.textContent).toBe('2');
             expect(indicator_el.textContent).toBe('2');
             done();
             done();
         }));
         }));
 
 
         it("is updated when message is received and chatbox is minimized",
         it("is updated when message is received and chatbox is minimized",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
 
             let indicator_el, msg;
             let indicator_el, msg;
-            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
+            const rosterview = document.querySelector('converse-roster');
+            await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             var chatboxview = _converse.chatboxviews.get(sender_jid);
             var chatboxview = _converse.chatboxviews.get(sender_jid);
@@ -1195,31 +1135,30 @@ describe("Chatboxes", function () {
             await _converse.handleMessageStanza(msg);
             await _converse.handleMessageStanza(msg);
             await u.waitUntil(() => chatbox.messages.length);
             await u.waitUntil(() => chatbox.messages.length);
             const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
             const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
-            indicator_el = sizzle(selector, _converse.rosterview.el).pop();
+            indicator_el = sizzle(selector, rosterview.el).pop();
             expect(indicator_el.textContent).toBe('1');
             expect(indicator_el.textContent).toBe('1');
 
 
             msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too');
             msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too');
             await _converse.handleMessageStanza(msg);
             await _converse.handleMessageStanza(msg);
             await u.waitUntil(() => chatbox.messages.length === 2);
             await u.waitUntil(() => chatbox.messages.length === 2);
-            indicator_el = sizzle(selector, _converse.rosterview.el).pop();
+            indicator_el = sizzle(selector, rosterview.el).pop();
             expect(indicator_el.textContent).toBe('2');
             expect(indicator_el.textContent).toBe('2');
             done();
             done();
         }));
         }));
 
 
         it("is cleared when chatbox is maximzied after receiving messages in minimized mode",
         it("is cleared when chatbox is maximzied after receiving messages in minimized mode",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
-            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
+            const rosterview = document.querySelector('converse-roster');
+            await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
             const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
             const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
-            const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
+            const select_msgs_indicator = () => sizzle(selector, rosterview.el).pop();
             view.minimize();
             view.minimize();
             _converse.handleMessageStanza(msgFactory());
             _converse.handleMessageStanza(msgFactory());
             await u.waitUntil(() => chatbox.messages.length);
             await u.waitUntil(() => chatbox.messages.length);
@@ -1233,45 +1172,43 @@ describe("Chatboxes", function () {
         }));
         }));
 
 
         it("is cleared when unread messages are viewed which were received in scrolled-up chatbox",
         it("is cleared when unread messages are viewed which were received in scrolled-up chatbox",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
+            const rosterview = document.querySelector('converse-roster');
+            await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
             const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`;
             const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`;
-            const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
+            const select_msgs_indicator = () => sizzle(selector, rosterview.el).pop();
             chatbox.save('scrolled', true);
             chatbox.save('scrolled', true);
             _converse.handleMessageStanza(msgFactory());
             _converse.handleMessageStanza(msgFactory());
             const view = _converse.chatboxviews.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
             await u.waitUntil(() => view.model.messages.length);
             await u.waitUntil(() => view.model.messages.length);
             expect(select_msgs_indicator().textContent).toBe('1');
             expect(select_msgs_indicator().textContent).toBe('1');
             view.viewUnreadMessages();
             view.viewUnreadMessages();
-            _converse.rosterview.render();
+            rosterview.render();
             expect(select_msgs_indicator()).toBeUndefined();
             expect(select_msgs_indicator()).toBeUndefined();
             done();
             done();
         }));
         }));
 
 
         it("is not cleared after user clicks on roster view when chatbox is already opened and scrolled up",
         it("is not cleared after user clicks on roster view when chatbox is already opened and scrolled up",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
+            const rosterview = document.querySelector('converse-roster');
+            await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
             const msg = 'This message will be received as unread, but eventually will be read';
             const msg = 'This message will be received as unread, but eventually will be read';
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, msg);
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, msg);
             const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
             const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
-            const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
+            const select_msgs_indicator = () => sizzle(selector, rosterview.el).pop();
             chatbox.save('scrolled', true);
             chatbox.save('scrolled', true);
             _converse.handleMessageStanza(msgFactory());
             _converse.handleMessageStanza(msgFactory());
             await u.waitUntil(() => view.model.messages.length);
             await u.waitUntil(() => view.model.messages.length);

+ 10 - 10
spec/controlbox.js

@@ -10,7 +10,7 @@ describe("The Controlbox", function () {
 
 
     it("can be opened by clicking a DOM element with class 'toggle-controlbox'",
     it("can be opened by clicking a DOM element with class 'toggle-controlbox'",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             function (done, _converse) {
             function (done, _converse) {
 
 
         // This spec will only pass if the controlbox is not currently
         // This spec will only pass if the controlbox is not currently
@@ -57,7 +57,7 @@ describe("The Controlbox", function () {
 
 
         it("can be used to add contact and it checks for case-sensivity",
         it("can be used to add contact and it checks for case-sensivity",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             spyOn(_converse.api, "trigger").and.callThrough();
             spyOn(_converse.api, "trigger").and.callThrough();
@@ -85,7 +85,7 @@ describe("The Controlbox", function () {
 
 
         it("shows the number of unread mentions received",
         it("shows the number of unread mentions received",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'all');
             await mock.waitForRoster(_converse, 'all');
@@ -135,7 +135,7 @@ describe("The Controlbox", function () {
 
 
         it("shows the user's chat status, which is online by default",
         it("shows the user's chat status, which is online by default",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 function (done, _converse) {
                 function (done, _converse) {
 
 
             mock.openControlBox(_converse);
             mock.openControlBox(_converse);
@@ -147,7 +147,7 @@ describe("The Controlbox", function () {
 
 
         it("can be used to set the current user's chat status",
         it("can be used to set the current user's chat status",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -175,7 +175,7 @@ describe("The Controlbox", function () {
 
 
         it("can be used to set a custom status message",
         it("can be used to set a custom status message",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -209,7 +209,7 @@ describe("The 'Add Contact' widget", function () {
 
 
     it("opens up an add modal when you click on it",
     it("opens up an add modal when you click on it",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'all');
         await mock.waitForRoster(_converse, 'all');
@@ -243,7 +243,7 @@ describe("The 'Add Contact' widget", function () {
 
 
     it("can be configured to not provide search suggestions",
     it("can be configured to not provide search suggestions",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {'autocomplete_add_contact': false},
+            ['rosterContactsFetched'], {'autocomplete_add_contact': false},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'all', 0);
         await mock.waitForRoster(_converse, 'all', 0);
@@ -275,7 +275,7 @@ describe("The 'Add Contact' widget", function () {
 
 
     it("integrates with xhr_user_search_url to search for contacts",
     it("integrates with xhr_user_search_url to search for contacts",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'],
+            ['rosterContactsFetched'],
             { 'xhr_user_search_url': 'http://example.org/?' },
             { 'xhr_user_search_url': 'http://example.org/?' },
             async function (done, _converse) {
             async function (done, _converse) {
 
 
@@ -331,7 +331,7 @@ describe("The 'Add Contact' widget", function () {
 
 
     it("can be configured to not provide search suggestions for XHR search results",
     it("can be configured to not provide search suggestions for XHR search results",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'],
+            ['rosterContactsFetched'],
             { 'autocomplete_add_contact': false,
             { 'autocomplete_add_contact': false,
               'xhr_user_search_url': 'http://example.org/?' },
               'xhr_user_search_url': 'http://example.org/?' },
             async function (done, _converse) {
             async function (done, _converse) {

+ 1 - 2
spec/converse.js

@@ -305,8 +305,7 @@ describe("Converse", function() {
         }));
         }));
 
 
         it("has a method 'open' which opens and returns a promise that resolves to a chat model", mock.initConverse(
         it("has a method 'open' which opens and returns a promise that resolves to a chat model", mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesInitialized'], {},
-                async (done, _converse) => {
+                ['chatBoxesInitialized'], {}, async (done, _converse) => {
 
 
             const u = converse.env.utils;
             const u = converse.env.utils;
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);

+ 6 - 6
spec/corrections.js

@@ -7,7 +7,7 @@ describe("A Chat Message", function () {
 
 
     it("can be sent as a correction by using the up arrow",
     it("can be sent as a correction by using the up arrow",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -165,7 +165,7 @@ describe("A Chat Message", function () {
 
 
     it("can be sent as a correction by clicking the pencil icon",
     it("can be sent as a correction by clicking the pencil icon",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -292,7 +292,7 @@ describe("A Chat Message", function () {
 
 
         it("can be replaced with a correction",
         it("can be replaced with a correction",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
@@ -357,7 +357,7 @@ describe("A Groupchat Message", function () {
 
 
     it("can be replaced with a correction",
     it("can be replaced with a correction",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -426,7 +426,7 @@ describe("A Groupchat Message", function () {
 
 
     it("keeps the same position in history after a correction",
     it("keeps the same position in history after a correction",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -522,7 +522,7 @@ describe("A Groupchat Message", function () {
 
 
     it("can be sent as a correction by using the up arrow",
     it("can be sent as a correction by using the up arrow",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';

+ 14 - 22
spec/emojis.js

@@ -12,9 +12,7 @@ describe("Emojis", function () {
         afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
         afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
 
 
         it("can be opened by clicking a button in the chat toolbar",
         it("can be opened by clicking a button in the chat toolbar",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -32,10 +30,9 @@ describe("Emojis", function () {
         }));
         }));
 
 
         it("is opened to autocomplete emojis in the textarea",
         it("is opened to autocomplete emojis in the textarea",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
+            await mock.waitForRoster(_converse, 'current', 0);
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const view = _converse.chatboxviews.get(muc_jid);
             const view = _converse.chatboxviews.get(muc_jid);
@@ -100,11 +97,10 @@ describe("Emojis", function () {
         }));
         }));
 
 
         it("is focused to autocomplete emojis in the textarea",
         it("is focused to autocomplete emojis in the textarea",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
+            await mock.waitForRoster(_converse, 'current', 0);
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const view = _converse.chatboxviews.get(muc_jid);
             const view = _converse.chatboxviews.get(muc_jid);
             await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
             await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
@@ -150,11 +146,10 @@ describe("Emojis", function () {
 
 
 
 
         it("properly inserts emojis into the chat textarea",
         it("properly inserts emojis into the chat textarea",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
+            await mock.waitForRoster(_converse, 'current', 0);
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const view = _converse.chatboxviews.get(muc_jid);
             const view = _converse.chatboxviews.get(muc_jid);
             await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
             await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
@@ -195,11 +190,10 @@ describe("Emojis", function () {
 
 
 
 
         it("allows you to search for particular emojis",
         it("allows you to search for particular emojis",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
+            await mock.waitForRoster(_converse, 'current', 0);
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const view = _converse.chatboxviews.get(muc_jid);
             const view = _converse.chatboxviews.get(muc_jid);
             await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
             await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
@@ -256,9 +250,7 @@ describe("Emojis", function () {
     describe("A Chat Message", function () {
     describe("A Chat Message", function () {
 
 
         it("will display larger if it's only emojis",
         it("will display larger if it's only emojis",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'use_system_emojis': true},
-                async function (done, _converse) {
+                mock.initConverse(['chatBoxesFetched'], {'use_system_emojis': true}, async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
             const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -342,9 +334,9 @@ describe("Emojis", function () {
         }));
         }));
 
 
         it("can render emojis as images",
         it("can render emojis as images",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'use_system_emojis': false},
-                async function (done, _converse) {
+                mock.initConverse(
+                    ['chatBoxesFetched'], {'use_system_emojis': false},
+                    async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
             const contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -391,7 +383,7 @@ describe("Emojis", function () {
 
 
         it("can show custom emojis",
         it("can show custom emojis",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'],
+                ['chatBoxesFetched'],
                 { emoji_categories: {
                 { emoji_categories: {
                     "smileys": ":grinning:",
                     "smileys": ":grinning:",
                     "people": ":thumbsup:",
                     "people": ":thumbsup:",

+ 1 - 1
spec/hats.js

@@ -6,7 +6,7 @@ describe("A XEP-0317 MUC Hat", function () {
 
 
     it("can be included in a presence stanza",
     it("can be included in a presence stanza",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';

+ 10 - 6
spec/headline.js

@@ -3,9 +3,9 @@
 describe("A headlines box", function () {
 describe("A headlines box", function () {
 
 
     it("will not open nor display non-headline messages",
     it("will not open nor display non-headline messages",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
+        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         const { $msg } = converse.env;
         const { $msg } = converse.env;
         /* XMPP spam message:
         /* XMPP spam message:
          *
          *
@@ -31,8 +31,9 @@ describe("A headlines box", function () {
     }));
     }));
 
 
     it("will open and display headline messages", mock.initConverse(
     it("will open and display headline messages", mock.initConverse(
-            ['rosterGroupsFetched'], {}, async function (done, _converse) {
+            [], {}, async function (done, _converse) {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         const { u, $msg} = converse.env;
         const { u, $msg} = converse.env;
         /* <message from='notify.example.com'
         /* <message from='notify.example.com'
          *          to='romeo@im.example.com'
          *          to='romeo@im.example.com'
@@ -67,8 +68,9 @@ describe("A headlines box", function () {
     }));
     }));
 
 
     it("will show headline messages in the controlbox", mock.initConverse(
     it("will show headline messages in the controlbox", mock.initConverse(
-        ['rosterGroupsFetched'], {}, async function (done, _converse) {
+            [], {}, async function (done, _converse) {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         const { u, $msg} = converse.env;
         const { u, $msg} = converse.env;
         /* <message from='notify.example.com'
         /* <message from='notify.example.com'
          *          to='romeo@im.example.com'
          *          to='romeo@im.example.com'
@@ -103,9 +105,10 @@ describe("A headlines box", function () {
     }));
     }));
 
 
     it("will remove headline messages from the controlbox if closed", mock.initConverse(
     it("will remove headline messages from the controlbox if closed", mock.initConverse(
-        ['rosterGroupsFetched'], {}, async function (done, _converse) {
+        [], {}, async function (done, _converse) {
 
 
         const { u, $msg} = converse.env;
         const { u, $msg} = converse.env;
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.openControlBox(_converse);
         await mock.openControlBox(_converse);
         /* <message from='notify.example.com'
         /* <message from='notify.example.com'
          *          to='romeo@im.example.com'
          *          to='romeo@im.example.com'
@@ -145,8 +148,9 @@ describe("A headlines box", function () {
 
 
     it("will not show a headline messages from a full JID if allow_non_roster_messaging is false",
     it("will not show a headline messages from a full JID if allow_non_roster_messaging is false",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
+            ['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         const { $msg } = converse.env;
         const { $msg } = converse.env;
         _converse.allow_non_roster_messaging = false;
         _converse.allow_non_roster_messaging = false;
         const stanza = $msg({
         const stanza = $msg({

+ 5 - 5
spec/http-file-upload.js

@@ -11,7 +11,7 @@ describe("XEP-0363: HTTP File Upload", function () {
 
 
         it("is done automatically",
         it("is done automatically",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    ['rosterContactsFetched', 'chatBoxesFetched'], {},
                         async function (done, _converse) {
                         async function (done, _converse) {
             const IQ_stanzas = _converse.connection.IQ_stanzas;
             const IQ_stanzas = _converse.connection.IQ_stanzas;
             await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
             await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
@@ -163,7 +163,7 @@ describe("XEP-0363: HTTP File Upload", function () {
             }));
             }));
 
 
             it("does not appear in MUC chats", mock.initConverse(
             it("does not appear in MUC chats", mock.initConverse(
-                    ['rosterGroupsFetched'], {},
+                    ['rosterContactsFetched'], {},
                     async (done, _converse) => {
                     async (done, _converse) => {
 
 
                 await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
                 await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -204,7 +204,7 @@ describe("XEP-0363: HTTP File Upload", function () {
             }));
             }));
 
 
             it("appears in MUC chats", mock.initConverse(
             it("appears in MUC chats", mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    ['rosterContactsFetched', 'chatBoxesFetched'], {},
                     async (done, _converse) => {
                     async (done, _converse) => {
 
 
                 await mock.waitUntilDiscoConfirmed(
                 await mock.waitUntilDiscoConfirmed(
@@ -224,7 +224,7 @@ describe("XEP-0363: HTTP File Upload", function () {
             describe("when clicked and a file chosen", function () {
             describe("when clicked and a file chosen", function () {
 
 
                 it("is uploaded and sent out", mock.initConverse(
                 it("is uploaded and sent out", mock.initConverse(
-                        ['rosterGroupsFetched', 'chatBoxesFetched'], {} ,async (done, _converse) => {
+                        ['rosterContactsFetched', 'chatBoxesFetched'], {} ,async (done, _converse) => {
 
 
                     const base_url = 'https://conversejs.org';
                     const base_url = 'https://conversejs.org';
                     await mock.waitUntilDiscoConfirmed(
                     await mock.waitUntilDiscoConfirmed(
@@ -561,7 +561,7 @@ describe("XEP-0363: HTTP File Upload", function () {
         describe("While a file is being uploaded", function () {
         describe("While a file is being uploaded", function () {
 
 
             it("shows a progress bar", mock.initConverse(
             it("shows a progress bar", mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
                 await mock.waitUntilDiscoConfirmed(
                 await mock.waitUntilDiscoConfirmed(

+ 4 - 4
spec/markers.js

@@ -9,7 +9,7 @@ describe("A XEP-0333 Chat Marker", function () {
 
 
     it("is sent when a markable message is received from a roster contact",
     it("is sent when a markable message is received from a roster contact",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -40,7 +40,7 @@ describe("A XEP-0333 Chat Marker", function () {
 
 
     it("is not sent when a markable message is received from someone not on the roster",
     it("is not sent when a markable message is received from someone not on the roster",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
+            ['rosterContactsFetched'], {'allow_non_roster_messaging': true},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitForRoster(_converse, 'current', 0);
@@ -75,7 +75,7 @@ describe("A XEP-0333 Chat Marker", function () {
 
 
     it("is ignored if it's a carbon copy of one that I sent from a different client",
     it("is ignored if it's a carbon copy of one that I sent from a different client",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -124,7 +124,7 @@ describe("A XEP-0333 Chat Marker", function () {
 
 
     it("may be returned for a MUC message",
     it("may be returned for a MUC message",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');

+ 2 - 2
spec/me-messages.js

@@ -6,7 +6,7 @@ describe("A Groupchat Message", function () {
 
 
     it("supports the /me command",
     it("supports the /me command",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
         await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
@@ -61,7 +61,7 @@ describe("A Groupchat Message", function () {
 
 
 describe("A Message", function () {
 describe("A Message", function () {
 
 
-    it("supports the /me command", mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) {
+    it("supports the /me command", mock.initConverse(['rosterContactsFetched'], {}, async function (done, _converse) {
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
         await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
         await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
         await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
         await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));

+ 9 - 9
spec/mentions.js

@@ -8,7 +8,7 @@ describe("An incoming groupchat message", function () {
 
 
     it("is specially marked when you are mentioned in it",
     it("is specially marked when you are mentioned in it",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -32,7 +32,7 @@ describe("An incoming groupchat message", function () {
 
 
     it("highlights all users mentioned via XEP-0372 references",
     it("highlights all users mentioned via XEP-0372 references",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -88,7 +88,7 @@ describe("An incoming groupchat message", function () {
 
 
     it("highlights all users mentioned via XEP-0372 references in a quoted message",
     it("highlights all users mentioned via XEP-0372 references in a quoted message",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -134,7 +134,7 @@ describe("A sent groupchat message", function () {
 
 
         it("gets parsed for mentions which get turned into references",
         it("gets parsed for mentions which get turned into references",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -266,7 +266,7 @@ describe("A sent groupchat message", function () {
 
 
         it("gets parsed for mentions as indicated with an @ preceded by a space or at the start of the text",
         it("gets parsed for mentions as indicated with an @ preceded by a space or at the start of the text",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -300,7 +300,7 @@ describe("A sent groupchat message", function () {
 
 
         it("properly encodes the URIs in sent out references",
         it("properly encodes the URIs in sent out references",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -344,7 +344,7 @@ describe("A sent groupchat message", function () {
 
 
         it("can get corrected and given new references",
         it("can get corrected and given new references",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -443,7 +443,7 @@ describe("A sent groupchat message", function () {
 
 
         it("includes a XEP-0372 references to that person",
         it("includes a XEP-0372 references to that person",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -494,7 +494,7 @@ describe("A sent groupchat message", function () {
 
 
     it("highlights all users mentioned via XEP-0372 references in a quoted message",
     it("highlights all users mentioned via XEP-0372 references in a quoted message",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         const members = [{'jid': 'gibson@gibson.net', 'nick': 'gibson', 'affiliation': 'member'}];
         const members = [{'jid': 'gibson@gibson.net', 'nick': 'gibson', 'affiliation': 'member'}];

+ 31 - 31
spec/messages.js

@@ -7,7 +7,7 @@ const u = converse.env.utils;
 describe("A Chat Message", function () {
 describe("A Chat Message", function () {
 
 
     it("will be demarcated if it's the first newly received message",
     it("will be demarcated if it's the first newly received message",
-        mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
+        mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -35,7 +35,7 @@ describe("A Chat Message", function () {
 
 
     it("is rejected if it's an unencapsulated forwarded message",
     it("is rejected if it's an unencapsulated forwarded message",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 2);
         await mock.waitForRoster(_converse, 'current', 2);
@@ -80,7 +80,7 @@ describe("A Chat Message", function () {
 
 
     it("can be received out of order, and will still be displayed in the right order",
     it("can be received out of order, and will still be displayed in the right order",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -245,7 +245,7 @@ describe("A Chat Message", function () {
 
 
     it("is ignored if it's a malformed headline message",
     it("is ignored if it's a malformed headline message",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -274,7 +274,7 @@ describe("A Chat Message", function () {
 
 
     it("can be a carbon message, as defined in XEP-0280",
     it("can be a carbon message, as defined in XEP-0280",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const include_nick = false;
         const include_nick = false;
@@ -325,7 +325,7 @@ describe("A Chat Message", function () {
 
 
     it("can be a carbon message that this user sent from a different client, as defined in XEP-0280",
     it("can be a carbon message that this user sent from a different client, as defined in XEP-0280",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
         await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
@@ -372,7 +372,7 @@ describe("A Chat Message", function () {
 
 
     it("will be discarded if it's a malicious message meant to look like a carbon copy",
     it("will be discarded if it's a malicious message meant to look like a carbon copy",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -418,7 +418,7 @@ describe("A Chat Message", function () {
 
 
     it("will indicate when it has a time difference of more than a day between it and its predecessor",
     it("will indicate when it has a time difference of more than a day between it and its predecessor",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const include_nick = false;
         const include_nick = false;
@@ -510,7 +510,7 @@ describe("A Chat Message", function () {
 
 
     it("is sanitized to prevent Javascript injection attacks",
     it("is sanitized to prevent Javascript injection attacks",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -530,7 +530,7 @@ describe("A Chat Message", function () {
 
 
     it("can contain hyperlinks, which will be clickable",
     it("can contain hyperlinks, which will be clickable",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -552,7 +552,7 @@ describe("A Chat Message", function () {
 
 
     it("will remove url query parameters from hyperlinks as set",
     it("will remove url query parameters from hyperlinks as set",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'],
+            ['rosterContactsFetched', 'chatBoxesFetched'],
             {'filter_url_query_params': ['utm_medium', 'utm_content', 's']},
             {'filter_url_query_params': ['utm_medium', 'utm_content', 's']},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
@@ -584,7 +584,7 @@ describe("A Chat Message", function () {
 
 
     it("will render newlines",
     it("will render newlines",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -635,7 +635,7 @@ describe("A Chat Message", function () {
 
 
     it("will render images from their URLs",
     it("will render images from their URLs",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -687,7 +687,7 @@ describe("A Chat Message", function () {
 
 
     it("will render images from approved URLs only",
     it("will render images from approved URLs only",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {'show_images_inline': ['conversejs.org']},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {'show_images_inline': ['conversejs.org']},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -711,7 +711,7 @@ describe("A Chat Message", function () {
 
 
     it("will fall back to rendering images as URLs",
     it("will fall back to rendering images as URLs",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -755,7 +755,7 @@ describe("A Chat Message", function () {
 
 
     it("will render the message time as configured",
     it("will render the message time as configured",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -781,7 +781,7 @@ describe("A Chat Message", function () {
 
 
     it("will be correctly identified and rendered as a followup message",
     it("will be correctly identified and rendered as a followup message",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {'debounced_content_rendering': false},
+            ['rosterContactsFetched'], {'debounced_content_rendering': false},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -943,7 +943,7 @@ describe("A Chat Message", function () {
 
 
         it("will appear inside the chatbox it was sent from",
         it("will appear inside the chatbox it was sent from",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -965,7 +965,7 @@ describe("A Chat Message", function () {
 
 
         it("will be trimmed of leading and trailing whitespace",
         it("will be trimmed of leading and trailing whitespace",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
@@ -986,7 +986,7 @@ describe("A Chat Message", function () {
 
 
         it("will open a chatbox and be displayed inside it",
         it("will open a chatbox and be displayed inside it",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const include_nick = false;
             const include_nick = false;
@@ -1032,7 +1032,7 @@ describe("A Chat Message", function () {
 
 
         it("will be trimmed of leading and trailing whitespace",
         it("will be trimmed of leading and trailing whitespace",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1, false);
             await mock.waitForRoster(_converse, 'current', 1, false);
@@ -1063,7 +1063,7 @@ describe("A Chat Message", function () {
 
 
             it("the VCard for that user is fetched and the chatbox updated with the results",
             it("the VCard for that user is fetched and the chatbox updated with the results",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
+                    ['rosterContactsFetched'], {'allow_non_roster_messaging': true},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 await mock.waitForRoster(_converse, 'current', 0);
                 await mock.waitForRoster(_converse, 'current', 0);
@@ -1117,7 +1117,7 @@ describe("A Chat Message", function () {
 
 
             it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true",
             it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched'], {'allow_non_roster_messaging': false},
+                    ['rosterContactsFetched'], {'allow_non_roster_messaging': false},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 await mock.waitForRoster(_converse, 'current', 0);
                 await mock.waitForRoster(_converse, 'current', 0);
@@ -1173,7 +1173,7 @@ describe("A Chat Message", function () {
 
 
             it("will have the error message displayed after itself",
             it("will have the error message displayed after itself",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    ['rosterContactsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 await mock.waitForRoster(_converse, 'current', 1);
                 await mock.waitForRoster(_converse, 'current', 1);
@@ -1298,7 +1298,7 @@ describe("A Chat Message", function () {
 
 
             it("will not show to the user an error message for a CSI message",
             it("will not show to the user an error message for a CSI message",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    ['rosterContactsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 // See #1317
                 // See #1317
@@ -1339,7 +1339,7 @@ describe("A Chat Message", function () {
 
 
         it("will cause the chat area to be scrolled down only if it was at the bottom originally",
         it("will cause the chat area to be scrolled down only if it was at the bottom originally",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -1376,7 +1376,7 @@ describe("A Chat Message", function () {
 
 
         it("is ignored if it's intended for a different resource and filter_by_resource is set to true",
         it("is ignored if it's intended for a different resource and filter_by_resource is set to true",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -1426,7 +1426,7 @@ describe("A Chat Message", function () {
 
 
         it("will render audio from oob mp3 URLs",
         it("will render audio from oob mp3 URLs",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
@@ -1476,7 +1476,7 @@ describe("A Chat Message", function () {
 
 
         it("will render video from oob mp4 URLs",
         it("will render video from oob mp4 URLs",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
@@ -1522,7 +1522,7 @@ describe("A Chat Message", function () {
 
 
         it("will render download links for files from oob URLs",
         it("will render download links for files from oob URLs",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
@@ -1551,7 +1551,7 @@ describe("A Chat Message", function () {
 
 
         it("will render images from oob URLs",
         it("will render images from oob URLs",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const base_url = 'https://conversejs.org';
             const base_url = 'https://conversejs.org';

+ 12 - 12
spec/minchats.js

@@ -9,7 +9,7 @@ describe("A chat message", function () {
 
 
     it("received for a minimized chat box will increment a counter on its header",
     it("received for a minimized chat box will increment a counter on its header",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         if (_converse.view_mode === 'fullscreen') {
         if (_converse.view_mode === 'fullscreen') {
@@ -76,7 +76,7 @@ describe("A Groupcaht", function () {
 
 
     it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
     it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
         await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
@@ -108,7 +108,7 @@ describe("A Chatbox", function () {
 
 
     it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
     it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -141,7 +141,7 @@ describe("A Chatbox", function () {
 
 
     it("can be opened in minimized mode initially",
     it("can be opened in minimized mode initially",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -159,7 +159,7 @@ describe("A Chatbox", function () {
 
 
 
 
     it("can be trimmed to conserve space",
     it("can be trimmed to conserve space",
-        mock.initConverse(['rosterGroupsFetched'], {},
+        mock.initConverse(['rosterContactsFetched'], {},
         async function (done, _converse) {
         async function (done, _converse) {
 
 
         spyOn(_converse.minimize, 'trimChats');
         spyOn(_converse.minimize, 'trimChats');
@@ -212,7 +212,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
 
 
     it("is displayed when scrolled up chatbox is minimized after receiving unread messages",
     it("is displayed when scrolled up chatbox is minimized after receiving unread messages",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -235,7 +235,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
 
 
     it("is incremented when message is received and windows is not focused",
     it("is incremented when message is received and windows is not focused",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -254,7 +254,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
 
 
     it("will render Openstreetmap-URL from geo-URI",
     it("will render Openstreetmap-URL from geo-URI",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -280,7 +280,7 @@ describe("The Minimized Chats Widget", function () {
 
 
     it("shows chats that have been minimized",
     it("shows chats that have been minimized",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -312,7 +312,7 @@ describe("The Minimized Chats Widget", function () {
 
 
     it("can be toggled to hide or show minimized chats",
     it("can be toggled to hide or show minimized chats",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -339,7 +339,7 @@ describe("The Minimized Chats Widget", function () {
 
 
     it("shows the number messages received to minimized chats",
     it("shows the number messages received to minimized chats",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 4);
         await mock.waitForRoster(_converse, 'current', 4);
@@ -415,7 +415,7 @@ describe("The Minimized Chats Widget", function () {
 
 
     it("shows the number messages received to minimized groupchats",
     it("shows the number messages received to minimized groupchats",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'kitchen@conference.shakespeare.lit';
         const muc_jid = 'kitchen@conference.shakespeare.lit';

+ 7 - 7
spec/modtools.js

@@ -22,7 +22,7 @@ describe("The groupchat moderator tool", function () {
 
 
     it("allows you to set affiliations and roles",
     it("allows you to set affiliations and roles",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@@ -143,7 +143,7 @@ describe("The groupchat moderator tool", function () {
 
 
     it("allows you to filter affiliation search results",
     it("allows you to filter affiliation search results",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@@ -199,7 +199,7 @@ describe("The groupchat moderator tool", function () {
 
 
     it("allows you to filter role search results",
     it("allows you to filter role search results",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@@ -310,7 +310,7 @@ describe("The groupchat moderator tool", function () {
 
 
     it("shows an error message if a particular affiliation list may not be retrieved",
     it("shows an error message if a particular affiliation list may not be retrieved",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@@ -362,7 +362,7 @@ describe("The groupchat moderator tool", function () {
 
 
     it("shows an error message if a particular affiliation may not be set",
     it("shows an error message if a particular affiliation may not be set",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@@ -429,7 +429,7 @@ describe("The groupchat moderator tool", function () {
 
 
     it("doesn't allow admins to make more admins",
     it("doesn't allow admins to make more admins",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
@@ -466,7 +466,7 @@ describe("The groupchat moderator tool", function () {
 
 
     it("lets the assignable affiliations and roles be configured via modtools_disable_assign",
     it("lets the assignable affiliations and roles be configured via modtools_disable_assign",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
         spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();

+ 1 - 1
spec/muc-mentions.js

@@ -9,7 +9,7 @@ describe("MUC Mention Notfications", function () {
 
 
     it("may be received from a MUC in which the user is not currently present",
     it("may be received from a MUC in which the user is not currently present",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {
+            ['rosterContactsFetched'], {
                 'allow_bookmarks': false, // Hack to get the rooms list to render
                 'allow_bookmarks': false, // Hack to get the rooms list to render
                 'muc_subscribe_to_rai': true,
                 'muc_subscribe_to_rai': true,
                 'view_mode': 'fullscreen'},
                 'view_mode': 'fullscreen'},

+ 81 - 81
spec/muc.js

@@ -15,7 +15,7 @@ describe("Groupchats", function () {
 
 
         it("has a method 'close' which closes rooms by JID or all rooms when called with no arguments",
         it("has a method 'close' which closes rooms by JID or all rooms when called with no arguments",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -50,7 +50,7 @@ describe("Groupchats", function () {
 
 
         it("has a method 'get' which returns a wrapped groupchat (if it exists)",
         it("has a method 'get' which returns a wrapped groupchat (if it exists)",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -96,7 +96,7 @@ describe("Groupchats", function () {
 
 
         it("has a method 'open' which opens (optionally configures) and returns a wrapped chat box",
         it("has a method 'open' which opens (optionally configures) and returns a wrapped chat box",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             // Mock 'getDiscoInfo', otherwise the room won't be
             // Mock 'getDiscoInfo', otherwise the room won't be
@@ -266,7 +266,7 @@ describe("Groupchats", function () {
 
 
         it("will be created when muc_instant_rooms is set to true",
         it("will be created when muc_instant_rooms is set to true",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             let IQ_stanzas = _converse.connection.IQ_stanzas;
             let IQ_stanzas = _converse.connection.IQ_stanzas;
@@ -403,7 +403,7 @@ describe("Groupchats", function () {
 
 
         it("maintains its state across reloads",
         it("maintains its state across reloads",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {
+                ['rosterContactsFetched'], {
                     'clear_messages_on_reconnection': true,
                     'clear_messages_on_reconnection': true,
                     'enable_smacks': false
                     'enable_smacks': false
                 }, async function (done, _converse) {
                 }, async function (done, _converse) {
@@ -511,7 +511,7 @@ describe("Groupchats", function () {
 
 
             it("will fetch the member list if muc_fetch_members is true",
             it("will fetch the member list if muc_fetch_members is true",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched'], {'muc_fetch_members': true},
+                    ['rosterContactsFetched'], {'muc_fetch_members': true},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 let sent_IQs = _converse.connection.IQ_stanzas;
                 let sent_IQs = _converse.connection.IQ_stanzas;
@@ -572,7 +572,7 @@ describe("Groupchats", function () {
 
 
                 it("gracefully handles being forbidden from fetching the lists for certain affiliations",
                 it("gracefully handles being forbidden from fetching the lists for certain affiliations",
                     mock.initConverse(
                     mock.initConverse(
-                        ['rosterGroupsFetched'], {'muc_fetch_members': true},
+                        ['rosterContactsFetched'], {'muc_fetch_members': true},
                         async function (done, _converse) {
                         async function (done, _converse) {
 
 
                     const sent_IQs = _converse.connection.IQ_stanzas;
                     const sent_IQs = _converse.connection.IQ_stanzas;
@@ -655,7 +655,7 @@ describe("Groupchats", function () {
 
 
             it("is shown the header",
             it("is shown the header",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched'], {},
+                    ['rosterContactsFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
                 await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
@@ -689,7 +689,7 @@ describe("Groupchats", function () {
 
 
             it("can be toggled by the user",
             it("can be toggled by the user",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched'], {},
+                    ['rosterContactsFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
                 await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
@@ -731,7 +731,7 @@ describe("Groupchats", function () {
 
 
             it("will always be shown when it's new",
             it("will always be shown when it's new",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched'], {},
+                    ['rosterContactsFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
                 await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
@@ -770,7 +770,7 @@ describe("Groupchats", function () {
 
 
             it("causes an info message to be shown when received in real-time",
             it("causes an info message to be shown when received in real-time",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched'], {},
+                    ['rosterContactsFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 spyOn(_converse.ChatRoom.prototype, 'handleSubjectChange').and.callThrough();
                 spyOn(_converse.ChatRoom.prototype, 'handleSubjectChange').and.callThrough();
@@ -833,7 +833,7 @@ describe("Groupchats", function () {
 
 
         it("restores cached messages when it reconnects and clear_messages_on_reconnection and muc_clear_messages_on_leave are false",
         it("restores cached messages when it reconnects and clear_messages_on_reconnection and muc_clear_messages_on_leave are false",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'],
+                ['rosterContactsFetched'],
                 {
                 {
                     'clear_messages_on_reconnection': false,
                     'clear_messages_on_reconnection': false,
                     'muc_clear_messages_on_leave': false
                     'muc_clear_messages_on_leave': false
@@ -866,7 +866,7 @@ describe("Groupchats", function () {
 
 
         it("clears cached messages when it reconnects and clear_messages_on_reconnection is true",
         it("clears cached messages when it reconnects and clear_messages_on_reconnection is true",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {'clear_messages_on_reconnection': true},
+                ['rosterContactsFetched'], {'clear_messages_on_reconnection': true},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -893,7 +893,7 @@ describe("Groupchats", function () {
 
 
         it("is opened when an xmpp: URI is clicked inside another groupchat",
         it("is opened when an xmpp: URI is clicked inside another groupchat",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -922,7 +922,7 @@ describe("Groupchats", function () {
 
 
         it("shows a notification if it's not anonymous",
         it("shows a notification if it's not anonymous",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'coven@chat.shakespeare.lit';
             const muc_jid = 'coven@chat.shakespeare.lit';
@@ -969,7 +969,7 @@ describe("Groupchats", function () {
 
 
         it("shows join/leave messages when users enter or exit a groupchat",
         it("shows join/leave messages when users enter or exit a groupchat",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_fetch_members': false},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'muc_fetch_members': false},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'coven@chat.shakespeare.lit';
             const muc_jid = 'coven@chat.shakespeare.lit';
@@ -1241,7 +1241,7 @@ describe("Groupchats", function () {
 
 
         it("combines subsequent join/leave messages when users enter or exit a groupchat",
         it("combines subsequent join/leave messages when users enter or exit a groupchat",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'romeo')
             await mock.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'romeo')
@@ -1379,7 +1379,7 @@ describe("Groupchats", function () {
 
 
         it("doesn't show the disconnection messages when join_leave_events is not in muc_show_info_messages setting",
         it("doesn't show the disconnection messages when join_leave_events is not in muc_show_info_messages setting",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_show_info_messages': []},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'muc_show_info_messages': []},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             spyOn(_converse.ChatRoom.prototype, 'onOccupantAdded').and.callThrough();
             spyOn(_converse.ChatRoom.prototype, 'onOccupantAdded').and.callThrough();
@@ -1420,7 +1420,7 @@ describe("Groupchats", function () {
 
 
         it("role-change messages that follow a MUC leave are left out",
         it("role-change messages that follow a MUC leave are left out",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             // See https://github.com/conversejs/converse.js/issues/1259
             // See https://github.com/conversejs/converse.js/issues/1259
@@ -1477,7 +1477,7 @@ describe("Groupchats", function () {
 
 
         it("can be configured if you're its owner",
         it("can be configured if you're its owner",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             let sent_IQ, IQ_id;
             let sent_IQ, IQ_id;
@@ -1688,7 +1688,7 @@ describe("Groupchats", function () {
 
 
         it("shows all members even if they're not currently present in the groupchat",
         it("shows all members even if they're not currently present in the groupchat",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit'
             const muc_jid = 'lounge@montague.lit'
@@ -1771,7 +1771,7 @@ describe("Groupchats", function () {
 
 
         it("shows users currently present in the groupchat",
         it("shows users currently present in the groupchat",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -1825,7 +1825,7 @@ describe("Groupchats", function () {
 
 
         it("indicates moderators and visitors by means of a special css class and tooltip",
         it("indicates moderators and visitors by means of a special css class and tooltip",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {'view_mode': 'fullscreen'},
+                ['rosterContactsFetched'], {'view_mode': 'fullscreen'},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -1891,7 +1891,7 @@ describe("Groupchats", function () {
 
 
         it("properly handles notification that a room has been destroyed",
         it("properly handles notification that a room has been destroyed",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openChatRoomViaModal(_converse, 'problematic@muc.montague.lit', 'romeo')
             await mock.openChatRoomViaModal(_converse, 'problematic@muc.montague.lit', 'romeo')
@@ -1921,7 +1921,7 @@ describe("Groupchats", function () {
 
 
         it("will use the user's reserved nickname, if it exists",
         it("will use the user's reserved nickname, if it exists",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const IQ_stanzas = _converse.connection.IQ_stanzas;
             const IQ_stanzas = _converse.connection.IQ_stanzas;
@@ -2014,7 +2014,7 @@ describe("Groupchats", function () {
 
 
         it("allows the user to invite their roster contacts to enter the groupchat",
         it("allows the user to invite their roster contacts to enter the groupchat",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             // We need roster contacts, so that we have someone to invite
             // We need roster contacts, so that we have someone to invite
@@ -2093,7 +2093,7 @@ describe("Groupchats", function () {
 
 
         it("can be joined automatically, based upon a received invite",
         it("can be joined automatically, based upon a received invite",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current'); // We need roster contacts, who can invite us
             await mock.waitForRoster(_converse, 'current'); // We need roster contacts, who can invite us
@@ -2129,7 +2129,7 @@ describe("Groupchats", function () {
 
 
         it("shows received groupchat messages",
         it("shows received groupchat messages",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const text = 'This is a received message';
             const text = 'This is a received message';
@@ -2161,7 +2161,7 @@ describe("Groupchats", function () {
 
 
         it("shows sent groupchat messages",
         it("shows sent groupchat messages",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -2207,7 +2207,7 @@ describe("Groupchats", function () {
 
 
         it("will cause the chat area to be scrolled down only if it was at the bottom already",
         it("will cause the chat area to be scrolled down only if it was at the bottom already",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const message = 'This message is received while the chat area is scrolled up';
             const message = 'This message is received while the chat area is scrolled up';
@@ -2249,7 +2249,7 @@ describe("Groupchats", function () {
 
 
         it("reconnects when no-acceptable error is returned when sending a message",
         it("reconnects when no-acceptable error is returned when sending a message",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'coven@chat.shakespeare.lit';
             const muc_jid = 'coven@chat.shakespeare.lit';
@@ -2304,7 +2304,7 @@ describe("Groupchats", function () {
 
 
         it("informs users if the room configuration has changed",
         it("informs users if the room configuration has changed",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'coven@chat.shakespeare.lit';
             const muc_jid = 'coven@chat.shakespeare.lit';
@@ -2331,7 +2331,7 @@ describe("Groupchats", function () {
 
 
         it("informs users if their nicknames have been changed.",
         it("informs users if their nicknames have been changed.",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             /* The service then sends two presence stanzas to the full JID
             /* The service then sends two presence stanzas to the full JID
@@ -2436,7 +2436,7 @@ describe("Groupchats", function () {
 
 
         it("queries for the groupchat information before attempting to join the user",
         it("queries for the groupchat information before attempting to join the user",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const nick = "some1";
             const nick = "some1";
@@ -2512,7 +2512,7 @@ describe("Groupchats", function () {
 
 
         it("updates the shown features when the groupchat configuration has changed",
         it("updates the shown features when the groupchat configuration has changed",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {'view_mode': 'fullscreen'},
+                ['rosterContactsFetched'], {'view_mode': 'fullscreen'},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             let features = [
             let features = [
@@ -2718,7 +2718,7 @@ describe("Groupchats", function () {
 
 
         it("indicates when a room is no longer anonymous",
         it("indicates when a room is no longer anonymous",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             let IQ_id;
             let IQ_id;
@@ -2767,7 +2767,7 @@ describe("Groupchats", function () {
 
 
         it("informs users if they have been kicked out of the groupchat",
         it("informs users if they have been kicked out of the groupchat",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             /*  <presence
             /*  <presence
@@ -2824,7 +2824,7 @@ describe("Groupchats", function () {
 
 
         it("informs users if they have exited the groupchat due to a technical reason",
         it("informs users if they have exited the groupchat due to a technical reason",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             /*  <presence
             /*  <presence
@@ -2876,7 +2876,7 @@ describe("Groupchats", function () {
 
 
         it("can be saved to, and retrieved from, browserStorage",
         it("can be saved to, and retrieved from, browserStorage",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
             await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
@@ -2910,7 +2910,7 @@ describe("Groupchats", function () {
 
 
         it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",
         it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
             await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
@@ -2931,7 +2931,7 @@ describe("Groupchats", function () {
 
 
         it("informs users of role and affiliation changes",
         it("informs users of role and affiliation changes",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -3002,7 +3002,7 @@ describe("Groupchats", function () {
 
 
         it("notifies users of role and affiliation changes for members not currently in the groupchat",
         it("notifies users of role and affiliation changes for members not currently in the groupchat",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -3049,7 +3049,7 @@ describe("Groupchats", function () {
 
 
         it("takes /help to show the available commands",
         it("takes /help to show the available commands",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             spyOn(window, 'confirm').and.callFake(() => true);
             spyOn(window, 'confirm').and.callFake(() => true);
@@ -3148,7 +3148,7 @@ describe("Groupchats", function () {
 
 
         it("takes /help to show the available commands and commands can be disabled by config",
         it("takes /help to show the available commands and commands can be disabled by config",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {muc_disable_slash_commands: ['mute', 'voice']},
+                ['rosterContactsFetched'], {muc_disable_slash_commands: ['mute', 'voice']},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -3186,7 +3186,7 @@ describe("Groupchats", function () {
 
 
         it("takes /member to make an occupant a member",
         it("takes /member to make an occupant a member",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             let iq_stanza;
             let iq_stanza;
@@ -3331,7 +3331,7 @@ describe("Groupchats", function () {
 
 
         it("takes /topic to set the groupchat topic",
         it("takes /topic to set the groupchat topic",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -3395,7 +3395,7 @@ describe("Groupchats", function () {
 
 
         it("takes /clear to clear messages",
         it("takes /clear to clear messages",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -3414,7 +3414,7 @@ describe("Groupchats", function () {
 
 
         it("takes /owner to make a user an owner",
         it("takes /owner to make a user an owner",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             let sent_IQ, IQ_id;
             let sent_IQ, IQ_id;
@@ -3506,7 +3506,7 @@ describe("Groupchats", function () {
 
 
         it("takes /ban to ban a user",
         it("takes /ban to ban a user",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             let sent_IQ, IQ_id;
             let sent_IQ, IQ_id;
@@ -3606,7 +3606,7 @@ describe("Groupchats", function () {
 
 
         it("takes a /kick command to kick a user",
         it("takes a /kick command to kick a user",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             let sent_IQ, IQ_id;
             let sent_IQ, IQ_id;
@@ -3698,7 +3698,7 @@ describe("Groupchats", function () {
 
 
         it("takes /op and /deop to make a user a moderator or not",
         it("takes /op and /deop to make a user a moderator or not",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -3839,7 +3839,7 @@ describe("Groupchats", function () {
 
 
         it("takes /mute and /voice to mute and unmute a user",
         it("takes /mute and /voice to mute and unmute a user",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -3977,7 +3977,7 @@ describe("Groupchats", function () {
 
 
         it("takes /destroy to destroy a muc",
         it("takes /destroy to destroy a muc",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -4072,7 +4072,7 @@ describe("Groupchats", function () {
 
 
         it("will use the nickname set in the global settings if the user doesn't have a VCard nickname",
         it("will use the nickname set in the global settings if the user doesn't have a VCard nickname",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'nickname': 'Benedict-Cucumberpatch'},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'nickname': 'Benedict-Cucumberpatch'},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openChatRoomViaModal(_converse, 'roomy@muc.montague.lit');
             await mock.openChatRoomViaModal(_converse, 'roomy@muc.montague.lit');
@@ -4083,7 +4083,7 @@ describe("Groupchats", function () {
 
 
         it("will show an error message if the groupchat requires a password",
         it("will show an error message if the groupchat requires a password",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'protected';
             const muc_jid = 'protected';
@@ -4119,7 +4119,7 @@ describe("Groupchats", function () {
 
 
         it("will show an error message if the groupchat is members-only and the user not included",
         it("will show an error message if the groupchat is members-only and the user not included",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'members-only@muc.montague.lit'
             const muc_jid = 'members-only@muc.montague.lit'
@@ -4168,7 +4168,7 @@ describe("Groupchats", function () {
 
 
         it("will show an error message if the user has been banned",
         it("will show an error message if the user has been banned",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'off-limits@muc.montague.lit'
             const muc_jid = 'off-limits@muc.montague.lit'
@@ -4213,7 +4213,7 @@ describe("Groupchats", function () {
 
 
         it("will render a nickname form if a nickname conflict happens and muc_nickname_from_jid=false",
         it("will render a nickname form if a nickname conflict happens and muc_nickname_from_jid=false",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'conflicted@muc.montague.lit';
             const muc_jid = 'conflicted@muc.montague.lit';
@@ -4258,7 +4258,7 @@ describe("Groupchats", function () {
 
 
         it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true",
         it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'conflicting@muc.montague.lit'
             const muc_jid = 'conflicting@muc.montague.lit'
@@ -4318,7 +4318,7 @@ describe("Groupchats", function () {
 
 
         it("will show an error message if the user is not allowed to have created the groupchat",
         it("will show an error message if the user is not allowed to have created the groupchat",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'impermissable@muc.montague.lit'
             const muc_jid = 'impermissable@muc.montague.lit'
@@ -4358,7 +4358,7 @@ describe("Groupchats", function () {
 
 
         it("will show an error message if the user's nickname doesn't conform to groupchat policy",
         it("will show an error message if the user's nickname doesn't conform to groupchat policy",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'conformist@muc.montague.lit'
             const muc_jid = 'conformist@muc.montague.lit'
@@ -4399,7 +4399,7 @@ describe("Groupchats", function () {
 
 
         it("will show an error message if the groupchat doesn't yet exist",
         it("will show an error message if the groupchat doesn't yet exist",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'nonexistent@muc.montague.lit'
             const muc_jid = 'nonexistent@muc.montague.lit'
@@ -4440,7 +4440,7 @@ describe("Groupchats", function () {
 
 
         it("will show an error message if the groupchat has reached its maximum number of participants",
         it("will show an error message if the groupchat has reached its maximum number of participants",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'maxed-out@muc.montague.lit'
             const muc_jid = 'maxed-out@muc.montague.lit'
@@ -4484,7 +4484,7 @@ describe("Groupchats", function () {
 
 
         it("will first be added to the member list if the groupchat is members only",
         it("will first be added to the member list if the groupchat is members only",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitForRoster(_converse, 'current', 0);
@@ -4632,7 +4632,7 @@ describe("Groupchats", function () {
 
 
         it("can be computed in various ways",
         it("can be computed in various ways",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'romeo');
             await mock.openChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'romeo');
@@ -4699,7 +4699,7 @@ describe("Groupchats", function () {
 
 
         it("can be opened from a link in the \"Groupchats\" section of the controlbox",
         it("can be opened from a link in the \"Groupchats\" section of the controlbox",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -4741,7 +4741,7 @@ describe("Groupchats", function () {
 
 
         it("doesn't show the nickname field if locked_muc_nickname is true",
         it("doesn't show the nickname field if locked_muc_nickname is true",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'locked_muc_nickname': true, 'muc_nickname_from_jid': true},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'locked_muc_nickname': true, 'muc_nickname_from_jid': true},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -4764,7 +4764,7 @@ describe("Groupchats", function () {
 
 
         it("uses the JID node if muc_nickname_from_jid is set to true",
         it("uses the JID node if muc_nickname_from_jid is set to true",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_nickname_from_jid': true},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'muc_nickname_from_jid': true},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -4783,7 +4783,7 @@ describe("Groupchats", function () {
 
 
         it("uses the nickname passed in to converse.initialize",
         it("uses the nickname passed in to converse.initialize",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'nickname': 'st.nick'},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'nickname': 'st.nick'},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -4802,7 +4802,7 @@ describe("Groupchats", function () {
 
 
         it("doesn't require the domain when muc_domain is set",
         it("doesn't require the domain when muc_domain is set",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org'},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org'},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -4842,7 +4842,7 @@ describe("Groupchats", function () {
 
 
         it("only uses the muc_domain is locked_muc_domain is true",
         it("only uses the muc_domain is locked_muc_domain is true",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org', 'locked_muc_domain': true},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org', 'locked_muc_domain': true},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -4884,7 +4884,7 @@ describe("Groupchats", function () {
 
 
         it("can be opened from a link in the \"Groupchats\" section of the controlbox",
         it("can be opened from a link in the \"Groupchats\" section of the controlbox",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -4960,7 +4960,7 @@ describe("Groupchats", function () {
 
 
         it("is pre-filled with the muc_domain",
         it("is pre-filled with the muc_domain",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'],
+                ['rosterContactsFetched', 'chatBoxesFetched'],
                 {'muc_domain': 'muc.example.org'},
                 {'muc_domain': 'muc.example.org'},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
@@ -4977,7 +4977,7 @@ describe("Groupchats", function () {
 
 
         it("doesn't let you set the MUC domain if it's locked",
         it("doesn't let you set the MUC domain if it's locked",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'],
+                ['rosterContactsFetched', 'chatBoxesFetched'],
                 {'muc_domain': 'chat.shakespeare.lit', 'locked_muc_domain': true},
                 {'muc_domain': 'chat.shakespeare.lit', 'locked_muc_domain': true},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
@@ -5028,7 +5028,7 @@ describe("Groupchats", function () {
 
 
         it("shows the number of unread mentions received",
         it("shows the number of unread mentions received",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {'allow_bookmarks': false},
+                ['rosterContactsFetched'], {'allow_bookmarks': false},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
@@ -5078,7 +5078,7 @@ describe("Groupchats", function () {
 
 
         it("is is not sent out to a MUC if the user is a visitor in a moderated room",
         it("is is not sent out to a MUC if the user is a visitor in a moderated room",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             spyOn(_converse.ChatRoom.prototype, 'sendChatState').and.callThrough();
             spyOn(_converse.ChatRoom.prototype, 'sendChatState').and.callThrough();
@@ -5132,7 +5132,7 @@ describe("Groupchats", function () {
 
 
             it("will be shown if received",
             it("will be shown if received",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched'], {},
+                    ['rosterContactsFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 const muc_jid = 'coven@chat.shakespeare.lit';
                 const muc_jid = 'coven@chat.shakespeare.lit';
@@ -5257,7 +5257,7 @@ describe("Groupchats", function () {
 
 
             it("will be shown if received",
             it("will be shown if received",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    ['rosterContactsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
                 const muc_jid = 'coven@chat.shakespeare.lit';
                 const muc_jid = 'coven@chat.shakespeare.lit';
@@ -5360,7 +5360,7 @@ describe("Groupchats", function () {
 
 
         it("will receive a user-friendly error message when trying to send a message",
         it("will receive a user-friendly error message when trying to send a message",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'trollbox@montague.lit';
             const muc_jid = 'trollbox@montague.lit';
@@ -5410,7 +5410,7 @@ describe("Groupchats", function () {
 
 
         it("will see an explanatory message instead of a textarea",
         it("will see an explanatory message instead of a textarea",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const features = [
             const features = [
@@ -5488,7 +5488,7 @@ describe("Groupchats", function () {
 
 
         it("sends presence probes when muc_send_probes is true",
         it("sends presence probes when muc_send_probes is true",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {'muc_send_probes': true},
+                ['rosterContactsFetched'], {'muc_send_probes': true},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';

+ 14 - 14
spec/muc_messages.js

@@ -13,7 +13,7 @@ describe("A Groupchat Message", function () {
 
 
         it("will have the error displayed below it",
         it("will have the error displayed below it",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -58,7 +58,7 @@ describe("A Groupchat Message", function () {
 
 
         it("is not rendered as a followup message",
         it("is not rendered as a followup message",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -96,7 +96,7 @@ describe("A Groupchat Message", function () {
 
 
         it("is not shown if its a duplicate",
         it("is not shown if its a duplicate",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -131,7 +131,7 @@ describe("A Groupchat Message", function () {
 
 
     it("is rejected if it's an unencapsulated forwarded message",
     it("is rejected if it's an unencapsulated forwarded message",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -166,7 +166,7 @@ describe("A Groupchat Message", function () {
 
 
     it("can contain a chat state notification and will still be shown",
     it("can contain a chat state notification and will still be shown",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -191,7 +191,7 @@ describe("A Groupchat Message", function () {
 
 
     it("can not be expected to have a unique id attribute",
     it("can not be expected to have a unique id attribute",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -222,7 +222,7 @@ describe("A Groupchat Message", function () {
 
 
     it("is ignored if it has the same archive-id of an already received one",
     it("is ignored if it has the same archive-id of an already received one",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'room@muc.example.com';
         const muc_jid = 'room@muc.example.com';
@@ -271,7 +271,7 @@ describe("A Groupchat Message", function () {
 
 
     it("is ignored if it has the same stanza-id of an already received one",
     it("is ignored if it has the same stanza-id of an already received one",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'room@muc.example.com';
         const muc_jid = 'room@muc.example.com';
@@ -319,7 +319,7 @@ describe("A Groupchat Message", function () {
 
 
     it("will be discarded if it's a malicious message meant to look like a carbon copy",
     it("will be discarded if it's a malicious message meant to look like a carbon copy",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -378,7 +378,7 @@ describe("A Groupchat Message", function () {
 
 
     it("keeps track of the sender's role and affiliation",
     it("keeps track of the sender's role and affiliation",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -506,7 +506,7 @@ describe("A Groupchat Message", function () {
 
 
     it("keeps track whether you are the sender or not",
     it("keeps track whether you are the sender or not",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';
@@ -526,7 +526,7 @@ describe("A Groupchat Message", function () {
 
 
     it("will be shown as received upon MUC reflection",
     it("will be shown as received upon MUC reflection",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -569,7 +569,7 @@ describe("A Groupchat Message", function () {
 
 
     it("gets updated with its stanza-id upon MUC reflection",
     it("gets updated with its stanza-id upon MUC reflection",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'room@muc.example.com';
         const muc_jid = 'room@muc.example.com';
@@ -604,7 +604,7 @@ describe("A Groupchat Message", function () {
 
 
     it("can cause a delivery receipt to be returned",
     it("can cause a delivery receipt to be returned",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {},
+            ['rosterContactsFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');

+ 10 - 5
spec/muclist.js

@@ -6,11 +6,12 @@ const u = converse.env.utils;
 describe("A list of open groupchats", function () {
 describe("A list of open groupchats", function () {
 
 
     it("is shown in controlbox", mock.initConverse(
     it("is shown in controlbox", mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'],
+            ['chatBoxesFetched'],
             { allow_bookmarks: false // Makes testing easier, otherwise we
             { allow_bookmarks: false // Makes testing easier, otherwise we
                                         // have to mock stanza traffic.
                                         // have to mock stanza traffic.
             }, async function (done, _converse) {
             }, async function (done, _converse) {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.openControlBox(_converse);
         await mock.openControlBox(_converse);
         const controlbox = _converse.chatboxviews.get('controlbox');
         const controlbox = _converse.chatboxviews.get('controlbox');
         let list = controlbox.querySelector('.list-container--openrooms');
         let list = controlbox.querySelector('.list-container--openrooms');
@@ -48,13 +49,14 @@ describe("A list of open groupchats", function () {
 
 
     it("uses bookmarks to determine groupchat names",
     it("uses bookmarks to determine groupchat names",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'],
+            ['chatBoxesFetched'],
             {'view_mode': 'fullscreen'},
             {'view_mode': 'fullscreen'},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const { Strophe, $iq, $pres, sizzle } = converse.env;
         const { Strophe, $iq, $pres, sizzle } = converse.env;
         const u = converse.env.utils;
         const u = converse.env.utils;
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
         let stanza = $pres({
         let stanza = $pres({
                 to: 'romeo@montague.lit/orchard',
                 to: 'romeo@montague.lit/orchard',
@@ -111,11 +113,12 @@ describe("A list of open groupchats", function () {
 describe("A groupchat shown in the groupchats list", function () {
 describe("A groupchat shown in the groupchats list", function () {
 
 
     it("is highlighted if it's currently open", mock.initConverse(
     it("is highlighted if it's currently open", mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'],
+            ['chatBoxesFetched'],
             { view_mode: 'fullscreen',
             { view_mode: 'fullscreen',
             allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
             allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
             }, async function (done, _converse) {
             }, async function (done, _converse) {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         const controlbox = _converse.chatboxviews.get('controlbox');
         const controlbox = _converse.chatboxviews.get('controlbox');
         const u = converse.env.utils;
         const u = converse.env.utils;
         const muc_jid = 'coven@chat.shakespeare.lit';
         const muc_jid = 'coven@chat.shakespeare.lit';
@@ -142,7 +145,7 @@ describe("A groupchat shown in the groupchats list", function () {
     }));
     }));
 
 
     it("has an info icon which opens a details modal when clicked", mock.initConverse(
     it("has an info icon which opens a details modal when clicked", mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'],
+            ['chatBoxesFetched'],
             { whitelisted_plugins: ['converse-roomslist'],
             { whitelisted_plugins: ['converse-roomslist'],
             allow_bookmarks: false // Makes testing easier, otherwise we
             allow_bookmarks: false // Makes testing easier, otherwise we
                                     // have to mock stanza traffic.
                                     // have to mock stanza traffic.
@@ -152,6 +155,7 @@ describe("A groupchat shown in the groupchats list", function () {
         const u = converse.env.utils;
         const u = converse.env.utils;
         const IQ_stanzas = _converse.connection.IQ_stanzas;
         const IQ_stanzas = _converse.connection.IQ_stanzas;
         const room_jid = 'coven@chat.shakespeare.lit';
         const room_jid = 'coven@chat.shakespeare.lit';
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.openControlBox(_converse);
         await mock.openControlBox(_converse);
         await _converse.api.rooms.open(room_jid, {'nick': 'some1'});
         await _converse.api.rooms.open(room_jid, {'nick': 'some1'});
         const view = _converse.chatboxviews.get(room_jid);
         const view = _converse.chatboxviews.get(room_jid);
@@ -250,7 +254,7 @@ describe("A groupchat shown in the groupchats list", function () {
     }));
     }));
 
 
     it("can be closed", mock.initConverse(
     it("can be closed", mock.initConverse(
-            ['rosterGroupsFetched'],
+            [],
             { whitelisted_plugins: ['converse-roomslist'],
             { whitelisted_plugins: ['converse-roomslist'],
             allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
             allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
             },
             },
@@ -259,6 +263,7 @@ describe("A groupchat shown in the groupchats list", function () {
         const u = converse.env.utils;
         const u = converse.env.utils;
         spyOn(window, 'confirm').and.callFake(() => true);
         spyOn(window, 'confirm').and.callFake(() => true);
         expect(_converse.chatboxes.length).toBe(1);
         expect(_converse.chatboxes.length).toBe(1);
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.openChatRoom(_converse, 'lounge', 'conference.shakespeare.lit', 'JC');
         await mock.openChatRoom(_converse, 'lounge', 'conference.shakespeare.lit', 'JC');
         expect(_converse.chatboxes.length).toBe(2);
         expect(_converse.chatboxes.length).toBe(2);
 
 

+ 8 - 8
spec/notification.js

@@ -12,7 +12,7 @@ describe("Notifications", function () {
             describe("an HTML5 Notification", function () {
             describe("an HTML5 Notification", function () {
 
 
                 it("is shown when a new private message is received",
                 it("is shown when a new private message is received",
-                        mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
+                        mock.initConverse(['rosterContactsFetched'], {}, async (done, _converse) => {
 
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
                     const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
@@ -34,7 +34,7 @@ describe("Notifications", function () {
                 }));
                 }));
 
 
                 it("is shown when you are mentioned in a groupchat",
                 it("is shown when you are mentioned in a groupchat",
-                        mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
+                        mock.initConverse(['rosterContactsFetched'], {}, async (done, _converse) => {
 
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
                     await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -79,7 +79,7 @@ describe("Notifications", function () {
                 }));
                 }));
 
 
                 it("is shown for headline messages",
                 it("is shown for headline messages",
-                        mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
+                        mock.initConverse(['rosterContactsFetched'], {}, async (done, _converse) => {
 
 
                     const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
                     const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
                     spyOn(window, 'Notification').and.returnValue(stub);
                     spyOn(window, 'Notification').and.returnValue(stub);
@@ -124,7 +124,7 @@ describe("Notifications", function () {
                 }));
                 }));
 
 
                 it("is shown when a user changes their chat state (if show_chat_state_notifications is true)",
                 it("is shown when a user changes their chat state (if show_chat_state_notifications is true)",
-                        mock.initConverse(['rosterGroupsFetched'], {show_chat_state_notifications: true},
+                        mock.initConverse(['rosterContactsFetched'], {show_chat_state_notifications: true},
                         async (done, _converse) => {
                         async (done, _converse) => {
 
 
                     await mock.waitForRoster(_converse, 'current', 3);
                     await mock.waitForRoster(_converse, 'current', 3);
@@ -153,7 +153,7 @@ describe("Notifications", function () {
         describe("A notification sound", function () {
         describe("A notification sound", function () {
 
 
             it("is played when the current user is mentioned in a groupchat",
             it("is played when the current user is mentioned in a groupchat",
-                    mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
+                    mock.initConverse(['rosterContactsFetched'], {}, async (done, _converse) => {
 
 
                 mock.createContacts(_converse, 'current');
                 mock.createContacts(_converse, 'current');
                 await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
                 await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -209,7 +209,7 @@ describe("Notifications", function () {
 
 
         it("is incremented when the message is received and the window is not focused",
         it("is incremented when the message is received and the window is not focused",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched'], {'show_tab_notifications': false},
+                    ['rosterContactsFetched'], {'show_tab_notifications': false},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -265,7 +265,7 @@ describe("Notifications", function () {
 
 
         it("is not incremented when the message is received and the window is focused",
         it("is not incremented when the message is received and the window is focused",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -296,7 +296,7 @@ describe("Notifications", function () {
 
 
         it("is incremented from zero when chatbox was closed after viewing previously received messages and the window is not focused now",
         it("is incremented from zero when chatbox was closed after viewing previously received messages and the window is not focused now",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');

+ 14 - 38
spec/omemo.js

@@ -73,9 +73,7 @@ async function initializedOMEMO (_converse) {
 describe("The OMEMO module", function() {
 describe("The OMEMO module", function() {
 
 
     it("adds methods for encrypting and decrypting messages via AES GCM",
     it("adds methods for encrypting and decrypting messages via AES GCM",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         const message = 'This message will be encrypted'
         const message = 'This message will be encrypted'
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -86,9 +84,7 @@ describe("The OMEMO module", function() {
     }));
     }));
 
 
     it("enables encrypted messages to be sent and received",
     it("enables encrypted messages to be sent and received",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         let sent_stanza;
         let sent_stanza;
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -224,9 +220,7 @@ describe("The OMEMO module", function() {
     }));
     }));
 
 
     it("enables encrypted groupchat messages to be sent and received",
     it("enables encrypted groupchat messages to be sent and received",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         // MEMO encryption works only in members only conferences
         // MEMO encryption works only in members only conferences
         // that are non-anonymous.
         // that are non-anonymous.
@@ -371,9 +365,7 @@ describe("The OMEMO module", function() {
     }));
     }));
 
 
     it("will create a new device based on a received carbon message",
     it("will create a new device based on a received carbon message",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
         await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -481,9 +473,7 @@ describe("The OMEMO module", function() {
     }));
     }));
 
 
     it("gracefully handles auth errors when trying to send encrypted groupchat messages",
     it("gracefully handles auth errors when trying to send encrypted groupchat messages",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         // MEMO encryption works only in members only conferences
         // MEMO encryption works only in members only conferences
         // that are non-anonymous.
         // that are non-anonymous.
@@ -610,9 +600,7 @@ describe("The OMEMO module", function() {
     }));
     }));
 
 
     it("can receive a PreKeySignalMessage",
     it("can receive a PreKeySignalMessage",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         _converse.NUM_PREKEYS = 5; // Restrict to 5, otherwise the resulting stanza is too large to easily test
         _converse.NUM_PREKEYS = 5; // Restrict to 5, otherwise the resulting stanza is too large to easily test
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -707,9 +695,7 @@ describe("The OMEMO module", function() {
     }));
     }));
 
 
     it("updates device lists based on PEP messages",
     it("updates device lists based on PEP messages",
-        mock.initConverse(
-            ['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
-            async function (done, _converse) {
+            mock.initConverse([], {'allow_non_roster_messaging': true}, async function (done, _converse) {
 
 
         await mock.waitUntilDiscoConfirmed(
         await mock.waitUntilDiscoConfirmed(
             _converse, _converse.bare_jid,
             _converse, _converse.bare_jid,
@@ -881,9 +867,7 @@ describe("The OMEMO module", function() {
 
 
 
 
     it("updates device bundles based on PEP messages",
     it("updates device bundles based on PEP messages",
-        mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse([], {}, async function (done, _converse) {
 
 
         await mock.waitUntilDiscoConfirmed(
         await mock.waitUntilDiscoConfirmed(
             _converse, _converse.bare_jid,
             _converse, _converse.bare_jid,
@@ -1031,9 +1015,7 @@ describe("The OMEMO module", function() {
     }));
     }));
 
 
     it("publishes a bundle with which an encrypted session can be created",
     it("publishes a bundle with which an encrypted session can be created",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         await mock.waitUntilDiscoConfirmed(
         await mock.waitUntilDiscoConfirmed(
             _converse, _converse.bare_jid,
             _converse, _converse.bare_jid,
@@ -1109,9 +1091,7 @@ describe("The OMEMO module", function() {
 
 
 
 
     it("adds a toolbar button for starting an encrypted chat session",
     it("adds a toolbar button for starting an encrypted chat session",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         await mock.waitUntilDiscoConfirmed(
         await mock.waitUntilDiscoConfirmed(
             _converse, _converse.bare_jid,
             _converse, _converse.bare_jid,
@@ -1230,7 +1210,7 @@ describe("The OMEMO module", function() {
         expect(devicelist.devices.at(1).get('id')).toBe('3300659945416e274474e469a1f0154c');
         expect(devicelist.devices.at(1).get('id')).toBe('3300659945416e274474e469a1f0154c');
         expect(devicelist.devices.at(2).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
         expect(devicelist.devices.at(2).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
         expect(devicelist.devices.at(3).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
         expect(devicelist.devices.at(3).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
-        await u.waitUntil(() => _converse.chatboxviews.get(contact_jid).el.querySelector('.chat-toolbar'));
+        await u.waitUntil(() => _converse.chatboxviews.get(contact_jid).querySelector('.chat-toolbar'));
         const view = _converse.chatboxviews.get(contact_jid);
         const view = _converse.chatboxviews.get(contact_jid);
         const toolbar = view.querySelector('.chat-toolbar');
         const toolbar = view.querySelector('.chat-toolbar');
         expect(view.model.get('omemo_active')).toBe(undefined);
         expect(view.model.get('omemo_active')).toBe(undefined);
@@ -1238,7 +1218,6 @@ describe("The OMEMO module", function() {
         expect(toggle === null).toBe(false);
         expect(toggle === null).toBe(false);
         expect(u.hasClass('fa-unlock', toggle.querySelector('converse-icon'))).toBe(true);
         expect(u.hasClass('fa-unlock', toggle.querySelector('converse-icon'))).toBe(true);
         expect(u.hasClass('fa-lock', toggle.querySelector('.converse-icon'))).toBe(false);
         expect(u.hasClass('fa-lock', toggle.querySelector('.converse-icon'))).toBe(false);
-
         view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
         view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
         toolbar.querySelector('.toggle-omemo').click();
         toolbar.querySelector('.toggle-omemo').click();
         expect(view.model.get('omemo_active')).toBe(true);
         expect(view.model.get('omemo_active')).toBe(true);
@@ -1271,10 +1250,9 @@ describe("The OMEMO module", function() {
     }));
     }));
 
 
     it("adds a toolbar button for starting an encrypted groupchat session",
     it("adds a toolbar button for starting an encrypted groupchat session",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilDiscoConfirmed(
         await mock.waitUntilDiscoConfirmed(
             _converse, _converse.bare_jid,
             _converse, _converse.bare_jid,
             [{'category': 'pubsub', 'type': 'pep'}],
             [{'category': 'pubsub', 'type': 'pep'}],
@@ -1444,9 +1422,7 @@ describe("The OMEMO module", function() {
 
 
 
 
     it("shows OMEMO device fingerprints in the user details modal",
     it("shows OMEMO device fingerprints in the user details modal",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         await mock.waitUntilDiscoConfirmed(
         await mock.waitUntilDiscoConfirmed(
             _converse, _converse.bare_jid,
             _converse, _converse.bare_jid,

+ 4 - 9
spec/presence.js

@@ -10,10 +10,9 @@ describe("A sent presence stanza", function () {
     afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
     afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
 
 
     it("includes a entity capabilities node",
     it("includes a entity capabilities node",
-        mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            (done, _converse) => {
+            mock.initConverse([], {}, async (done, _converse) => {
 
 
+        await mock.waitForRoster(_converse, 'current', 0);
         _converse.api.disco.own.identities.clear();
         _converse.api.disco.own.identities.clear();
         _converse.api.disco.own.features.clear();
         _converse.api.disco.own.features.clear();
 
 
@@ -66,9 +65,7 @@ describe("A sent presence stanza", function () {
     }));
     }));
 
 
     it("includes the saved status message",
     it("includes the saved status message",
-        mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async (done, _converse) => {
+        mock.initConverse([], {}, async (done, _converse) => {
 
 
         const { u, Strophe } = converse.env;
         const { u, Strophe } = converse.env;
         mock.openControlBox(_converse);
         mock.openControlBox(_converse);
@@ -116,9 +113,7 @@ describe("A sent presence stanza", function () {
 describe("A received presence stanza", function () {
 describe("A received presence stanza", function () {
 
 
     it("has its priority taken into account",
     it("has its priority taken into account",
-        mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async (done, _converse) => {
+        mock.initConverse([], {}, async (done, _converse) => {
 
 
         const u = converse.env.utils;
         const u = converse.env.utils;
         mock.openControlBox(_converse);
         mock.openControlBox(_converse);

+ 26 - 26
spec/protocol.js

@@ -39,13 +39,11 @@ describe("The Protocol", function () {
          * stanza of type "result".
          * stanza of type "result".
          */
          */
         it("Subscribe to contact, contact accepts and subscribes back",
         it("Subscribe to contact, contact accepts and subscribes back",
-            mock.initConverse(
-                ['rosterGroupsFetched'],
-                { roster_groups: false },
-                async function (done, _converse) {
+                mock.initConverse([], { roster_groups: false }, async function (done, _converse) {
 
 
             const { u, $iq, $pres, sizzle, Strophe } = converse.env;
             const { u, $iq, $pres, sizzle, Strophe } = converse.env;
             let contact, sent_stanza, IQ_id, stanza;
             let contact, sent_stanza, IQ_id, stanza;
+            await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
             await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
             await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'), 300);
             await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'), 300);
             /* The process by which a user subscribes to a contact, including
             /* The process by which a user subscribes to a contact, including
@@ -191,6 +189,7 @@ describe("The Protocol", function () {
              *    </query>
              *    </query>
              *  </iq>
              *  </iq>
              */
              */
+
             spyOn(_converse.roster, "updateContact").and.callThrough();
             spyOn(_converse.roster, "updateContact").and.callThrough();
             stanza = $iq({'type': 'set', 'from': _converse.bare_jid})
             stanza = $iq({'type': 'set', 'from': _converse.bare_jid})
                 .c('query', {'xmlns': 'jabber:iq:roster'})
                 .c('query', {'xmlns': 'jabber:iq:roster'})
@@ -201,15 +200,17 @@ describe("The Protocol", function () {
                     'name': 'contact@example.org'});
                     'name': 'contact@example.org'});
             _converse.connection._dataRecv(mock.createRequest(stanza));
             _converse.connection._dataRecv(mock.createRequest(stanza));
             expect(_converse.roster.updateContact).toHaveBeenCalled();
             expect(_converse.roster.updateContact).toHaveBeenCalled();
+
+            const rosterview = document.querySelector('converse-roster');
             // Check that the user is now properly shown as a pending
             // Check that the user is now properly shown as a pending
             // contact in the roster.
             // contact in the roster.
             await u.waitUntil(() => {
             await u.waitUntil(() => {
-                const header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop();
+                const header = sizzle('a:contains("Pending contacts")', rosterview).pop();
                 const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible);
                 const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible);
                 return contacts.length;
                 return contacts.length;
             }, 600);
             }, 600);
 
 
-            let header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop();
+            let header = sizzle('a:contains("Pending contacts")', rosterview).pop();
             let contacts = header.parentElement.querySelectorAll('li');
             let contacts = header.parentElement.querySelectorAll('li');
             expect(contacts.length).toBe(1);
             expect(contacts.length).toBe(1);
             expect(u.isVisible(contacts[0])).toBe(true);
             expect(u.isVisible(contacts[0])).toBe(true);
@@ -271,13 +272,12 @@ describe("The Protocol", function () {
             );
             );
             expect(_converse.roster.updateContact).toHaveBeenCalled();
             expect(_converse.roster.updateContact).toHaveBeenCalled();
 
 
-            // The contact should now be visible as an existing
-            // contact (but still offline).
+            // The contact should now be visible as an existing contact (but still offline).
             await u.waitUntil(() => {
             await u.waitUntil(() => {
-                const header = sizzle('a:contains("My contacts")', _converse.rosterview.el);
-                return sizzle('li', header[0].parentNode).filter(l => u.isVisible(l)).length;
+                const header = sizzle('a:contains("My contacts")', rosterview).pop();
+                return sizzle('li', header?.parentNode).filter(l => u.isVisible(l)).length;
             }, 600);
             }, 600);
-            header = sizzle('a:contains("My contacts")', _converse.rosterview.el);
+            header = sizzle('a:contains("My contacts")', rosterview);
             expect(header.length).toBe(1);
             expect(header.length).toBe(1);
             expect(u.isVisible(header[0])).toBeTruthy();
             expect(u.isVisible(header[0])).toBeTruthy();
             contacts = header[0].parentNode.querySelectorAll('li');
             contacts = header[0].parentNode.querySelectorAll('li');
@@ -359,15 +359,14 @@ describe("The Protocol", function () {
         }));
         }));
 
 
         it("Alternate Flow: Contact Declines Subscription Request",
         it("Alternate Flow: Contact Declines Subscription Request",
-            mock.initConverse(
-                ['rosterGroupsFetched'], {},
-                function (done, _converse) {
+                mock.initConverse([], {}, async function (done, _converse) {
 
 
             const { $iq, $pres } = converse.env;
             const { $iq, $pres } = converse.env;
             /* The process by which a user subscribes to a contact, including
             /* The process by which a user subscribes to a contact, including
              * the interaction between roster items and subscription states.
              * the interaction between roster items and subscription states.
              */
              */
             var contact, stanza, sent_stanza, sent_IQ;
             var contact, stanza, sent_stanza, sent_IQ;
+            await mock.waitForRoster(_converse, 'current', 0);
             mock.openControlBox(_converse);
             mock.openControlBox(_converse);
             // Add a new roster contact via roster push
             // Add a new roster contact via roster push
             stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
             stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
@@ -443,10 +442,7 @@ describe("The Protocol", function () {
         }));
         }));
 
 
         it("Unsubscribe to a contact when subscription is mutual",
         it("Unsubscribe to a contact when subscription is mutual",
-            mock.initConverse(
-                ['rosterGroupsFetched'],
-                { roster_groups: false },
-                async function (done, _converse) {
+                mock.initConverse([], { roster_groups: false }, async function (done, _converse) {
 
 
             const { u, $iq, sizzle, Strophe } = converse.env;
             const { u, $iq, sizzle, Strophe } = converse.env;
             const jid = 'abram@montague.lit';
             const jid = 'abram@montague.lit';
@@ -456,7 +452,8 @@ describe("The Protocol", function () {
             // We now have a contact we want to remove
             // We now have a contact we want to remove
             expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy();
             expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy();
 
 
-            const header = sizzle('a:contains("My contacts")', _converse.rosterview.el).pop();
+            const rosterview = document.querySelector('converse-roster');
+            const header = sizzle('a:contains("My contacts")', rosterview).pop();
             await u.waitUntil(() => header.parentElement.querySelectorAll('li').length);
             await u.waitUntil(() => header.parentElement.querySelectorAll('li').length);
 
 
             // remove the first user
             // remove the first user
@@ -483,6 +480,7 @@ describe("The Protocol", function () {
              * </iq>
              * </iq>
              */
              */
             const sent_iq = _converse.connection.IQ_stanzas.pop();
             const sent_iq = _converse.connection.IQ_stanzas.pop();
+
             expect(Strophe.serialize(sent_iq)).toBe(
             expect(Strophe.serialize(sent_iq)).toBe(
                 `<iq id="${sent_iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
                 `<iq id="${sent_iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
                     `<query xmlns="jabber:iq:roster">`+
                     `<query xmlns="jabber:iq:roster">`+
@@ -500,8 +498,7 @@ describe("The Protocol", function () {
         }));
         }));
 
 
         it("Receiving a subscription request", mock.initConverse(
         it("Receiving a subscription request", mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async function (done, _converse) {
+                [], {}, async function (done, _converse) {
 
 
             const { u, $pres, sizzle, Strophe } = converse.env;
             const { u, $pres, sizzle, Strophe } = converse.env;
             spyOn(_converse.api, "trigger").and.callThrough();
             spyOn(_converse.api, "trigger").and.callThrough();
@@ -519,16 +516,19 @@ describe("The Protocol", function () {
             }).c('nick', {
             }).c('nick', {
                 'xmlns': Strophe.NS.NICK,
                 'xmlns': Strophe.NS.NICK,
             }).t('Clint Contact');
             }).t('Clint Contact');
+
+
             _converse.connection._dataRecv(mock.createRequest(stanza));
             _converse.connection._dataRecv(mock.createRequest(stanza));
+            const rosterview = document.querySelector('converse-roster');
             await u.waitUntil(() => {
             await u.waitUntil(() => {
-                const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop();
-                const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible);
-                return contacts.length;
+                const header = sizzle('a:contains("Contact requests")', rosterview).pop();
+                return Array.from(header?.parentElement.querySelectorAll('li') ?? []).filter(u.isVisible)?.length;
             }, 500);
             }, 500);
             expect(_converse.api.trigger).toHaveBeenCalledWith('contactRequest', jasmine.any(Object));
             expect(_converse.api.trigger).toHaveBeenCalledWith('contactRequest', jasmine.any(Object));
-            const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop();
+
+            const header = sizzle('a:contains("Contact requests")', rosterview).pop();
             expect(u.isVisible(header)).toBe(true);
             expect(u.isVisible(header)).toBe(true);
-            const contacts = header.parentElement.querySelectorAll('li');
+            const contacts = header.nextElementSibling.querySelectorAll('li');
             expect(contacts.length).toBe(1);
             expect(contacts.length).toBe(1);
             done();
             done();
         }));
         }));

+ 7 - 15
spec/push.js

@@ -13,7 +13,7 @@ describe("XEP-0357 Push Notifications", function () {
 
 
     it("can be enabled",
     it("can be enabled",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {
+            [], {
                 'push_app_servers': [{
                 'push_app_servers': [{
                     'jid': 'push-5@client.example',
                     'jid': 'push-5@client.example',
                     'node': 'yxs32uqsflafdk3iuqo'
                     'node': 'yxs32uqsflafdk3iuqo'
@@ -51,7 +51,7 @@ describe("XEP-0357 Push Notifications", function () {
 
 
     it("can be enabled for a MUC domain",
     it("can be enabled for a MUC domain",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {
+            [], {
                 'enable_muc_push': true,
                 'enable_muc_push': true,
                 'push_app_servers': [{
                 'push_app_servers': [{
                     'jid': 'push-5@client.example',
                     'jid': 'push-5@client.example',
@@ -68,10 +68,7 @@ describe("XEP-0357 Push Notifications", function () {
             _converse, _converse.bare_jid, [],
             _converse, _converse.bare_jid, [],
             ['urn:xmpp:push:0']);
             ['urn:xmpp:push:0']);
 
 
-        let iq = await u.waitUntil(() => _.filter(
-            IQ_stanzas,
-            iq => sizzle(`iq[type="set"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length
-        ).pop());
+        let iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(`iq[type="set"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length).pop());
 
 
         expect(Strophe.serialize(iq)).toBe(
         expect(Strophe.serialize(iq)).toBe(
             `<iq id="${iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
             `<iq id="${iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
@@ -111,7 +108,7 @@ describe("XEP-0357 Push Notifications", function () {
 
 
     it("can be disabled",
     it("can be disabled",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {
+            [], {
                 'push_app_servers': [{
                 'push_app_servers': [{
                     'jid': 'push-5@client.example',
                     'jid': 'push-5@client.example',
                     'node': 'yxs32uqsflafdk3iuqo',
                     'node': 'yxs32uqsflafdk3iuqo',
@@ -127,9 +124,7 @@ describe("XEP-0357 Push Notifications", function () {
             _converse.bare_jid,
             _converse.bare_jid,
             [{'category': 'account', 'type':'registered'}],
             [{'category': 'account', 'type':'registered'}],
             ['urn:xmpp:push:0'], [], 'info');
             ['urn:xmpp:push:0'], [], 'info');
-        const stanza = await u.waitUntil(
-            () => _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] disable[xmlns="urn:xmpp:push:0"]')).pop()
-        );
+        const stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector('iq[type="set"] disable[xmlns="urn:xmpp:push:0"]')).pop());
         expect(Strophe.serialize(stanza)).toEqual(
         expect(Strophe.serialize(stanza)).toEqual(
             `<iq id="${stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
             `<iq id="${stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
                 '<disable jid="push-5@client.example" node="yxs32uqsflafdk3iuqo" xmlns="urn:xmpp:push:0"/>'+
                 '<disable jid="push-5@client.example" node="yxs32uqsflafdk3iuqo" xmlns="urn:xmpp:push:0"/>'+
@@ -146,8 +141,7 @@ describe("XEP-0357 Push Notifications", function () {
 
 
 
 
     it("can require a secret token to be included",
     it("can require a secret token to be included",
-        mock.initConverse(
-            ['rosterGroupsFetched'], {
+        mock.initConverse([], {
                 'push_app_servers': [{
                 'push_app_servers': [{
                     'jid': 'push-5@client.example',
                     'jid': 'push-5@client.example',
                     'node': 'yxs32uqsflafdk3iuqo',
                     'node': 'yxs32uqsflafdk3iuqo',
@@ -168,9 +162,7 @@ describe("XEP-0357 Push Notifications", function () {
                 [{'category': 'account', 'type':'registered'}],
                 [{'category': 'account', 'type':'registered'}],
                 ['urn:xmpp:push:0'], [], 'info');
                 ['urn:xmpp:push:0'], [], 'info');
 
 
-        const stanza = await u.waitUntil(
-            () => _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop()
-        );
+        const stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop());
         expect(Strophe.serialize(stanza)).toEqual(
         expect(Strophe.serialize(stanza)).toEqual(
             `<iq id="${stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
             `<iq id="${stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
                 '<enable jid="push-5@client.example" node="yxs32uqsflafdk3iuqo" xmlns="urn:xmpp:push:0">'+
                 '<enable jid="push-5@client.example" node="yxs32uqsflafdk3iuqo" xmlns="urn:xmpp:push:0">'+

+ 3 - 3
spec/rai.js

@@ -9,7 +9,7 @@ describe("XEP-0437 Room Activity Indicators", function () {
 
 
     it("will be activated for a MUC that becomes hidden",
     it("will be activated for a MUC that becomes hidden",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {
+            ['rosterContactsFetched'], {
                 'allow_bookmarks': false, // Hack to get the rooms list to render
                 'allow_bookmarks': false, // Hack to get the rooms list to render
                 'muc_subscribe_to_rai': true,
                 'muc_subscribe_to_rai': true,
                 'view_mode': 'fullscreen'},
                 'view_mode': 'fullscreen'},
@@ -116,7 +116,7 @@ describe("XEP-0437 Room Activity Indicators", function () {
 
 
     it("will be activated for a MUC that starts out hidden",
     it("will be activated for a MUC that starts out hidden",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {
+            ['rosterContactsFetched'], {
                 'allow_bookmarks': false, // Hack to get the rooms list to render
                 'allow_bookmarks': false, // Hack to get the rooms list to render
                 'muc_subscribe_to_rai': true,
                 'muc_subscribe_to_rai': true,
                 'view_mode': 'fullscreen'},
                 'view_mode': 'fullscreen'},
@@ -181,7 +181,7 @@ describe("XEP-0437 Room Activity Indicators", function () {
 
 
     it("may not be activated due to server resource constraints",
     it("may not be activated due to server resource constraints",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched'], {
+            ['rosterContactsFetched'], {
                 'allow_bookmarks': false, // Hack to get the rooms list to render
                 'allow_bookmarks': false, // Hack to get the rooms list to render
                 'muc_subscribe_to_rai': true,
                 'muc_subscribe_to_rai': true,
                 'view_mode': 'fullscreen'},
                 'view_mode': 'fullscreen'},

+ 4 - 4
spec/receipts.js

@@ -8,7 +8,7 @@ describe("A delivery receipt", function () {
 
 
     it("is emitted for a received message which requests it",
     it("is emitted for a received message which requests it",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
@@ -34,7 +34,7 @@ describe("A delivery receipt", function () {
 
 
     it("is not emitted for a carbon message",
     it("is not emitted for a carbon message",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -64,7 +64,7 @@ describe("A delivery receipt", function () {
 
 
     it("is not emitted for an archived message",
     it("is not emitted for an archived message",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
@@ -101,7 +101,7 @@ describe("A delivery receipt", function () {
 
 
     it("can be received for a sent message",
     it("can be received for a sent message",
         mock.initConverse(
         mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            ['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);

+ 15 - 15
spec/retractions.js

@@ -38,7 +38,7 @@ describe("Message Retractions", function () {
 
 
         it("is not applied if it's not from the right author",
         it("is not applied if it's not from the right author",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -82,7 +82,7 @@ describe("Message Retractions", function () {
 
 
         it("can be received before the message it pertains to",
         it("can be received before the message it pertains to",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const date = (new Date()).toISOString();
             const date = (new Date()).toISOString();
@@ -138,7 +138,7 @@ describe("Message Retractions", function () {
 
 
         it("can be received before the message it pertains to",
         it("can be received before the message it pertains to",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const date = (new Date()).toISOString();
             const date = (new Date()).toISOString();
@@ -199,7 +199,7 @@ describe("Message Retractions", function () {
 
 
         it("can be received before the message it pertains to",
         it("can be received before the message it pertains to",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const date = (new Date()).toISOString();
             const date = (new Date()).toISOString();
@@ -257,7 +257,7 @@ describe("Message Retractions", function () {
 
 
         it("can be followed up by a retraction",
         it("can be followed up by a retraction",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
@@ -327,7 +327,7 @@ describe("Message Retractions", function () {
 
 
         it("can be retracted by its author",
         it("can be retracted by its author",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.waitForRoster(_converse, 'current', 1);
@@ -377,7 +377,7 @@ describe("Message Retractions", function () {
 
 
         it("can be followed up by a retraction by the author",
         it("can be followed up by a retraction by the author",
                 mock.initConverse(
                 mock.initConverse(
-                    ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    ['rosterContactsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
                     async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -421,7 +421,7 @@ describe("Message Retractions", function () {
 
 
         it("can be retracted by a moderator, with the IQ response received before the retraction message",
         it("can be retracted by a moderator, with the IQ response received before the retraction message",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -507,7 +507,7 @@ describe("Message Retractions", function () {
 
 
         it("can not be retracted if the MUC doesn't support message moderation",
         it("can not be retracted if the MUC doesn't support message moderation",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -533,7 +533,7 @@ describe("Message Retractions", function () {
 
 
         it("can be retracted by a moderator, with the retraction message received before the IQ response",
         it("can be retracted by a moderator, with the retraction message received before the IQ response",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -604,7 +604,7 @@ describe("Message Retractions", function () {
 
 
         it("can be retracted by its author",
         it("can be retracted by its author",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -662,7 +662,7 @@ describe("Message Retractions", function () {
 
 
         it("can be retracted by its author, causing an error message in response",
         it("can be retracted by its author, causing an error message in response",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -711,7 +711,7 @@ describe("Message Retractions", function () {
 
 
         it("can be retracted by its author, causing a timeout error in response",
         it("can be retracted by its author, causing a timeout error in response",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             _converse.STANZA_TIMEOUT = 1;
             _converse.STANZA_TIMEOUT = 1;
@@ -748,7 +748,7 @@ describe("Message Retractions", function () {
 
 
         it("can be retracted by a moderator",
         it("can be retracted by a moderator",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';
@@ -800,7 +800,7 @@ describe("Message Retractions", function () {
 
 
         it("can be retracted by the sender if they're a moderator",
         it("can be retracted by the sender if they're a moderator",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'allow_message_retraction': 'moderator'},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'allow_message_retraction': 'moderator'},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'lounge@montague.lit';
             const muc_jid = 'lounge@montague.lit';

+ 2 - 2
spec/room_registration.js

@@ -12,7 +12,7 @@ describe("Chatrooms", function () {
 
 
         it("allows you to register your nickname in a room",
         it("allows you to register your nickname in a room",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'coven@chat.shakespeare.lit';
             const muc_jid = 'coven@chat.shakespeare.lit';
@@ -69,7 +69,7 @@ describe("Chatrooms", function () {
 
 
         it("allows you to automatically register your nickname when joining a room",
         it("allows you to automatically register your nickname when joining a room",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             const muc_jid = 'coven@chat.shakespeare.lit';
             const muc_jid = 'coven@chat.shakespeare.lit';

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 225 - 235
spec/roster.js


+ 0 - 5
spec/smacks.js

@@ -18,9 +18,6 @@ describe("XEP-0198 Stream Management", function () {
             },
             },
             async function (done, _converse) {
             async function (done, _converse) {
 
 
-        const view = _converse.chatboxviews.get('controlbox');
-        spyOn(view, 'renderControlBoxPane').and.callThrough();
-
         await _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
         await _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
         const sent_stanzas = _converse.connection.sent_stanzas;
         const sent_stanzas = _converse.connection.sent_stanzas;
         let stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'enable'), 1000).pop());
         let stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'enable'), 1000).pop());
@@ -32,8 +29,6 @@ describe("XEP-0198 Stream Management", function () {
         _converse.connection._dataRecv(mock.createRequest(result));
         _converse.connection._dataRecv(mock.createRequest(result));
         expect(_converse.session.get('smacks_enabled')).toBe(true);
         expect(_converse.session.get('smacks_enabled')).toBe(true);
 
 
-        await u.waitUntil(() => view.renderControlBoxPane.calls?.count());
-
         let IQ_stanzas = _converse.connection.IQ_stanzas;
         let IQ_stanzas = _converse.connection.IQ_stanzas;
         await u.waitUntil(() => IQ_stanzas.length === 4);
         await u.waitUntil(() => IQ_stanzas.length === 4);
 
 

+ 4 - 12
spec/spoilers.js

@@ -8,9 +8,7 @@ describe("A spoiler message", function () {
     afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
     afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
 
 
     it("can be received with a hint",
     it("can be received with a hint",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async (done, _converse) => {
+        mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
         const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -48,9 +46,7 @@ describe("A spoiler message", function () {
     }));
     }));
 
 
     it("can be received without a hint",
     it("can be received without a hint",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async (done, _converse) => {
+            mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
 
 
         await mock.waitForRoster(_converse, 'current');
         await mock.waitForRoster(_converse, 'current');
         const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -87,9 +83,7 @@ describe("A spoiler message", function () {
     }));
     }));
 
 
     it("can be sent without a hint",
     it("can be sent without a hint",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async (done, _converse) => {
+            mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
         mock.openControlBox(_converse);
         mock.openControlBox(_converse);
@@ -166,9 +160,7 @@ describe("A spoiler message", function () {
     }));
     }));
 
 
     it("can be sent with a hint",
     it("can be sent with a hint",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async (done, _converse) => {
+            mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
         mock.openControlBox(_converse);
         mock.openControlBox(_converse);

+ 6 - 6
spec/styling.js

@@ -5,7 +5,7 @@ const { u, Promise, $msg } = converse.env;
 describe("An incoming chat Message", function () {
 describe("An incoming chat Message", function () {
 
 
     it("can have styling disabled via an \"unstyled\" element",
     it("can have styling disabled via an \"unstyled\" element",
-        mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
+        mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const include_nick = false;
         const include_nick = false;
@@ -37,7 +37,7 @@ describe("An incoming chat Message", function () {
 
 
 
 
     it("can have styling disabled via the allow_message_styling setting",
     it("can have styling disabled via the allow_message_styling setting",
-        mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {'allow_message_styling': false},
+        mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {'allow_message_styling': false},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const include_nick = false;
         const include_nick = false;
@@ -67,7 +67,7 @@ describe("An incoming chat Message", function () {
     }));
     }));
 
 
     it("can be styled with span XEP-0393 message styling hints",
     it("can be styled with span XEP-0393 message styling hints",
-        mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
+        mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         let msg_text, msg, msg_el;
         let msg_text, msg, msg_el;
@@ -192,7 +192,7 @@ describe("An incoming chat Message", function () {
     }));
     }));
 
 
     it("can be styled with block XEP-0393 message styling hints",
     it("can be styled with block XEP-0393 message styling hints",
-        mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
+        mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         let msg_text, msg, msg_el;
         let msg_text, msg, msg_el;
@@ -240,7 +240,7 @@ describe("An incoming chat Message", function () {
     }));
     }));
 
 
     it("can be styled with quote XEP-0393 message styling hints",
     it("can be styled with quote XEP-0393 message styling hints",
-        mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
+        mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         let msg_text, msg, msg_el;
         let msg_text, msg, msg_el;
@@ -392,7 +392,7 @@ describe("An incoming chat Message", function () {
 describe("A outgoing groupchat Message", function () {
 describe("A outgoing groupchat Message", function () {
 
 
     it("can be styled with span XEP-0393 message styling hints that contain mentions",
     it("can be styled with span XEP-0393 message styling hints that contain mentions",
-        mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
+        mock.initConverse(['rosterContactsFetched', 'chatBoxesFetched'], {},
             async function (done, _converse) {
             async function (done, _converse) {
 
 
         const muc_jid = 'lounge@montague.lit';
         const muc_jid = 'lounge@montague.lit';

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

@@ -5,9 +5,7 @@ const u = converse.env.utils;
 describe("The User Details Modal", function () {
 describe("The User Details Modal", function () {
 
 
     it("can be used to remove a contact",
     it("can be used to remove a contact",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
         _converse.api.trigger('rosterContactsFetched');
         _converse.api.trigger('rosterContactsFetched');
@@ -36,7 +34,7 @@ describe("The User Details Modal", function () {
     }));
     }));
 
 
     it("shows an alert when an error happened while removing the contact",
     it("shows an alert when an error happened while removing the contact",
-            mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) {
+            mock.initConverse([], {}, async function (done, _converse) {
 
 
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
         _converse.api.trigger('rosterContactsFetched');
         _converse.api.trigger('rosterContactsFetched');

+ 6 - 6
spec/xss.js

@@ -9,7 +9,7 @@ describe("XSS", function () {
 
 
         it("will escape IMG payload XSS attempts",
         it("will escape IMG payload XSS attempts",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             spyOn(window, 'alert').and.callThrough();
             spyOn(window, 'alert').and.callThrough();
@@ -69,7 +69,7 @@ describe("XSS", function () {
 
 
         it("will escape SVG payload XSS attempts",
         it("will escape SVG payload XSS attempts",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             spyOn(window, 'alert').and.callThrough();
             spyOn(window, 'alert').and.callThrough();
@@ -128,7 +128,7 @@ describe("XSS", function () {
 
 
         it("will have properly escaped URLs",
         it("will have properly escaped URLs",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -186,7 +186,7 @@ describe("XSS", function () {
 
 
         it("will avoid malformed and unsafe urls urls from rendering as anchors",
         it("will avoid malformed and unsafe urls urls from rendering as anchors",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                ['rosterContactsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.waitForRoster(_converse, 'current');
             await mock.waitForRoster(_converse, 'current');
@@ -270,7 +270,7 @@ describe("XSS", function () {
     describe("A Groupchat", function () {
     describe("A Groupchat", function () {
 
 
         it("escapes occupant nicknames when rendering them, to avoid JS-injection attacks",
         it("escapes occupant nicknames when rendering them, to avoid JS-injection attacks",
-                mock.initConverse(['rosterGroupsFetched'], {},
+                mock.initConverse(['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
@@ -303,7 +303,7 @@ describe("XSS", function () {
 
 
         it("escapes the subject before rendering it, to avoid JS-injection attacks",
         it("escapes the subject before rendering it, to avoid JS-injection attacks",
             mock.initConverse(
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterContactsFetched'], {},
                 async function (done, _converse) {
                 async function (done, _converse) {
 
 
             await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
             await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');

+ 1 - 1
src/components/message.js

@@ -12,7 +12,7 @@ import { CustomElement } from './element.js';
 import { __ } from '../i18n';
 import { __ } from '../i18n';
 import { _converse, api, converse } from  '@converse/headless/core';
 import { _converse, api, converse } from  '@converse/headless/core';
 import { html } from 'lit-element';
 import { html } from 'lit-element';
-import { renderAvatar } from './../templates/directives/avatar';
+import { renderAvatar } from 'templates/directives/avatar';
 
 
 const { Strophe } = converse.env;
 const { Strophe } = converse.env;
 const u = converse.env.utils;
 const u = converse.env.utils;

+ 6 - 14
src/headless/plugins/roster/contacts.js

@@ -2,6 +2,7 @@ import RosterContact from './contact.js';
 import log from "@converse/headless/log";
 import log from "@converse/headless/log";
 import sum from 'lodash/sum';
 import sum from 'lodash/sum';
 import { Collection } from "@converse/skeletor/src/collection";
 import { Collection } from "@converse/skeletor/src/collection";
+import { Model } from "@converse/skeletor/src/model";
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
 
 
@@ -12,19 +13,11 @@ const u = converse.env.utils;
 const RosterContacts = Collection.extend({
 const RosterContacts = Collection.extend({
     model: RosterContact,
     model: RosterContact,
 
 
-    comparator (contact1, contact2) {
-        // Groups are sorted alphabetically, ignoring case.
-        // However, Ungrouped, Requesting Contacts and Pending Contacts
-        // appear last and in that order.
-        const status1 = contact1.presence.get('show') || 'offline';
-        const status2 = contact2.presence.get('show') || 'offline';
-        if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) {
-            const name1 = (contact1.getDisplayName()).toLowerCase();
-            const name2 = (contact2.getDisplayName()).toLowerCase();
-            return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
-        } else  {
-            return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1;
-        }
+    initialize () {
+        const id = `roster.state-${_converse.bare_jid}-${this.get('jid')}`;
+        this.state = new Model({ id, 'collapsed_groups': [] });
+        this.state.browserStorage = _converse.createStore(id);
+        this.state.fetch();
     },
     },
 
 
     onConnected () {
     onConnected () {
@@ -302,7 +295,6 @@ const RosterContacts = Collection.extend({
      */
      */
     updateContact (item) {
     updateContact (item) {
         const jid = item.getAttribute('jid');
         const jid = item.getAttribute('jid');
-
         const contact = this.get(jid);
         const contact = this.get(jid);
         const subscription = item.getAttribute("subscription");
         const subscription = item.getAttribute("subscription");
         const ask = item.getAttribute("ask");
         const ask = item.getAttribute("ask");

+ 0 - 18
src/headless/plugins/roster/group.js

@@ -1,18 +0,0 @@
-import { Model } from '@converse/skeletor/src/model.js';
-import { __ } from 'i18n';
-import { _converse } from "@converse/headless/core";
-
-
-const RosterGroup = Model.extend({
-
-    initialize (attributes) {
-        this.set(Object.assign({
-            description: __('Click to hide these contacts'),
-            state: _converse.OPENED
-        }, attributes));
-        // Collection of contacts belonging to this group.
-        this.contacts = new _converse.RosterContacts();
-    }
-});
-
-export default RosterGroup;

+ 0 - 58
src/headless/plugins/roster/groups.js

@@ -1,58 +0,0 @@
-import RosterGroup from './group.js';
-import { Collection } from "@converse/skeletor/src/collection";
-import { _converse } from "@converse/headless/core";
-
-
-/**
- * @class
- */
-const RosterGroups = Collection.extend({
-    model: RosterGroup,
-
-    comparator (a, b) {
-        const HEADER_WEIGHTS = {};
-        HEADER_WEIGHTS[_converse.HEADER_UNREAD] = 0;
-        HEADER_WEIGHTS[_converse.HEADER_REQUESTING_CONTACTS] = 1;
-        HEADER_WEIGHTS[_converse.HEADER_CURRENT_CONTACTS]    = 2;
-        HEADER_WEIGHTS[_converse.HEADER_UNGROUPED]           = 3;
-        HEADER_WEIGHTS[_converse.HEADER_PENDING_CONTACTS]    = 4;
-
-        a = a.get('name');
-        b = b.get('name');
-        const WEIGHTS =  HEADER_WEIGHTS;
-        const special_groups = Object.keys(HEADER_WEIGHTS);
-        const a_is_special = special_groups.includes(a);
-        const b_is_special = special_groups.includes(b);
-        if (!a_is_special && !b_is_special ) {
-            return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
-        } else if (a_is_special && b_is_special) {
-            return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
-        } else if (!a_is_special && b_is_special) {
-            const a_header = _converse.HEADER_CURRENT_CONTACTS;
-            return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
-        } else if (a_is_special && !b_is_special) {
-            const b_header = _converse.HEADER_CURRENT_CONTACTS;
-            return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
-        }
-    },
-
-    /**
-     * Fetches all the roster groups from sessionStorage.
-     * @private
-     * @method _converse.RosterGroups#fetchRosterGroups
-     * @returns { Promise } - A promise which resolves once the groups have been fetched.
-     */
-    fetchRosterGroups () {
-        return new Promise(success => {
-            this.fetch({
-                success,
-                // We need to first have all groups before
-                // we can start positioning them, so we set
-                // 'silent' to true.
-                silent: true,
-            });
-        });
-    }
-});
-
-export default RosterGroups;

+ 0 - 15
src/headless/plugins/roster/index.js

@@ -6,8 +6,6 @@
 import "@converse/headless/plugins/status";
 import "@converse/headless/plugins/status";
 import RosterContact from './contact.js';
 import RosterContact from './contact.js';
 import RosterContacts from './contacts.js';
 import RosterContacts from './contacts.js';
-import RosterGroup from './group.js';
-import RosterGroups from './groups.js';
 import invoke from 'lodash/invoke';
 import invoke from 'lodash/invoke';
 import log from "@converse/headless/log";
 import log from "@converse/headless/log";
 import roster_api from './api.js';
 import roster_api from './api.js';
@@ -37,7 +35,6 @@ converse.plugins.add('converse-roster', {
             'cachedRoster',
             'cachedRoster',
             'roster',
             'roster',
             'rosterContactsFetched',
             'rosterContactsFetched',
-            'rosterGroupsFetched',
             'rosterInitialized',
             'rosterInitialized',
         ]);
         ]);
 
 
@@ -96,16 +93,6 @@ converse.plugins.add('converse-roster', {
                 _converse.send_initial_presence = true;
                 _converse.send_initial_presence = true;
             }
             }
             try {
             try {
-                await _converse.rostergroups.fetchRosterGroups();
-                /**
-                 * Triggered once roster groups have been fetched. Used by the
-                 * `converse-rosterview.js` plugin to know when it can start alphabetically
-                 * position roster groups.
-                 * @event _converse#rosterGroupsFetched
-                 * @example _converse.api.listen.on('rosterGroupsFetched', () => { ... });
-                 * @example _converse.api.waitUntil('rosterGroupsFetched').then(() => { ... });
-                 */
-                api.trigger('rosterGroupsFetched');
                 await _converse.roster.fetchRosterContacts();
                 await _converse.roster.fetchRosterContacts();
                 api.trigger('rosterContactsFetched');
                 api.trigger('rosterContactsFetched');
             } catch (reason) {
             } catch (reason) {
@@ -119,8 +106,6 @@ converse.plugins.add('converse-roster', {
         _converse.Presences = Presences;
         _converse.Presences = Presences;
         _converse.RosterContact = RosterContact;
         _converse.RosterContact = RosterContact;
         _converse.RosterContacts = RosterContacts;
         _converse.RosterContacts = RosterContacts;
-        _converse.RosterGroup = RosterGroup;
-        _converse.RosterGroups = RosterGroups;
 
 
         _converse.unregisterPresenceHandler = function () {
         _converse.unregisterPresenceHandler = function () {
             if (_converse.presence_ref !== undefined) {
             if (_converse.presence_ref !== undefined) {

+ 40 - 5
src/headless/plugins/roster/utils.js

@@ -15,12 +15,8 @@ export async function initRoster () {
     _converse.roster.data.id = id;
     _converse.roster.data.id = id;
     _converse.roster.data.browserStorage = _converse.createStore(id);
     _converse.roster.data.browserStorage = _converse.createStore(id);
     _converse.roster.data.fetch();
     _converse.roster.data.fetch();
-
-    id = `converse.roster.groups${_converse.bare_jid}`;
-    _converse.rostergroups = new _converse.RosterGroups();
-    _converse.rostergroups.browserStorage = _converse.createStore(id);
     /**
     /**
-     * Triggered once the `_converse.RosterContacts` and `_converse.RosterGroups` have
+     * Triggered once the `_converse.RosterContacts`
      * been created, but not yet populated with data.
      * been created, but not yet populated with data.
      * This event is useful when you want to create views for these collections.
      * This event is useful when you want to create views for these collections.
      * @event _converse#chatBoxMaximized
      * @event _converse#chatBoxMaximized
@@ -42,3 +38,42 @@ export function updateUnreadCounter (chatbox) {
 export async function clearPresences () {
 export async function clearPresences () {
     await _converse.presences?.clearStore();
     await _converse.presences?.clearStore();
 }
 }
+
+
+export function contactsComparator (contact1, contact2) {
+    const status1 = contact1.presence.get('show') || 'offline';
+    const status2 = contact2.presence.get('show') || 'offline';
+    if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) {
+        const name1 = (contact1.getDisplayName()).toLowerCase();
+        const name2 = (contact2.getDisplayName()).toLowerCase();
+        return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
+    } else  {
+        return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1;
+    }
+}
+
+
+export function groupsComparator (a, b) {
+    const HEADER_WEIGHTS = {};
+    HEADER_WEIGHTS[_converse.HEADER_UNREAD] = 0;
+    HEADER_WEIGHTS[_converse.HEADER_REQUESTING_CONTACTS] = 1;
+    HEADER_WEIGHTS[_converse.HEADER_CURRENT_CONTACTS]    = 2;
+    HEADER_WEIGHTS[_converse.HEADER_UNGROUPED]           = 3;
+    HEADER_WEIGHTS[_converse.HEADER_PENDING_CONTACTS]    = 4;
+
+    const WEIGHTS =  HEADER_WEIGHTS;
+    const special_groups = Object.keys(HEADER_WEIGHTS);
+    const a_is_special = special_groups.includes(a);
+    const b_is_special = special_groups.includes(b);
+    if (!a_is_special && !b_is_special ) {
+        return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
+    } else if (a_is_special && b_is_special) {
+        return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
+    } else if (!a_is_special && b_is_special) {
+        const a_header = _converse.HEADER_CURRENT_CONTACTS;
+        return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
+    } else if (a_is_special && !b_is_special) {
+        const b_header = _converse.HEADER_CURRENT_CONTACTS;
+        return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
+    }
+}

+ 1 - 1
src/modals/user-details.js

@@ -85,7 +85,7 @@ const UserDetailsModal = BootstrapModal.extend({
     },
     },
 
 
     removeContact (ev) {
     removeContact (ev) {
-        if (ev && ev.preventDefault) { ev.preventDefault(); }
+        ev?.preventDefault?.();
         if (!api.settings.get('allow_contact_removal')) { return; }
         if (!api.settings.get('allow_contact_removal')) { return; }
         const result = confirm(__("Are you sure you want to remove this contact?"));
         const result = confirm(__("Are you sure you want to remove this contact?"));
         if (result === true) {
         if (result === true) {

+ 5 - 1
src/plugins/controlbox/templates/controlbox.js

@@ -1,4 +1,5 @@
 import { html } from 'lit-html';
 import { html } from 'lit-html';
+import { _converse, api } from "@converse/headless/core";
 
 
 export default o => html`
 export default o => html`
     <div class="flyout box-flyout">
     <div class="flyout box-flyout">
@@ -19,7 +20,10 @@ export default o => html`
                           <div id="chatrooms" class="controlbox-section">
                           <div id="chatrooms" class="controlbox-section">
                               <converse-rooms-list></converse-rooms-list>
                               <converse-rooms-list></converse-rooms-list>
                               <converse-bookmarks></converse-bookmarks>
                               <converse-bookmarks></converse-bookmarks>
-                          </div>`
+                          </div>
+                          ${ api.settings.get("authentication") === _converse.ANONYMOUS ? '' :
+                            html`<div id="converse-roster" class="controlbox-section"><converse-roster></converse-roster></div>`
+                          }`
                     : o['active-form'] === 'register'
                     : o['active-form'] === 'register'
                         ? html`<converse-register-panel></converse-register-panel>`
                         ? html`<converse-register-panel></converse-register-panel>`
                         : html`<converse-login-panel></converse-login-panel>`
                         : html`<converse-login-panel></converse-login-panel>`

+ 3 - 5
src/plugins/muc-views/index.js

@@ -12,13 +12,13 @@ import MUCConfigForm from './config-form.js';
 import MUCPasswordForm from './password-form.js';
 import MUCPasswordForm from './password-form.js';
 import log from '@converse/headless/log';
 import log from '@converse/headless/log';
 import muc_api from './api.js';
 import muc_api from './api.js';
-import { RoomsPanel, RoomsPanelViewMixin } from './rooms-panel.js';
 import { api, converse, _converse } from '@converse/headless/core';
 import { api, converse, _converse } from '@converse/headless/core';
 
 
 const { Strophe } = converse.env;
 const { Strophe } = converse.env;
 
 
 function setMUCDomain (domain, controlboxview) {
 function setMUCDomain (domain, controlboxview) {
-    controlboxview.getRoomsPanel().model.save('muc_domain', Strophe.getDomainFromJid(domain));
+    controlboxview.querySelector('converse-rooms-list')
+        .model.save('muc_domain', Strophe.getDomainFromJid(domain));
 }
 }
 
 
 function setMUCDomainFromDisco (controlboxview) {
 function setMUCDomainFromDisco (controlboxview) {
@@ -50,7 +50,7 @@ function setMUCDomainFromDisco (controlboxview) {
 
 
 function fetchAndSetMUCDomain (controlboxview) {
 function fetchAndSetMUCDomain (controlboxview) {
     if (controlboxview.model.get('connected')) {
     if (controlboxview.model.get('connected')) {
-        if (!controlboxview.getRoomsPanel().model.get('muc_domain')) {
+        if (!controlboxview.querySelector('converse-rooms-list').model.get('muc_domain')) {
             if (api.settings.get('muc_domain') === undefined) {
             if (api.settings.get('muc_domain') === undefined) {
                 setMUCDomainFromDisco(controlboxview);
                 setMUCDomainFromDisco(controlboxview);
             } else {
             } else {
@@ -120,8 +120,6 @@ converse.plugins.add('converse-muc-views', {
         _converse.MUCConfigForm = MUCConfigForm;
         _converse.MUCConfigForm = MUCConfigForm;
         _converse.MUCPasswordForm = MUCPasswordForm;
         _converse.MUCPasswordForm = MUCPasswordForm;
         _converse.ChatRoomView = MUCView;
         _converse.ChatRoomView = MUCView;
-        _converse.RoomsPanel = RoomsPanel;
-        _converse.ControlBoxView && Object.assign(_converse.ControlBoxView.prototype, RoomsPanelViewMixin);
 
 
         Object.assign(_converse.api, muc_api);
         Object.assign(_converse.api, muc_api);
 
 

+ 0 - 17
src/plugins/muc-views/rooms-panel.js

@@ -1,17 +0,0 @@
-import { converse } from '@converse/headless/core';
-
-const u = converse.env.utils;
-
-/**
- * Mixin which adds the ability to a ControlBox to render a list of open groupchats
- * @mixin
- */
-export const RoomsPanelViewMixin = {
-    getRoomsPanel () {
-        if (this.roomspanel && u.isInDOM(this.roomspanel.el)) {
-            return this.roomspanel;
-        } else {
-            return this.renderRoomsPanel();
-        }
-    }
-};

+ 72 - 133
src/plugins/rosterview/contactview.js

@@ -2,79 +2,33 @@ import log from "@converse/headless/log";
 import tpl_pending_contact from "./templates/pending_contact.js";
 import tpl_pending_contact from "./templates/pending_contact.js";
 import tpl_requesting_contact from "./templates/requesting_contact.js";
 import tpl_requesting_contact from "./templates/requesting_contact.js";
 import tpl_roster_item from "./templates/roster_item.js";
 import tpl_roster_item from "./templates/roster_item.js";
-import { ViewWithAvatar } from 'shared/avatar.js';
+import { CustomElement } from 'components/element.js';
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
-import { debounce, without } from "lodash-es";
-import { render } from 'lit-html';
 
 
 const u = converse.env.utils;
 const u = converse.env.utils;
 
 
-const STATUSES = {
-    'dnd': __('This contact is busy'),
-    'online': __('This contact is online'),
-    'offline': __('This contact is offline'),
-    'unavailable': __('This contact is unavailable'),
-    'xa': __('This contact is away for an extended period'),
-    'away': __('This contact is away')
-};
-
-
-const RosterContactView = ViewWithAvatar.extend({
-    tagName: 'li',
-    className: 'list-item d-flex hidden controlbox-padded',
-
-    events: {
-        "click .accept-xmpp-request": "acceptRequest",
-        "click .decline-xmpp-request": "declineRequest",
-        "click .open-chat": "openChat",
-        "click .remove-xmpp-contact": "removeContact"
-    },
-
-    async initialize () {
-        await this.model.initialized;
-        this.debouncedRender = debounce(this.render, 50);
-        this.listenTo(this.model, "change", this.debouncedRender);
-        this.listenTo(this.model, "destroy", this.remove);
-        this.listenTo(this.model, "highlight", this.highlight);
-        this.listenTo(this.model, "remove", this.remove);
-        this.listenTo(this.model, 'vcard:change', this.debouncedRender);
-        this.listenTo(this.model.presence, "change:show", this.debouncedRender);
-        this.render();
-    },
 
 
-    render () {
-        if (!this.mayBeShown()) {
-            u.hideElement(this.el);
-            return this;
-        }
-        const ask = this.model.get('ask'),
-            show = this.model.presence.get('show'),
-            requesting  = this.model.get('requesting'),
-            subscription = this.model.get('subscription'),
-            jid = this.model.get('jid');
-
-        const classes_to_remove = [
-            'current-xmpp-contact',
-            'pending-xmpp-contact',
-            'requesting-xmpp-contact'
-            ].concat(Object.keys(STATUSES));
-        classes_to_remove.forEach(c => u.removeClass(c, this.el));
-
-        this.el.classList.add(show);
-        this.el.setAttribute('data-status', show);
-        this.highlight();
-
-        if (_converse.isUniView()) {
-            const chatbox = _converse.chatboxes.get(this.model.get('jid'));
-            if (chatbox) {
-                if (chatbox.get('hidden')) {
-                    this.el.classList.remove('open');
-                } else {
-                    this.el.classList.add('open');
-                }
-            }
+class RosterContact extends CustomElement {
+
+    static get properties () {
+        return {
+            model: { type: Object }
         }
         }
+    }
+
+    connectedCallback () {
+        super.connectedCallback();
+        this.listenTo(this.model, "change", this.requestUpdate);
+        this.listenTo(this.model, "highlight", this.requestUpdate);
+        this.listenTo(this.model, 'vcard:change', this.requestUpdate);
+    }
+
+    render () {
+        const ask = this.model.get('ask');
+        const requesting  = this.model.get('requesting');
+        const subscription = this.model.get('subscription');
+        const jid = this.model.get('jid');
 
 
         if ((ask === 'subscribe') || (subscription === 'from')) {
         if ((ask === 'subscribe') || (subscription === 'from')) {
             /* ask === 'subscribe'
             /* ask === 'subscribe'
@@ -89,46 +43,41 @@ const RosterContactView = ViewWithAvatar.extend({
              *  So in both cases the user is a "pending" contact.
              *  So in both cases the user is a "pending" contact.
              */
              */
             const display_name = this.model.getDisplayName();
             const display_name = this.model.getDisplayName();
-            this.el.classList.add('pending-xmpp-contact');
-            render(tpl_pending_contact(Object.assign(this.model.toJSON(), { display_name })), this.el);
+            return tpl_pending_contact(Object.assign(
+                this.model.toJSON(), {
+                    display_name,
+                    'openChat': ev => this.openChat(ev),
+                    'removeContact':  ev => this.removeContact(ev)
+                }));
 
 
         } else if (requesting === true) {
         } else if (requesting === true) {
             const display_name = this.model.getDisplayName();
             const display_name = this.model.getDisplayName();
-            this.el.classList.add('requesting-xmpp-contact');
-            render(tpl_requesting_contact(
+            return tpl_requesting_contact(
                 Object.assign(this.model.toJSON(), {
                 Object.assign(this.model.toJSON(), {
                     display_name,
                     display_name,
+                    'openChat': ev => this.openChat(ev),
+                    'acceptRequest': ev => this.acceptRequest(ev),
+                    'declineRequest': ev => this.declineRequest(ev),
                     'desc_accept': __("Click to accept the contact request from %1$s", display_name),
                     'desc_accept': __("Click to accept the contact request from %1$s", display_name),
                     'desc_decline': __("Click to decline the contact request from %1$s", display_name),
                     'desc_decline': __("Click to decline the contact request from %1$s", display_name),
                     'allow_chat_pending_contacts': api.settings.get('allow_chat_pending_contacts')
                     'allow_chat_pending_contacts': api.settings.get('allow_chat_pending_contacts')
                 })
                 })
-            ), this.el);
-        } else if (subscription === 'both' || subscription === 'to' || _converse.rosterview.isSelf(jid)) {
-            this.el.classList.add('current-xmpp-contact');
-            this.el.classList.remove(without(['both', 'to'], subscription)[0]);
-            this.el.classList.add(subscription);
-            this.renderRosterItem(this.model);
-        }
-        return this;
-    },
-
-    /**
-     * If appropriate, highlight the contact (by adding the 'open' class).
-     * @private
-     * @method _converse.RosterContactView#highlight
-     */
-    highlight () {
-        if (_converse.isUniView()) {
-            const chatbox = _converse.chatboxes.get(this.model.get('jid'));
-            if ((chatbox && chatbox.get('hidden')) || !chatbox) {
-                this.el.classList.remove('open');
-            } else {
-                this.el.classList.add('open');
-            }
+            );
+        } else if (subscription === 'both' || subscription === 'to' || u.isSameBareJID(jid, _converse.connection.jid)) {
+            return this.renderRosterItem(this.model);
         }
         }
-    },
+    }
+
+    renderRosterItem (item) { // eslint-disable-line class-methods-use-this
+        const STATUSES = {
+            'dnd': __('This contact is busy'),
+            'online': __('This contact is online'),
+            'offline': __('This contact is offline'),
+            'unavailable': __('This contact is unavailable'),
+            'xa': __('This contact is away for an extended period'),
+            'away': __('This contact is away')
+        };
 
 
-    renderRosterItem (item) {
         const show = item.presence.get('show') || 'offline';
         const show = item.presence.get('show') || 'offline';
         let status_icon;
         let status_icon;
         if (show === 'online') {
         if (show === 'online') {
@@ -143,55 +92,45 @@ const RosterContactView = ViewWithAvatar.extend({
             status_icon = 'fa fa-times-circle chat-status chat-status--offline';
             status_icon = 'fa fa-times-circle chat-status chat-status--offline';
         }
         }
         const display_name = item.getDisplayName();
         const display_name = item.getDisplayName();
-        render(tpl_roster_item(
+        return tpl_roster_item(
             Object.assign(item.toJSON(), {
             Object.assign(item.toJSON(), {
                 show,
                 show,
                 display_name,
                 display_name,
                 status_icon,
                 status_icon,
+                'openChat': ev => this.openChat(ev),
+                'removeContact':  ev => this.removeContact(ev),
+                'getAvatarData': () => this.getAvatarData(),
                 'desc_status': STATUSES[show],
                 'desc_status': STATUSES[show],
                 'num_unread': item.get('num_unread') || 0,
                 'num_unread': item.get('num_unread') || 0,
                 classes: ''
                 classes: ''
             })
             })
-        ), this.el);
-        this.renderAvatar();
-        return this;
-    },
-
-    /**
-     * Returns a boolean indicating whether this contact should
-     * generally be visible in the roster.
-     * It doesn't check for the more specific case of whether
-     * the group it's in is collapsed.
-     * @private
-     * @method _converse.RosterContactView#mayBeShown
-     */
-    mayBeShown () {
-        const chatStatus = this.model.presence.get('show');
-        if (api.settings.get('hide_offline_users') && chatStatus === 'offline') {
-            // If pending or requesting, show
-            if ((this.model.get('ask') === 'subscribe') ||
-                    (this.model.get('subscription') === 'from') ||
-                    (this.model.get('requesting') === true)) {
-                return true;
-            }
-            return false;
-        }
-        return true;
-    },
+        );
+    }
+
+    getAvatarData () {
+        const image_type = this.model.vcard?.get('image_type') || _converse.DEFAULT_IMAGE_TYPE;
+        const image_data = this.model.vcard?.get('image') || _converse.DEFAULT_IMAGE;
+        const image = "data:" + image_type + ";base64," + image_data;
+        return {
+            'classes': 'avatar',
+            'height': 30,
+            'width': 30,
+            image,
+        };
+    }
 
 
     openChat (ev) {
     openChat (ev) {
-        if (ev && ev.preventDefault) { ev.preventDefault(); }
+        ev?.preventDefault?.();
         this.model.openChat();
         this.model.openChat();
-    },
+    }
 
 
-    async removeContact (ev) {
-        if (ev && ev.preventDefault) { ev.preventDefault(); }
+    removeContact (ev) {
+        ev?.preventDefault?.();
         if (!api.settings.get('allow_contact_removal')) { return; }
         if (!api.settings.get('allow_contact_removal')) { return; }
         if (!confirm(__("Are you sure you want to remove this contact?"))) { return; }
         if (!confirm(__("Are you sure you want to remove this contact?"))) { return; }
 
 
         try {
         try {
-            await this.model.removeFromRoster();
-            this.remove();
+            this.model.removeFromRoster();
             if (this.model.collection) {
             if (this.model.collection) {
                 // The model might have already been removed as
                 // The model might have already been removed as
                 // result of a roster push.
                 // result of a roster push.
@@ -203,10 +142,10 @@ const RosterContactView = ViewWithAvatar.extend({
                 [__('Sorry, there was an error while trying to remove %1$s as a contact.', this.model.getDisplayName())]
                 [__('Sorry, there was an error while trying to remove %1$s as a contact.', this.model.getDisplayName())]
             );
             );
         }
         }
-    },
+    }
 
 
     async acceptRequest (ev) {
     async acceptRequest (ev) {
-        if (ev && ev.preventDefault) { ev.preventDefault(); }
+        ev?.preventDefault?.();
 
 
         await _converse.roster.sendContactAddIQ(
         await _converse.roster.sendContactAddIQ(
             this.model.get('jid'),
             this.model.get('jid'),
@@ -214,7 +153,7 @@ const RosterContactView = ViewWithAvatar.extend({
             []
             []
         );
         );
         this.model.authorize().subscribe();
         this.model.authorize().subscribe();
-    },
+    }
 
 
     declineRequest (ev) {
     declineRequest (ev) {
         if (ev && ev.preventDefault) { ev.preventDefault(); }
         if (ev && ev.preventDefault) { ev.preventDefault(); }
@@ -224,6 +163,6 @@ const RosterContactView = ViewWithAvatar.extend({
         }
         }
         return this;
         return this;
     }
     }
-});
+}
 
 
-export default RosterContactView;
+api.elements.define('converse-roster-contact', RosterContact);

+ 1 - 1
src/plugins/rosterview/filterview.js

@@ -23,6 +23,7 @@ export class RosterFilterView extends ElementView {
         model.id = `_converse.rosterfilter-${_converse.bare_jid}`;
         model.id = `_converse.rosterfilter-${_converse.bare_jid}`;
         model.browserStorage = _converse.createStore(model.id);
         model.browserStorage = _converse.createStore(model.id);
         this.model = model;
         this.model = model;
+        _converse.roster_filter = model;
 
 
         this.liveFilter = debounce(() => {
         this.liveFilter = debounce(() => {
             this.model.save({'filter_text': this.querySelector('.roster-filter').value});
             this.model.save({'filter_text': this.querySelector('.roster-filter').value});
@@ -39,7 +40,6 @@ export class RosterFilterView extends ElementView {
         this.listenTo(_converse.roster, "destroy", this.render);
         this.listenTo(_converse.roster, "destroy", this.render);
         this.listenTo(_converse.roster, "remove", this.render);
         this.listenTo(_converse.roster, "remove", this.render);
         _converse.presences.on('change:show', this.render, this);
         _converse.presences.on('change:show', this.render, this);
-        api.listen.on('rosterContactsFetchedAndProcessed', () => this.render());
 
 
         this.model.fetch();
         this.model.fetch();
         this.render();
         this.render();

+ 0 - 206
src/plugins/rosterview/groupview.js

@@ -1,206 +0,0 @@
-import RosterContactView from './contactview.js';
-import tpl_group_header from "./templates/group_header.js";
-import { OrderedListView } from "@converse/skeletor/src/overview";
-import { _converse, api, converse } from "@converse/headless/core";
-import { render } from 'lit-html';
-
-const u = converse.env.utils;
-
-/**
- * @class
- * @namespace _converse.RosterGroupView
- * @memberOf _converse
- */
-const RosterGroupView = OrderedListView.extend({
-    tagName: 'div',
-    className: 'roster-group hidden',
-    events: {
-        "click a.group-toggle": "toggle"
-    },
-
-    sortImmediatelyOnAdd: true,
-    ItemView: RosterContactView,
-    listItems: 'model.contacts',
-    listSelector: '.roster-group-contacts',
-    sortEvent: 'presenceChanged',
-
-    initialize () {
-        OrderedListView.prototype.initialize.apply(this, arguments);
-
-        if (this.model.get('name') === _converse.HEADER_UNREAD) {
-            this.listenTo(this.model.contacts, "change:num_unread",
-                c => !this.model.get('unread_messages') && this.removeContact(c)
-            );
-        }
-        if (this.model.get('name') === _converse.HEADER_REQUESTING_CONTACTS) {
-            this.listenTo(this.model.contacts, "change:requesting",
-                c => !c.get('requesting') && this.removeContact(c)
-            );
-        }
-        if (this.model.get('name') === _converse.HEADER_PENDING_CONTACTS) {
-            this.listenTo(this.model.contacts, "change:subscription",
-                c => (c.get('subscription') !== 'from') && this.removeContact(c)
-            );
-        }
-
-        this.listenTo(this.model.contacts, "remove", this.onRemove);
-        this.listenTo(_converse.roster, 'change:groups', this.onContactGroupChange);
-
-        // This event gets triggered once *all* contacts (i.e. not
-        // just this group's) have been fetched from browser
-        // storage or the XMPP server and once they've been
-        // assigned to their various groups.
-        api.listen.on('rosterContactsFetchedAndProcessed', () => this.sortAndPositionAllItems());
-    },
-
-    render () {
-        this.el.setAttribute('data-group', this.model.get('name'));
-        render(tpl_group_header({
-            'label_group': this.model.get('name'),
-            'desc_group_toggle': this.model.get('description'),
-            'toggle_state': this.model.get('state')
-        }), this.el);
-        this.contacts_el = this.el.querySelector('.roster-group-contacts');
-        return this;
-    },
-
-    show () {
-        u.showElement(this.el);
-        if (this.model.get('state') === _converse.OPENED) {
-            Object.values(this.getAll())
-                .filter(v => v.mayBeShown())
-                .forEach(v => u.showElement(v.el));
-        }
-        return this;
-    },
-
-    collapse () {
-        return u.slideIn(this.contacts_el);
-    },
-
-    /* Given a list of contacts, make sure they're filtered out
-     * (aka hidden) and that all other contacts are visible.
-     * If all contacts are hidden, then also hide the group title.
-     * @private
-     * @method _converse.RosterGroupView#filterOutContacts
-     * @param { Array } contacts
-     */
-    filterOutContacts (contacts=[]) {
-        let shown = 0;
-        this.model.contacts.forEach(contact => {
-            const contact_view = this.get(contact.get('id'));
-            if (contacts.includes(contact)) {
-                u.hideElement(contact_view.el);
-            } else if (contact_view.mayBeShown()) {
-                u.showElement(contact_view.el);
-                shown += 1;
-            }
-        });
-        if (shown) {
-            u.showElement(this.el);
-        } else {
-            u.hideElement(this.el);
-        }
-    },
-
-    /**
-     * Given the filter query "q" and the filter type "type",
-     * return a list of contacts that need to be filtered out.
-     * @private
-     * @method _converse.RosterGroupView#getFilterMatches
-     * @param { String } q - The filter query
-     * @param { String } type - The filter type
-     */
-    getFilterMatches (q, type) {
-        if (q.length === 0) {
-            return [];
-        }
-        q = q.toLowerCase();
-        const contacts = this.model.contacts;
-        if (type === 'state') {
-            const sticky_groups = [_converse.HEADER_REQUESTING_CONTACTS, _converse.HEADER_UNREAD];
-            if (sticky_groups.includes(this.model.get('name'))) {
-                // When filtering by chat state, we still want to
-                // show sticky groups, even though they don't
-                // match the state in question.
-                return [];
-            } else if (q === 'unread_messages') {
-                return contacts.filter({'num_unread': 0});
-            } else if (q === 'online') {
-                return contacts.filter(c => ["offline", "unavailable"].includes(c.presence.get('show')));
-            } else {
-                return contacts.filter(c => !c.presence.get('show').includes(q));
-            }
-        } else  {
-            return contacts.filter(c => !c.getFilterCriteria().includes(q));
-        }
-    },
-
-    /**
-     * Filter the group's contacts based on the query "q".
-     *
-     * If all contacts are filtered out (i.e. hidden), then the
-     * group must be filtered out as well.
-     * @private
-     * @method _converse.RosterGroupView#filter
-     * @param { string } q - The query to filter against
-     * @param { string } type
-     */
-    filter (q, type) {
-        if (q === null || q === undefined) {
-            type = type || _converse.rosterview.filter_view.model.get('filter_type');
-            if (type === 'state') {
-                q = _converse.rosterview.filter_view.model.get('chat_state');
-            } else {
-                q = _converse.rosterview.filter_view.model.get('filter_text');
-            }
-        }
-        this.filterOutContacts(this.getFilterMatches(q, type));
-    },
-
-    async toggle (ev) {
-        if (ev && ev.preventDefault) { ev.preventDefault(); }
-        const icon_el = ev.target.matches('.fa') ? ev.target : ev.target.querySelector('.fa');
-        if (u.hasClass("fa-caret-down", icon_el)) {
-            this.model.save({state: _converse.CLOSED});
-            await this.collapse();
-            icon_el.classList.remove("fa-caret-down");
-            icon_el.classList.add("fa-caret-right");
-        } else {
-            icon_el.classList.remove("fa-caret-right");
-            icon_el.classList.add("fa-caret-down");
-            this.model.save({state: _converse.OPENED});
-            this.filter();
-            u.showElement(this.el);
-            u.slideOut(this.contacts_el);
-        }
-    },
-
-    onContactGroupChange (contact) {
-        const in_this_group = contact.get('groups').includes(this.model.get('name'));
-        const cid = contact.get('id');
-        const in_this_overview = !this.get(cid);
-        if (in_this_group && !in_this_overview) {
-            this.items.trigger('add', contact);
-        } else if (!in_this_group) {
-            this.removeContact(contact);
-        }
-    },
-
-    removeContact (contact) {
-        // We suppress events, otherwise the remove event will
-        // also cause the contact's view to be removed from the
-        // "Pending Contacts" group.
-        this.model.contacts.remove(contact, {'silent': true});
-        this.onRemove(contact);
-    },
-
-    onRemove (contact) {
-        this.remove(contact.get('jid'));
-        if (this.model.contacts.length === 0) {
-            this.remove();
-        }
-    }
-});
-
-export default RosterGroupView;

+ 3 - 21
src/plugins/rosterview/index.js

@@ -7,12 +7,11 @@ import "../modal";
 import "@converse/headless/plugins/chatboxes";
 import "@converse/headless/plugins/chatboxes";
 import "@converse/headless/plugins/roster/index.js";
 import "@converse/headless/plugins/roster/index.js";
 import "modals/add-contact.js";
 import "modals/add-contact.js";
+import './rosterview.js';
 import RosterContactView from './contactview.js';
 import RosterContactView from './contactview.js';
-import RosterGroupView from './groupview.js';
-import RosterView from './rosterview.js';
-import { initRosterView, highlightRosterItem, insertRoster } from './utils.js';
 import { RosterFilter, RosterFilterView } from './filterview.js';
 import { RosterFilter, RosterFilterView } from './filterview.js';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
+import { highlightRosterItem } from './utils.js';
 
 
 
 
 converse.plugins.add('converse-rosterview', {
 converse.plugins.add('converse-rosterview', {
@@ -33,8 +32,6 @@ converse.plugins.add('converse-rosterview', {
         _converse.RosterFilter = RosterFilter;
         _converse.RosterFilter = RosterFilter;
         _converse.RosterFilterView = RosterFilterView;
         _converse.RosterFilterView = RosterFilterView;
         _converse.RosterContactView = RosterContactView;
         _converse.RosterContactView = RosterContactView;
-        _converse.RosterGroupView = RosterGroupView;
-        _converse.RosterView = RosterView;
 
 
         /* -------- Event Handlers ----------- */
         /* -------- Event Handlers ----------- */
         api.listen.on('chatBoxesInitialized', () => {
         api.listen.on('chatBoxesInitialized', () => {
@@ -42,21 +39,6 @@ converse.plugins.add('converse-rosterview', {
             _converse.chatboxes.on('change:hidden', chatbox => highlightRosterItem(chatbox));
             _converse.chatboxes.on('change:hidden', chatbox => highlightRosterItem(chatbox));
         });
         });
 
 
-        api.listen.on('controlBoxInitialized', (view) => {
-            insertRoster(view);
-            view.model.on('change:connected', () => insertRoster(view));
-        });
-
-        api.listen.on('rosterInitialized', initRosterView);
-        api.listen.on('rosterReadyAfterReconnection', initRosterView);
-
-        api.listen.on('afterTearDown', () => {
-            if (converse.rosterview) {
-                converse.rosterview.model.off().reset();
-                converse.rosterview.each(groupview => groupview.removeAll().remove());
-                converse.rosterview.removeAll().remove();
-                delete converse.rosterview;
-            }
-        });
+        api.listen.on('afterTearDown', () => _converse.rotergroups?.off().reset());
     }
     }
 });
 });

+ 42 - 197
src/plugins/rosterview/rosterview.js

@@ -1,11 +1,8 @@
-import RosterGroupView from './groupview.js';
-import log from "@converse/headless/log";
+import debounce from 'lodash/debounce';
 import tpl_roster from "./templates/roster.js";
 import tpl_roster from "./templates/roster.js";
+import { ElementView } from "@converse/skeletor/src/element";
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
-import { OrderedListView } from "@converse/skeletor/src/overview";
-import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
-import { debounce, has } from "lodash-es";
 import { render } from 'lit-html';
 import { render } from 'lit-html';
 
 
 const u = converse.env.utils;
 const u = converse.env.utils;
@@ -16,217 +13,65 @@ const u = converse.env.utils;
  * @namespace _converse.RosterView
  * @namespace _converse.RosterView
  * @memberOf _converse
  * @memberOf _converse
  */
  */
-const RosterView = OrderedListView.extend({
-    tagName: 'div',
-    id: 'converse-roster',
-    className: 'controlbox-section',
-
-    ItemView: RosterGroupView,
-    listItems: 'model',
-    listSelector: '.roster-contacts',
-    sortEvent: null, // Groups are immutable, so they don't get re-sorted
-    subviewIndex: 'name',
-    sortImmediatelyOnAdd: true,
-
-    events: {
+export default class RosterView extends ElementView {
+    events = {
         'click a.controlbox-heading__btn.add-contact': 'showAddContactModal',
         'click a.controlbox-heading__btn.add-contact': 'showAddContactModal',
         'click a.controlbox-heading__btn.sync-contacts': 'syncContacts'
         'click a.controlbox-heading__btn.sync-contacts': 'syncContacts'
-    },
-
-    initialize () {
-        OrderedListView.prototype.initialize.apply(this, arguments);
-
-        this.listenTo(_converse.roster, "add", this.onContactAdded);
-        this.listenTo(_converse.roster, 'change:groups', this.onContactAdded);
-        this.listenTo(_converse.roster, 'change', this.onContactChange);
-        this.listenTo(_converse.roster, "destroy", this.update);
-        this.listenTo(_converse.roster, "remove", this.update);
-        _converse.presences.on('change:show', () => {
-            this.update();
-            this.updateFilter();
-        });
+    }
 
 
-        this.listenTo(this.model, "reset", this.reset);
+    async initialize () {
+        await api.waitUntil('rosterInitialized')
+        this.debouncedRender = debounce(this.render, 100);
+        this.listenTo(_converse.roster, "add", this.debouncedRender);
+        this.listenTo(_converse.roster, "destroy", this.debouncedRender);
+        this.listenTo(_converse.roster, "remove", this.debouncedRender);
+        this.listenTo(_converse.roster, 'change', this.renderIfRelevantChange);
 
 
-        // This event gets triggered once *all* contacts (i.e. not
-        // just this group's) have been fetched from browser
-        // storage or the XMPP server and once they've been
-        // assigned to their various groups.
-        api.listen.on('rosterGroupsFetched', this.sortAndPositionAllItems.bind(this));
+        // FIXME Need to find a fix for this on the contact.presence
+        // this.listenTo(this.model.presence, "change:show", this.requestUpdate);
 
 
-        api.listen.on('rosterContactsFetched', () => {
-            _converse.roster.each(contact => this.addRosterContact(contact, {'silent': true}));
-            this.update();
-            this.updateFilter();
-            api.trigger('rosterContactsFetchedAndProcessed');
-        });
+        this.listenTo(_converse.roster.state, "change", this.render);
+        _converse.presences.on('change:show', () => this.debouncedRender());
+        api.listen.on('rosterContactsFetched', () => this.render());
         this.render();
         this.render();
         this.listenToRosterFilter();
         this.listenToRosterFilter();
-    },
+        /**
+         * Triggered once the _converse.RosterView instance has been created and initialized.
+         * @event _converse#rosterViewInitialized
+         * @example _converse.api.listen.on('rosterViewInitialized', () => { ... });
+         */
+        api.trigger('rosterViewInitialized');
+    }
 
 
     render () {
     render () {
-        render(tpl_roster({
-            'heading_contacts': __('Contacts'),
-            'title_add_contact': __('Add a contact'),
-            'title_sync_contacts': __('Re-sync your contacts')
-        }), this.el);
-        this.roster_el = this.el.querySelector('.roster-contacts');
-        return this;
-    },
-
-    showAddContactModal (ev) {
-        api.modal.show(_converse.AddContactModal, {'model': new Model()}, ev);
-    },
-
-    listenToRosterFilter () {
-        this.filter_view = this.el.querySelector('converse-roster-filter');
-        this.filter_view.addEventListener('update', () => this.updateFilter());
-    },
+        render(tpl_roster(), this);
+    }
 
 
-    /**
-     * Called whenever the filter settings have been changed or
-     * when contacts have been added, removed or changed.
-     *
-     * Debounced for 100ms so that it doesn't get called for every
-     * contact fetched from browser storage.
-     */
-    updateFilter: debounce(function () {
-        const filter = new _converse.RosterFilter();
-        const type = filter.get('filter_type');
-        if (type === 'state') {
-            this.filter(filter.get('chat_state'), type);
-        } else {
-            this.filter(filter.get('filter_text'), type);
+    renderIfRelevantChange (model) {
+        const attrs = ['ask', 'requesting', 'groups', 'num_unread'];
+        const changed = model.changed || {};
+        if (Object.keys(changed).filter(m => attrs.includes(m)).length) {
+            this.render();
         }
         }
-    }, 100),
+    }
 
 
-    update () {
-        if (!u.isVisible(this.roster_el)) {
-            u.showElement(this.roster_el);
-        }
-        return this;
-    },
+    listenToRosterFilter () {
+        this.filter_view = this.querySelector('converse-roster-filter');
+        this.filter_view.addEventListener('update', () => this.render());
+    }
 
 
-    filter (query, type) {
-        const views = Object.values(this.getAll());
-        // First ensure the filter is restored to its original state
-        views.forEach(v => (v.model.contacts.length > 0) && v.show().filter(''));
-        // Now we can filter
-        query = query.toLowerCase();
-        if (type === 'groups') {
-            views.forEach(view => {
-                if (!view.model.get('name').toLowerCase().includes(query)) {
-                    u.slideIn(view.el);
-                } else if (view.model.contacts.length > 0) {
-                    u.slideOut(view.el);
-                }
-            });
-        } else {
-            views.forEach(v => v.filter(query, type));
-        }
-    },
+    showAddContactModal (ev) { // eslint-disable-line class-methods-use-this
+        api.modal.show(_converse.AddContactModal, {'model': new Model()}, ev);
+    }
 
 
-    async syncContacts (ev) {
+    async syncContacts (ev) { // eslint-disable-line class-methods-use-this
         ev.preventDefault();
         ev.preventDefault();
         u.addClass('fa-spin', ev.target);
         u.addClass('fa-spin', ev.target);
         _converse.roster.data.save('version', null);
         _converse.roster.data.save('version', null);
         await _converse.roster.fetchFromServer();
         await _converse.roster.fetchFromServer();
         api.user.presence.send();
         api.user.presence.send();
         u.removeClass('fa-spin', ev.target);
         u.removeClass('fa-spin', ev.target);
-    },
-
-    reset () {
-        this.removeAll();
-        this.render().update();
-        return this;
-    },
-
-    onContactAdded (contact) {
-        this.addRosterContact(contact)
-        this.update();
-        this.updateFilter();
-    },
-
-    onContactChange (contact) {
-        this.update();
-        if (has(contact.changed, 'subscription')) {
-            if (contact.changed.subscription === 'from') {
-                this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
-            } else if (['both', 'to'].includes(contact.get('subscription'))) {
-                this.addExistingContact(contact);
-            }
-        }
-        if (has(contact.changed, 'num_unread') && contact.get('num_unread')) {
-            this.addContactToGroup(contact, _converse.HEADER_UNREAD);
-        }
-        if (has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') {
-            this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
-        }
-        if (has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
-            this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS);
-        }
-        this.updateFilter();
-    },
-
-    /**
-     * Returns the group as specified by name.
-     * Creates the group if it doesn't exist.
-     * @method _converse.RosterView#getGroup
-     * @private
-     * @param {string} name
-     */
-    getGroup (name) {
-        const view =  this.get(name);
-        if (view) {
-            return view.model;
-        }
-        return this.model.create({name});
-    },
-
-    addContactToGroup (contact, name, options) {
-        this.getGroup(name).contacts.add(contact, options);
-        this.sortAndPositionAllItems();
-    },
-
-    addExistingContact (contact, options) {
-        let groups;
-        if (api.settings.get('roster_groups')) {
-            groups = contact.get('groups');
-            groups = (groups.length === 0) ? [_converse.HEADER_UNGROUPED] : groups;
-        } else {
-            groups = [_converse.HEADER_CURRENT_CONTACTS];
-        }
-        if (contact.get('num_unread')) {
-            groups.push(_converse.HEADER_UNREAD);
-        }
-        groups.forEach(g => this.addContactToGroup(contact, g, options));
-    },
-
-    isSelf (jid) {
-        return u.isSameBareJID(jid, _converse.connection.jid);
-    },
-
-    addRosterContact (contact, options) {
-        const jid = contact.get('jid');
-        if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to' || this.isSelf(jid)) {
-            this.addExistingContact(contact, options);
-        } else {
-            if (!api.settings.get('allow_contact_requests')) {
-                log.debug(
-                    `Not adding requesting or pending contact ${jid} `+
-                    `because allow_contact_requests is false`
-                );
-                return;
-            }
-            if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
-                this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS, options);
-            } else if (contact.get('requesting') === true) {
-                this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS, options);
-            }
-        }
-        return this;
     }
     }
-});
-
+}
 
 
-export default RosterView;
+api.elements.define('converse-roster', RosterView);

+ 60 - 0
src/plugins/rosterview/templates/group.js

@@ -0,0 +1,60 @@
+import { __ } from 'i18n';
+import { _converse, converse } from "@converse/headless/core";
+import { html } from "lit-html";
+import { toggleGroup } from '../utils.js';
+
+const { u } = converse.env;
+
+
+function renderContact (contact) {
+    const jid = contact.get('jid');
+    const extra_classes = [];
+    if (_converse.isUniView()) {
+        const chatbox = _converse.chatboxes.get(jid);
+        if (chatbox && !chatbox.get('hidden')) {
+            extra_classes.push('open');
+        }
+    }
+    const ask = contact.get('ask');
+    const requesting  = contact.get('requesting');
+    const subscription = contact.get('subscription');
+    if ((ask === 'subscribe') || (subscription === 'from')) {
+        /* ask === 'subscribe'
+         *      Means we have asked to subscribe to them.
+         *
+         * subscription === 'from'
+         *      They are subscribed to us, but not vice versa.
+         *      We assume that there is a pending subscription
+         *      from us to them (otherwise we're in a state not
+         *      supported by converse.js).
+         *
+         *  So in both cases the user is a "pending" contact.
+         */
+        extra_classes.push('pending-xmpp-contact');
+    } else if (requesting === true) {
+        extra_classes.push('requesting-xmpp-contact');
+    } else if (subscription === 'both' || subscription === 'to' || u.isSameBareJID(jid, _converse.connection.jid)) {
+        extra_classes.push('current-xmpp-contact');
+        extra_classes.push(subscription);
+        extra_classes.push(contact.presence.get('show'));
+    }
+    return html`
+        <li class="list-item d-flex controlbox-padded ${extra_classes.join(' ')}" data-status="${contact.presence.get('show')}">
+            <converse-roster-contact .model=${contact}></converse-roster-contact>
+        </li>`;
+}
+
+
+export default  (o) => {
+    const i18n_title = __('Click to hide these contacts');
+    const collapsed = _converse.roster.state.get('collapsed_groups');
+    return html`
+        <div class="roster-group" data-group="${o.name}">
+            <a href="#" class="list-toggle group-toggle controlbox-padded" title="${i18n_title}" @click=${ev => toggleGroup(ev, o.name)}>
+                <span class="fa ${ (collapsed.includes(o.name)) ? 'fa-caret-right' : 'fa-caret-down' }"></span> ${o.name}
+            </a>
+            <ul class="items-list roster-group-contacts ${ (collapsed.includes(o.name)) ? 'collapsed' : '' }" data-group="${o.name}">
+                ${ o.contacts.map(renderContact) }
+            </ul>
+        </div>`;
+}

+ 0 - 9
src/plugins/rosterview/templates/group_header.js

@@ -1,9 +0,0 @@
-import { html } from "lit-html";
-import { _converse } from "@converse/headless/core";
-
-export default  (o) => html`
-    <a href="#" class="list-toggle group-toggle controlbox-padded" title="${o.desc_group_toggle}">
-        <span class="fa ${ (o.toggle_state === _converse.OPENED) ? 'fa-caret-down' : 'fa-caret-right' }">
-        </span> ${o.label_group}</a>
-    <ul class="items-list roster-group-contacts ${ (o.toggle_state === _converse.CLOSED) ? 'collapsed' : '' }"></ul>
-`;

+ 2 - 2
src/plugins/rosterview/templates/pending_contact.js

@@ -7,6 +7,6 @@ const tpl_pending_contact = o => html`<span class="pending-contact-name" title="
 export default  (o) => {
 export default  (o) => {
     const i18n_remove = __('Click to remove %1$s as a contact', o.display_name);
     const i18n_remove = __('Click to remove %1$s as a contact', o.display_name);
     return html`
     return html`
-        ${ api.settings.get('allow_chat_pending_contacts') ? html`<a class="list-item-link open-chat w-100" href="#">${tpl_pending_contact(o)}</a>` : tpl_pending_contact(o) };
-        <a class="list-item-action remove-xmpp-contact far fa-trash-alt" title="${i18n_remove}" href="#"></a>`;
+        ${ api.settings.get('allow_chat_pending_contacts') ? html`<a class="list-item-link open-chat w-100" href="#" @click=${o.openChat}>${tpl_pending_contact(o)}</a>` : tpl_pending_contact(o) }
+        <a class="list-item-action remove-xmpp-contact far fa-trash-alt" @click=${o.removeContact} title="${i18n_remove}" href="#"></a>`;
 }
 }

+ 3 - 1
src/plugins/rosterview/templates/requesting_contact.js

@@ -4,8 +4,10 @@ import { html } from "lit-html";
 const tpl_requesting_contact = o => html`<span class="req-contact-name w-100" title="JID: ${o.jid}">${o.display_name}</span>`;
 const tpl_requesting_contact = o => html`<span class="req-contact-name w-100" title="JID: ${o.jid}">${o.display_name}</span>`;
 
 
 export default  (o) => html`
 export default  (o) => html`
-   ${ api.settings.get('allow_chat_pending_contacts') ? html`<a class="open-chat w-100" href="#">${tpl_requesting_contact(o) }</a>` : tpl_requesting_contact(o) }
+   ${ api.settings.get('allow_chat_pending_contacts') ? html`<a class="open-chat w-100" href="#" @click=${o.openChat}>${tpl_requesting_contact(o) }</a>` : tpl_requesting_contact(o) }
    <a class="accept-xmpp-request list-item-action list-item-action--visible fa fa-check"
    <a class="accept-xmpp-request list-item-action list-item-action--visible fa fa-check"
+      @click=${o.acceptRequest}
       aria-label="${o.desc_accept}" title="${o.desc_accept}" href="#"></a>
       aria-label="${o.desc_accept}" title="${o.desc_accept}" href="#"></a>
    <a class="decline-xmpp-request list-item-action list-item-action--visible  fa fa-times"
    <a class="decline-xmpp-request list-item-action list-item-action--visible  fa fa-times"
+      @click=${o.declineRequest}
       aria-label="${o.desc_decline}" title="${o.desc_decline}" href="#"></a>`;
       aria-label="${o.desc_decline}" title="${o.desc_decline}" href="#"></a>`;

+ 69 - 14
src/plugins/rosterview/templates/roster.js

@@ -1,16 +1,71 @@
+import tpl_group from "./group.js";
+import { __ } from 'i18n';
+import { _converse, api } from "@converse/headless/core";
+import { contactsComparator, groupsComparator } from '@converse/headless/plugins/roster/utils.js';
 import { html } from "lit-html";
 import { html } from "lit-html";
-import { api } from "@converse/headless/core";
+import { shouldShowContact, shouldShowGroup } from '../utils.js';
 
 
-export default  (o) => html`
-    <div class="d-flex controlbox-padded">
-        <span class="w-100 controlbox-heading controlbox-heading--contacts">${o.heading_contacts}</span>
-        <a class="controlbox-heading__btn sync-contacts fa fa-sync" title="${o.title_sync_contacts}"></a>
-        ${ api.settings.get('allow_contact_requests') ? html`
-            <a class="controlbox-heading__btn add-contact fa fa-user-plus"
-               title="${o.title_add_contact}"
-               data-toggle="modal"
-               data-target="#add-contact-modal"></a>` : '' }
-    </div>
-    <converse-roster-filter></converse-roster-filter>
-    <div class="list-container roster-contacts"></div>
-`;
+
+function populateContactsMap (contacts_map, contact) {
+    if (contact.get('ask') === 'subscribe') {
+        const name = _converse.HEADER_PENDING_CONTACTS;
+        contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
+    } else if (contact.get('requesting')) {
+        const name = _converse.HEADER_REQUESTING_CONTACTS;
+        contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
+    } else {
+        let contact_groups;
+        if (api.settings.get('roster_groups')) {
+            contact_groups = contact.get('groups');
+            contact_groups = (contact_groups.length === 0) ? [_converse.HEADER_UNGROUPED] : contact_groups;
+        } else {
+            contact_groups = [_converse.HEADER_CURRENT_CONTACTS];
+        }
+        for (const name of contact_groups) {
+            contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
+        }
+    }
+    if (contact.get('num_unread')) {
+        const name = _converse.HEADER_UNREAD;
+        contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
+    }
+    return contacts_map;
+}
+
+
+export default () => {
+    const i18n_heading_contacts = __('Contacts');
+    const i18n_title_add_contact = __('Add a contact');
+    const i18n_title_sync_contacts = __('Re-sync your contacts');
+    const roster = _converse.roster || [];
+    const contacts_map = roster.reduce((acc, contact) => populateContactsMap(acc, contact), {});
+    const groupnames = Object.keys(contacts_map).filter(shouldShowGroup);
+    groupnames.sort(groupsComparator);
+
+    return html`
+        <div class="d-flex controlbox-padded">
+            <span class="w-100 controlbox-heading controlbox-heading--contacts">${i18n_heading_contacts}</span>
+            <a class="controlbox-heading__btn sync-contacts fa fa-sync" title="${i18n_title_sync_contacts}"></a>
+            ${ api.settings.get('allow_contact_requests') ? html`
+                <a class="controlbox-heading__btn add-contact fa fa-user-plus"
+                    title="${i18n_title_add_contact}"
+                    data-toggle="modal"
+                    data-target="#add-contact-modal"></a>` : '' }
+        </div>
+        <converse-roster-filter></converse-roster-filter>
+        <div class="list-container roster-contacts">
+            ${ groupnames?.map(name => {
+                const contacts = contacts_map[name].filter(c => shouldShowContact(c));
+                contacts.sort(contactsComparator);
+                if (contacts.length) {
+                    return tpl_group({
+                        'contacts': contacts,
+                        'name': name,
+                    });
+                } else {
+                    return '';
+                }
+            }) }
+        </div>
+    `;
+}

+ 4 - 3
src/plugins/rosterview/templates/roster_item.js

@@ -1,16 +1,17 @@
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { api } from "@converse/headless/core";
 import { api } from "@converse/headless/core";
 import { html } from "lit-html";
 import { html } from "lit-html";
+import { renderAvatar } from 'templates/directives/avatar';
 
 
 export default  (o) => {
 export default  (o) => {
    const i18n_chat = __('Click to chat with %1$s (XMPP address: %2$s)', o.display_name, o.jid);
    const i18n_chat = __('Click to chat with %1$s (XMPP address: %2$s)', o.display_name, o.jid);
    const i18n_remove = __('Click to remove %1$s as a contact', o.display_name);
    const i18n_remove = __('Click to remove %1$s as a contact', o.display_name);
    return html`
    return html`
-   <a class="list-item-link cbox-list-item open-chat w-100 ${ o.num_unread ? 'unread-msgs' : '' }" title="${i18n_chat}" href="#">
-      <canvas class="avatar" height="30" width="30"></canvas>
+   <a class="list-item-link cbox-list-item open-chat w-100 ${ o.num_unread ? 'unread-msgs' : '' }" title="${i18n_chat}" href="#" @click=${o.openChat}>
+      ${ renderAvatar(o.getAvatarData()) }
       <span class="${o.status_icon}" title="${o.desc_status}"></span>
       <span class="${o.status_icon}" title="${o.desc_status}"></span>
       ${ o.num_unread ? html`<span class="msgs-indicator">${ o.num_unread }</span>` : '' }
       ${ o.num_unread ? html`<span class="msgs-indicator">${ o.num_unread }</span>` : '' }
       <span class="contact-name contact-name--${o.show} ${ o.num_unread ? 'unread-msgs' : ''}">${o.display_name}</span>
       <span class="contact-name contact-name--${o.show} ${ o.num_unread ? 'unread-msgs' : ''}">${o.display_name}</span>
    </a>
    </a>
-   ${ api.settings.get('allow_contact_removal') ? html`<a class="list-item-action remove-xmpp-contact far fa-trash-alt" title="${i18n_remove}" href="#"></a>` : '' }`;
+   ${ api.settings.get('allow_contact_removal') ? html`<a class="list-item-action remove-xmpp-contact far fa-trash-alt" @click=${o.removeContact} title="${i18n_remove}" href="#"></a>` : '' }`;
 }
 }

+ 63 - 21
src/plugins/rosterview/utils.js

@@ -1,33 +1,75 @@
-import log from "@converse/headless/log";
 import { _converse, api } from "@converse/headless/core";
 import { _converse, api } from "@converse/headless/core";
 
 
 
 
-export function initRosterView () {
-    if (api.settings.get("authentication") === _converse.ANONYMOUS) {
-        return;
+export function highlightRosterItem (chatbox) {
+    _converse.roster?.findWhere({'jid': chatbox.get('jid')})?.trigger('highlight');
+}
+
+
+export function toggleGroup (ev, name) {
+    ev?.preventDefault?.();
+    const collapsed = _converse.roster.state.get('collapsed_groups');
+    if (collapsed.includes(name)) {
+        _converse.roster.state.save('collapsed_groups', collapsed.filter(n => n !== name));
+    } else {
+        _converse.roster.state.save('collapsed_groups', [...collapsed, name]);
     }
     }
-    _converse.rosterview = new _converse.RosterView({'model': _converse.rostergroups });
-    _converse.rosterview.render();
-    /**
-     * Triggered once the _converse.RosterView instance has been created and initialized.
-     * @event _converse#rosterViewInitialized
-     * @example _converse.api.listen.on('rosterViewInitialized', () => { ... });
-     */
-    api.trigger('rosterViewInitialized');
 }
 }
 
 
 
 
-export function highlightRosterItem (chatbox) {
-    _converse.roster?.findWhere({'jid': chatbox.get('jid')})?.trigger('highlight');
+export function isContactFiltered (contact) {
+    const filter = _converse.roster_filter;
+    const type = filter.get('filter_type');
+    const q = (type === 'state') ?
+        filter.get('chat_state').toLowerCase() :
+        filter.get('filter_text').toLowerCase();
+
+    if (!q) return false;
+
+    if (type === 'state') {
+        const sticky_groups = [_converse.HEADER_REQUESTING_CONTACTS, _converse.HEADER_UNREAD];
+        if (sticky_groups.includes(this.model.get('name'))) {
+            // When filtering by chat state, we still want to
+            // show sticky groups, even though they don't
+            // match the state in question.
+            return false;
+        } else if (q === 'unread_messages') {
+            return contact.get('num_unread') === 0;
+        } else if (q === 'online') {
+            return ["offline", "unavailable"].includes(contact.presence.get('show'));
+        } else {
+            return !contact.presence.get('show').includes(q);
+        }
+    } else if (type === 'contacts')  {
+        return !contact.getFilterCriteria().includes(q);
+    }
 }
 }
 
 
+export function shouldShowContact (contact) {
+    const chat_status = contact.presence.get('show');
+    if (api.settings.get('hide_offline_users') && chat_status === 'offline') {
+        // If pending or requesting, show
+        if ((contact.get('ask') === 'subscribe') ||
+                (contact.get('subscription') === 'from') ||
+                (contact.get('requesting') === true)) {
+            return !isContactFiltered(contact);
+        }
+        return false;
+    }
+    return !isContactFiltered(contact);
+}
 
 
-export function insertRoster (view) {
-    if (!view.model.get('connected') || api.settings.get("authentication") === _converse.ANONYMOUS) {
-        return;
+export function shouldShowGroup (group) {
+    const filter = _converse.roster_filter;
+    const type = filter.get('filter_type');
+    if (type === 'groups') {
+        const q = filter.get('filter_text')?.toLowerCase();
+        if (!q) {
+            return true;
+        }
+        if (!group.toLowerCase().includes(q)) {
+            return false;
+        }
     }
     }
-    /* Place the rosterview inside the "Contacts" panel. */
-    api.waitUntil('rosterViewInitialized')
-        .then(() => view.controlbox_pane.el.insertAdjacentElement('beforeEnd', _converse.rosterview.el))
-        .catch(e => log.fatal(e));
+    return true;
 }
 }

+ 1 - 1
src/templates/form_captcha.js

@@ -1,6 +1,6 @@
 import { html } from "lit-html";
 import { html } from "lit-html";
 
 
-export default  (o) => html`
+export default (o) => html`
     <fieldset class="form-group">
     <fieldset class="form-group">
         ${o.label ? html`<label>${o.label}</label>` : '' }
         ${o.label ? html`<label>${o.label}</label>` : '' }
         <img src="data:${o.type};base64,${o.data}">
         <img src="data:${o.type};base64,${o.data}">

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.