浏览代码

Refactoring of the roster view.

* Removed the dependency on jQuery
* Contacts are now shown inside a group element, simplifying the code
JC Brand 7 年之前
父节点
当前提交
6c6ef1f1f9
共有 13 个文件被更改,包括 628 次插入617 次删除
  1. 1 1
      .eslintrc.json
  2. 87 88
      css/converse.css
  3. 87 88
      css/inverse.css
  4. 3 2
      sass/_core.scss
  5. 96 98
      sass/_roster.scss
  6. 21 21
      spec/chatbox.js
  7. 4 3
      spec/chatroom.js
  8. 169 131
      spec/controlbox.js
  9. 7 7
      spec/protocol.js
  10. 3 1
      src/converse-core.js
  11. 148 176
      src/converse-rosterview.js
  12. 1 0
      src/templates/group_header.html
  13. 1 1
      src/templates/roster.html

+ 1 - 1
.eslintrc.json

@@ -18,7 +18,7 @@
         "lodash/prefer-lodash-method": [2, {
             "ignoreMethods": [
                 "find", "endsWith", "startsWith", "filter", "reduce",
-                "map", "replace", "toLower", "split", "trim", "forEach", "toUpperCase"
+                "map", "replace", "toLower", "split", "trim", "forEach", "toUpperCase", "includes"
             ]
         }],
         "lodash/prefer-startswith": "off",

+ 87 - 88
css/converse.css

@@ -1203,8 +1203,9 @@
     display: none; }
   #converse-embedded-chat .collapsed,
   #conversejs .collapsed {
-    height: 0;
-    overflow: hidden; }
+    height: 0 !important;
+    overflow: hidden !important;
+    padding: 0 !important; }
   #converse-embedded-chat .locked,
   #conversejs .locked {
     padding-right: 22px; }
@@ -2374,103 +2375,101 @@
     margin: 0;
     height: 100%;
     overflow-x: hidden;
-    overflow-y: auto;
-    display: none; }
-    #conversejs #converse-roster .roster-contacts dt.roster-group {
+    overflow-y: auto; }
+    #conversejs #converse-roster .roster-contacts .roster-group {
       border: none;
       color: #777;
-      display: none;
       font-weight: normal;
-      margin: 1em 0 0.5em 0;
       text-shadow: 0 1px 0 #FAFAFA; }
-      #conversejs #converse-roster .roster-contacts dt.roster-group .group-toggle {
+      #conversejs #converse-roster .roster-contacts .roster-group .group-toggle {
         color: #777;
         display: block;
-        width: 100%; }
-        #conversejs #converse-roster .roster-contacts dt.roster-group .group-toggle:hover {
+        width: 100%;
+        margin: 0.5em 0; }
+        #conversejs #converse-roster .roster-contacts .roster-group .group-toggle:hover {
           color: #585B51; }
-    #conversejs #converse-roster .roster-contacts dd {
-      border: none;
-      clear: both;
-      color: #777;
-      display: block;
-      height: 24px;
-      overflow-y: hidden;
-      text-shadow: 0 1px 0 #FAFAFA;
-      line-height: 14px;
-      width: 100%;
-      height: 30px;
-      margin: 0;
-      padding: 0.5em 0 0 0; }
-      #conversejs #converse-roster .roster-contacts dd a:hover {
-        color: #206485; }
-      #conversejs #converse-roster .roster-contacts dd .open-chat {
-        margin: auto;
-        padding: 0;
-        width: 85%; }
-        #conversejs #converse-roster .roster-contacts dd .open-chat.unread-msgs {
-          font-weight: bold; }
-          #conversejs #converse-roster .roster-contacts dd .open-chat.unread-msgs .contact-name {
-            width: 70%; }
-        #conversejs #converse-roster .roster-contacts dd .open-chat .msgs-indicator {
-          background-color: #3AA569;
-          opacity: 1;
-          border-radius: 10%;
-          padding: 0 0.2em;
-          font-size: 12px; }
-        #conversejs #converse-roster .roster-contacts dd .open-chat .contact-name {
+      #conversejs #converse-roster .roster-contacts .roster-group li {
+        border: none;
+        clear: both;
+        color: #777;
+        display: block;
+        height: 24px;
+        overflow-y: hidden;
+        text-shadow: 0 1px 0 #FAFAFA;
+        line-height: 14px;
+        width: 100%;
+        height: 30px;
+        margin: 0;
+        padding: 0.5em 0 0 0; }
+        #conversejs #converse-roster .roster-contacts .roster-group li a:hover {
+          color: #206485; }
+        #conversejs #converse-roster .roster-contacts .roster-group li .open-chat {
+          margin: auto;
+          padding: 0;
+          width: 85%; }
+          #conversejs #converse-roster .roster-contacts .roster-group li .open-chat.unread-msgs {
+            font-weight: bold; }
+            #conversejs #converse-roster .roster-contacts .roster-group li .open-chat.unread-msgs .contact-name {
+              width: 70%; }
+          #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .msgs-indicator {
+            background-color: #3AA569;
+            opacity: 1;
+            border-radius: 10%;
+            padding: 0 0.2em;
+            font-size: 12px; }
+          #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .contact-name {
+            overflow: hidden;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            padding: 0;
+            margin: 0;
+            max-width: 80%;
+            float: none;
+            height: 60px; }
+            #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .contact-name.unread-msgs {
+              max-width: 60%; }
+          #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .avatar {
+            float: left;
+            display: inline-block;
+            height: 60px; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .request-actions {
+          padding: 0 0 0 0.3em;
+          float: right; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .open-chat {
+          max-width: 70%; }
+          #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .open-chat .req-contact-name {
+            width: 100%; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .req-contact-name {
+          line-height: 16px;
+          width: 69%;
+          padding: 0; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.current-xmpp-contact span {
+          font-size: 14px;
+          float: left;
+          margin-right: 0.5em; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.odd {
+          background-color: #DCEAC5;
+          /* Make this difference */ }
+        #conversejs #converse-roster .roster-contacts .roster-group li a, #conversejs #converse-roster .roster-contacts .roster-group li span {
+          display: inline-block;
           overflow: hidden;
           white-space: nowrap;
-          text-overflow: ellipsis;
+          text-overflow: ellipsis; }
+        #conversejs #converse-roster .roster-contacts .roster-group li span {
           padding: 0;
+          height: 100%; }
+        #conversejs #converse-roster .roster-contacts .roster-group li a.decline-xmpp-request {
+          margin-left: 5px; }
+        #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact {
+          font-size: 10px;
+          float: right;
           margin: 0;
-          max-width: 80%;
-          float: none;
-          height: 60px; }
-          #conversejs #converse-roster .roster-contacts dd .open-chat .contact-name.unread-msgs {
-            max-width: 60%; }
-        #conversejs #converse-roster .roster-contacts dd .open-chat .avatar {
-          float: left;
-          display: inline-block;
-          height: 60px; }
-      #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .request-actions {
-        padding: 0 0 0 0.3em;
-        float: right; }
-      #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .open-chat {
-        max-width: 70%; }
-        #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .open-chat .req-contact-name {
-          width: 100%; }
-      #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .req-contact-name {
-        line-height: 16px;
-        width: 69%;
-        padding: 0; }
-      #conversejs #converse-roster .roster-contacts dd.current-xmpp-contact span {
-        font-size: 14px;
-        float: left;
-        margin-right: 0.5em; }
-      #conversejs #converse-roster .roster-contacts dd.odd {
-        background-color: #DCEAC5;
-        /* Make this difference */ }
-      #conversejs #converse-roster .roster-contacts dd a, #conversejs #converse-roster .roster-contacts dd span {
-        display: inline-block;
-        overflow: hidden;
-        white-space: nowrap;
-        text-overflow: ellipsis; }
-      #conversejs #converse-roster .roster-contacts dd span {
-        padding: 0;
-        height: 100%; }
-      #conversejs #converse-roster .roster-contacts dd a.decline-xmpp-request {
-        margin-left: 5px; }
-      #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact {
-        font-size: 10px;
-        float: right;
-        margin: 0;
-        padding: 0;
-        color: #A8ABA1; }
-        #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact:before {
-          font-size: 14px; }
-        #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact:hover {
-          color: #818479; }
+          padding: 0;
+          color: #A8ABA1; }
+          #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact:before {
+            font-size: 14px; }
+          #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact:hover {
+            color: #818479; }
   #conversejs #converse-roster span.pending-contact-name {
     line-height: 16px;
     width: 100%; }

+ 87 - 88
css/inverse.css

@@ -1203,8 +1203,9 @@
     display: none; }
   #converse-embedded-chat .collapsed,
   #conversejs .collapsed {
-    height: 0;
-    overflow: hidden; }
+    height: 0 !important;
+    overflow: hidden !important;
+    padding: 0 !important; }
   #converse-embedded-chat .locked,
   #conversejs .locked {
     padding-right: 22px; }
@@ -2533,103 +2534,101 @@ body {
     margin: 0;
     height: 100%;
     overflow-x: hidden;
-    overflow-y: auto;
-    display: none; }
-    #conversejs #converse-roster .roster-contacts dt.roster-group {
+    overflow-y: auto; }
+    #conversejs #converse-roster .roster-contacts .roster-group {
       border: none;
       color: #777;
-      display: none;
       font-weight: normal;
-      margin: 1em 0 0.5em 0;
       text-shadow: 0 1px 0 #FAFAFA; }
-      #conversejs #converse-roster .roster-contacts dt.roster-group .group-toggle {
+      #conversejs #converse-roster .roster-contacts .roster-group .group-toggle {
         color: #777;
         display: block;
-        width: 100%; }
-        #conversejs #converse-roster .roster-contacts dt.roster-group .group-toggle:hover {
+        width: 100%;
+        margin: 0.5em 0; }
+        #conversejs #converse-roster .roster-contacts .roster-group .group-toggle:hover {
           color: #585B51; }
-    #conversejs #converse-roster .roster-contacts dd {
-      border: none;
-      clear: both;
-      color: #777;
-      display: block;
-      height: 24px;
-      overflow-y: hidden;
-      text-shadow: 0 1px 0 #FAFAFA;
-      line-height: 16px;
-      width: 100%;
-      height: 30px;
-      margin: 0;
-      padding: 0.5em 0 0 0; }
-      #conversejs #converse-roster .roster-contacts dd a:hover {
-        color: #206485; }
-      #conversejs #converse-roster .roster-contacts dd .open-chat {
-        margin: auto;
-        padding: 0;
-        width: 85%; }
-        #conversejs #converse-roster .roster-contacts dd .open-chat.unread-msgs {
-          font-weight: bold; }
-          #conversejs #converse-roster .roster-contacts dd .open-chat.unread-msgs .contact-name {
-            width: 70%; }
-        #conversejs #converse-roster .roster-contacts dd .open-chat .msgs-indicator {
-          background-color: #3AA569;
-          opacity: 1;
-          border-radius: 10%;
-          padding: 0 0.2em;
-          font-size: 14px; }
-        #conversejs #converse-roster .roster-contacts dd .open-chat .contact-name {
+      #conversejs #converse-roster .roster-contacts .roster-group li {
+        border: none;
+        clear: both;
+        color: #777;
+        display: block;
+        height: 24px;
+        overflow-y: hidden;
+        text-shadow: 0 1px 0 #FAFAFA;
+        line-height: 16px;
+        width: 100%;
+        height: 30px;
+        margin: 0;
+        padding: 0.5em 0 0 0; }
+        #conversejs #converse-roster .roster-contacts .roster-group li a:hover {
+          color: #206485; }
+        #conversejs #converse-roster .roster-contacts .roster-group li .open-chat {
+          margin: auto;
+          padding: 0;
+          width: 85%; }
+          #conversejs #converse-roster .roster-contacts .roster-group li .open-chat.unread-msgs {
+            font-weight: bold; }
+            #conversejs #converse-roster .roster-contacts .roster-group li .open-chat.unread-msgs .contact-name {
+              width: 70%; }
+          #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .msgs-indicator {
+            background-color: #3AA569;
+            opacity: 1;
+            border-radius: 10%;
+            padding: 0 0.2em;
+            font-size: 14px; }
+          #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .contact-name {
+            overflow: hidden;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            padding: 0;
+            margin: 0;
+            max-width: 80%;
+            float: none;
+            height: 30px; }
+            #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .contact-name.unread-msgs {
+              max-width: 60%; }
+          #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .avatar {
+            float: left;
+            display: inline-block;
+            height: 30px; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .request-actions {
+          padding: 0 0 0 0.3em;
+          float: right; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .open-chat {
+          max-width: 70%; }
+          #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .open-chat .req-contact-name {
+            width: 100%; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .req-contact-name {
+          line-height: 22px;
+          width: 69%;
+          padding: 0; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.current-xmpp-contact span {
+          font-size: 16px;
+          float: left;
+          margin-right: 0.5em; }
+        #conversejs #converse-roster .roster-contacts .roster-group li.odd {
+          background-color: #DCEAC5;
+          /* Make this difference */ }
+        #conversejs #converse-roster .roster-contacts .roster-group li a, #conversejs #converse-roster .roster-contacts .roster-group li span {
+          display: inline-block;
           overflow: hidden;
           white-space: nowrap;
-          text-overflow: ellipsis;
+          text-overflow: ellipsis; }
+        #conversejs #converse-roster .roster-contacts .roster-group li span {
           padding: 0;
+          height: 100%; }
+        #conversejs #converse-roster .roster-contacts .roster-group li a.decline-xmpp-request {
+          margin-left: 5px; }
+        #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact {
+          font-size: 10px;
+          float: right;
           margin: 0;
-          max-width: 80%;
-          float: none;
-          height: 30px; }
-          #conversejs #converse-roster .roster-contacts dd .open-chat .contact-name.unread-msgs {
-            max-width: 60%; }
-        #conversejs #converse-roster .roster-contacts dd .open-chat .avatar {
-          float: left;
-          display: inline-block;
-          height: 30px; }
-      #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .request-actions {
-        padding: 0 0 0 0.3em;
-        float: right; }
-      #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .open-chat {
-        max-width: 70%; }
-        #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .open-chat .req-contact-name {
-          width: 100%; }
-      #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .req-contact-name {
-        line-height: 22px;
-        width: 69%;
-        padding: 0; }
-      #conversejs #converse-roster .roster-contacts dd.current-xmpp-contact span {
-        font-size: 16px;
-        float: left;
-        margin-right: 0.5em; }
-      #conversejs #converse-roster .roster-contacts dd.odd {
-        background-color: #DCEAC5;
-        /* Make this difference */ }
-      #conversejs #converse-roster .roster-contacts dd a, #conversejs #converse-roster .roster-contacts dd span {
-        display: inline-block;
-        overflow: hidden;
-        white-space: nowrap;
-        text-overflow: ellipsis; }
-      #conversejs #converse-roster .roster-contacts dd span {
-        padding: 0;
-        height: 100%; }
-      #conversejs #converse-roster .roster-contacts dd a.decline-xmpp-request {
-        margin-left: 5px; }
-      #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact {
-        font-size: 10px;
-        float: right;
-        margin: 0;
-        padding: 0;
-        color: #A8ABA1; }
-        #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact:before {
-          font-size: 16px; }
-        #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact:hover {
-          color: #818479; }
+          padding: 0;
+          color: #A8ABA1; }
+          #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact:before {
+            font-size: 16px; }
+          #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact:hover {
+            color: #818479; }
   #conversejs #converse-roster span.pending-contact-name {
     line-height: 22px;
     width: 100%; }

+ 3 - 2
sass/_core.scss

@@ -86,8 +86,9 @@
         display: none;
     }
     .collapsed {
-        height: 0;
-        overflow: hidden;
+        height: 0 !important;
+        overflow: hidden !important;
+        padding: 0 !important;
     }
 
     .locked {

+ 96 - 98
sass/_roster.scss

@@ -83,13 +83,10 @@
         height: 100%;
         overflow-x: hidden;
         overflow-y: auto;
-        display: none;
-        dt.roster-group {
+        .roster-group {
             border: none;
             color: $text-color;
-            display: none;
             font-weight: normal;
-            margin: 1em 0 0.5em 0;
             text-shadow: 0 1px 0 $text-shadow-color;
             .group-toggle {
                 &:hover {
@@ -98,116 +95,117 @@
                 color: $text-color;
                 display: block;
                 width: 100%;
+                margin: 0.5em 0;;
             }
-        }
-        dd {
-            border: none;
-            clear: both;
-            color: $text-color;
-            display: block;
-            height: 24px;
-            overflow-y: hidden;
-            text-shadow: 0 1px 0 $text-shadow-color;
-            line-height: $font-size;
-            width: 100%;
-            height: 30px;
-            margin: 0;
-            padding: 0.5em 0 0 0;
-
-            a:hover {
-                color: $dark-link-color;
-            }
+            li {
+                border: none;
+                clear: both;
+                color: $text-color;
+                display: block;
+                height: 24px;
+                overflow-y: hidden;
+                text-shadow: 0 1px 0 $text-shadow-color;
+                line-height: $font-size;
+                width: 100%;
+                height: 30px;
+                margin: 0;
+                padding: 0.5em 0 0 0;
 
-            .open-chat {
-                margin: auto;
-                padding: 0;
-                width: 85%;
-                &.unread-msgs {
-                    font-weight: bold;
-                    .contact-name {
-                        width: 70%;
-                    }
+                a:hover {
+                    color: $dark-link-color;
                 }
 
-				.msgs-indicator {
-                    background-color: $chat-head-color;
-                    opacity: 1;
-                    border-radius: 10%;
-                    padding: 0 0.2em;
-                    font-size: $font-size-small;
-				}
-
-                .contact-name {
-                    overflow: hidden;
-                    white-space: nowrap;
-                    text-overflow: ellipsis;
+                .open-chat {
+                    margin: auto;
                     padding: 0;
-                    margin: 0;
-                    max-width: 80%;
-                    float: none;
-                    height: $roster-item-height;
+                    width: 85%;
                     &.unread-msgs {
-                        max-width: 60%;
+                        font-weight: bold;
+                        .contact-name {
+                            width: 70%;
+                        }
                     }
-                }
 
-                .avatar {
-                    float: left;
-                    display: inline-block;
-                    height: $roster-item-height;
-                }
-            }
-            &.requesting-xmpp-contact {
-                .request-actions {
-                    padding: 0 0 0 0.3em;
-                    float: right;
+                    .msgs-indicator {
+                        background-color: $chat-head-color;
+                        opacity: 1;
+                        border-radius: 10%;
+                        padding: 0 0.2em;
+                        font-size: $font-size-small;
+                    }
+
+                    .contact-name {
+                        overflow: hidden;
+                        white-space: nowrap;
+                        text-overflow: ellipsis;
+                        padding: 0;
+                        margin: 0;
+                        max-width: 80%;
+                        float: none;
+                        height: $roster-item-height;
+                        &.unread-msgs {
+                            max-width: 60%;
+                        }
+                    }
+
+                    .avatar {
+                        float: left;
+                        display: inline-block;
+                        height: $roster-item-height;
+                    }
                 }
-                .open-chat {
-                    max-width: 70%;
+                &.requesting-xmpp-contact {
+                    .request-actions {
+                        padding: 0 0 0 0.3em;
+                        float: right;
+                    }
+                    .open-chat {
+                        max-width: 70%;
+                        .req-contact-name {
+                            width: 100%;
+                        }
+                    }
                     .req-contact-name {
-                        width: 100%;
+                        line-height: $line-height;
+                        width: 69%;
+                        padding: 0;
                     }
                 }
-                .req-contact-name {
-                    line-height: $line-height;
-                    width: 69%;
-                    padding: 0;
+                &.current-xmpp-contact span {
+                    font-size: $font-size;
+                    float: left;
+                    margin-right: 0.5em;
                 }
-            }
-            &.current-xmpp-contact span {
-                font-size: $font-size;
-                float: left;
-                margin-right: 0.5em;
-            }
-            &.odd {
-                background-color: #DCEAC5;
-                /* Make this difference */
-            }
-            a, span {
-                display: inline-block;
-                overflow: hidden;
-                white-space: nowrap;
-                text-overflow: ellipsis;
-            }
-            span {
-                padding: 0;
-                height: 100%;
-            }
-            a {
-                &.decline-xmpp-request {
-                    margin-left: 5px;
+                &.odd {
+                    background-color: #DCEAC5;
+                    /* Make this difference */
                 }
-                &.remove-xmpp-contact {
-                    font-size: $font-size-tiny;
-                    float: right;
-                    margin: 0;
+                a, span {
+                    display: inline-block;
+                    overflow: hidden;
+                    white-space: nowrap;
+                    text-overflow: ellipsis;
+                }
+                span {
                     padding: 0;
-                    color: $subdued-color;
-                    &:before {
-                        font-size: $font-size;
+                    height: 100%;
+                }
+                a {
+                    &.decline-xmpp-request {
+                        margin-left: 5px;
                     }
-                    &:hover {
-                        color: $gray-color;
+                    &.remove-xmpp-contact {
+                        font-size: $font-size-tiny;
+                        float: right;
+                        margin: 0;
+                        padding: 0;
+                        color: $subdued-color;
+                        &:before {
+                            font-size: $font-size;
+                        }
+                        &:hover {
+                            color: $gray-color;
+                        }
                     }
                 }
             }

+ 21 - 21
spec/chatbox.js

@@ -110,7 +110,7 @@
 
                 _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 300)
                 .then(function () {
                     // Test that they can be maximized again
@@ -243,7 +243,7 @@
                 test_utils.openControlBox();
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 300)
                 .then(function () {
                     var chatbox = test_utils.openChatBoxes(_converse, 1)[0],
@@ -281,7 +281,7 @@
                 test_utils.openControlBox();
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 300)
                 .then(function () {
                     var chatbox = test_utils.openChatBoxes(_converse, 1)[0],
@@ -328,7 +328,7 @@
                 test_utils.openControlBox();
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 300)
                 .then(function () {
                     spyOn(_converse, 'emit');
@@ -443,7 +443,7 @@
                     test_utils.openContactsPanel(_converse);
 
                     test_utils.waitUntil(function () {
-                            return _converse.rosterview.$el.find('dt').length;
+                            return _converse.rosterview.$el.find('.roster-group').length;
                         }, 300)
                     .then(function () {
                         // TODO: More tests can be added here...
@@ -546,7 +546,7 @@
                         test_utils.openControlBox();
                         test_utils.openContactsPanel(_converse);
                         test_utils.waitUntil(function () {
-                                return _converse.rosterview.$el.find('dt').length;
+                                return _converse.rosterview.$el.find('.roster-group').length;
                             }, 300)
                         .then(function () {
                             spyOn(_converse, 'emit');
@@ -885,7 +885,7 @@
                         test_utils.openContactsPanel(_converse);
 
                         test_utils.waitUntil(function () {
-                                return _converse.rosterview.$el.find('dt').length;
+                                return _converse.rosterview.$el.find('.roster-group').length;
                             }, 300)
                         .then(function () {
                             // Send a message from a different resource
@@ -1130,7 +1130,7 @@
                     test_utils.openControlBox();
                     test_utils.openContactsPanel(_converse);
                     test_utils.waitUntil(function () {
-                            return _converse.rosterview.$el.find('dt').length;
+                            return _converse.rosterview.$el.find('.roster-group').length;
                         }, 300)
                     .then(function () {
                         var contact_name = mock.cur_names[0];
@@ -1191,7 +1191,7 @@
                     test_utils.openControlBox();
                     test_utils.openContactsPanel(_converse);
                     test_utils.waitUntil(function () {
-                            return _converse.rosterview.$el.find('dt').length;
+                            return _converse.rosterview.$el.find('.roster-group').length;
                         }, 300)
                     .then(function () {
                         spyOn(_converse, 'emit');
@@ -1503,7 +1503,7 @@
                         test_utils.openControlBox();
                         test_utils.openContactsPanel(_converse);
                         test_utils.waitUntil(function () {
-                            return _converse.rosterview.$el.find('dt').length;
+                            return _converse.rosterview.$el.find('.roster-group').length;
                         }, 300).then(function () {
                             spyOn(_converse.connection, 'send');
                             var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -1531,7 +1531,7 @@
                         var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
                         test_utils.waitUntil(function () {
-                            return _converse.rosterview.$el.find('dt').length;
+                            return _converse.rosterview.$el.find('.roster-group').length;
                         }, 500).then(function () {
                             test_utils.openChatBoxFor(_converse, contact_jid);
                             var view = _converse.chatboxviews.get(contact_jid);
@@ -1570,7 +1570,7 @@
                         test_utils.openControlBox();
                         test_utils.openContactsPanel(_converse);
                         test_utils.waitUntil(function () {
-                                return _converse.rosterview.$el.find('dt').length;
+                                return _converse.rosterview.$el.find('.roster-group').length;
                             }, 300)
                         .then(function () {
                             var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -1694,7 +1694,7 @@
                         test_utils.openControlBox();
                         test_utils.openContactsPanel(_converse);
                         test_utils.waitUntil(function () {
-                                return _converse.rosterview.$el.find('dt').length;
+                                return _converse.rosterview.$el.find('.roster-group').length;
                             }, 300)
                         .then(function () {
                             _converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test
@@ -1757,7 +1757,7 @@
                         test_utils.openControlBox();
                         test_utils.openContactsPanel(_converse);
                         test_utils.waitUntil(function () {
-                                return _converse.rosterview.$el.find('dt').length;
+                                return _converse.rosterview.$el.find('.roster-group').length;
                             }, 300)
                         .then(function () {
                             // TODO: only show paused state if the previous state was composing
@@ -1842,7 +1842,7 @@
                         test_utils.openControlBox();
                         test_utils.openContactsPanel(_converse);
                         test_utils.waitUntil(function () {
-                            return _converse.rosterview.$el.find('dt').length;
+                            return _converse.rosterview.$el.find('.roster-group').length;
                         }, 500).then(function () {
                             // Make the timeouts shorter so that we can test
                             _converse.TIMEOUTS.PAUSED = 200;
@@ -1932,7 +1932,7 @@
                         test_utils.openControlBox();
                         test_utils.openContactsPanel(_converse);
                         test_utils.waitUntil(function () {
-                            return _converse.rosterview.$el.find('dt').length;
+                            return _converse.rosterview.$el.find('.roster-group').length;
                         }, 300).then(function () {
                             var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
                             test_utils.openChatBoxFor(_converse, contact_jid);
@@ -2327,7 +2327,7 @@
                 test_utils.createContacts(_converse, 'current');
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('dt').length;
+                    return _converse.rosterview.$el.find('.roster-group').length;
                 }, 500)
                 .then(function () {
                     var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -2360,7 +2360,7 @@
                 test_utils.createContacts(_converse, 'current');
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('dt').length;
+                    return _converse.rosterview.$el.find('.roster-group').length;
                 }, 500)
                 .then(function () {
                     var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -2394,7 +2394,7 @@
                 test_utils.createContacts(_converse, 'current');
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('dt').length;
+                    return _converse.rosterview.$el.find('.roster-group').length;
                 }, 500)
                 .then(function () {
                     var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -2429,7 +2429,7 @@
                 test_utils.createContacts(_converse, 'current');
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('dt').length;
+                    return _converse.rosterview.$el.find('.roster-group').length;
                 }, 500)
                 .then(function () {
                     var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -2462,7 +2462,7 @@
                 test_utils.createContacts(_converse, 'current');
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('dt').length;
+                    return _converse.rosterview.$el.find('.roster-group').length;
                 }, 500)
                 .then(function () {
                     var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';

+ 4 - 3
spec/chatroom.js

@@ -72,7 +72,7 @@
 
                 test_utils.createContacts(_converse, 'current');
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group .group-toggle').length;
                     }, 300)
                 .then(function () {
                     test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () {
@@ -129,9 +129,10 @@
                     return deferred.promise();
                 });
 
+                test_utils.openControlBox();
                 test_utils.createContacts(_converse, 'current');
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                    return _converse.rosterview.$el.find('.roster-group .group-toggle').length;
                 }, 300).then(function () {
                     var jid = 'lounge@localhost';
                     var room = _converse.api.rooms.open(jid);
@@ -256,7 +257,7 @@
                        ' </query>'+
                        ' </iq>')[0]));
 
-                    test_utils.waitUntil(function () {
+                    return test_utils.waitUntil(function () {
                         return sent_IQ.toLocaleString() !==
                             "<iq to='room@conference.example.org' type='get' xmlns='jabber:client' id='"+IQ_id+
                             "'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>";

+ 169 - 131
spec/controlbox.js

@@ -5,21 +5,29 @@
     var $pres = converse.env.$pres;
     var $msg = converse.env.$msg;
     var $iq = converse.env.$iq;
+    var u = converse.env.utils;
 
-    var checkHeaderToggling = function ($header) {
-        var $toggle = $header.find('a.group-toggle');
-        expect($header.css('display')).toEqual('block');
-        expect($header.nextUntil('dt', 'dd').length === $header.nextUntil('dt', 'dd:visible').length).toBeTruthy();
+    var checkHeaderToggling = function ($group) {
+        var $toggle = $group.find('a.group-toggle');
+        expect(u.isVisible($group[0])).toBeTruthy();
+        expect($group.find('ul.collapsed').length).toBe(0);
         expect($toggle.hasClass('icon-closed')).toBeFalsy();
         expect($toggle.hasClass('icon-opened')).toBeTruthy();
         $toggle.click();
-        expect($toggle.hasClass('icon-closed')).toBeTruthy();
-        expect($toggle.hasClass('icon-opened')).toBeFalsy();
-        expect($header.nextUntil('dt', 'dd').length === $header.nextUntil('dt', 'dd:hidden').length).toBeTruthy();
-        $toggle.click();
-        expect($toggle.hasClass('icon-closed')).toBeFalsy();
-        expect($toggle.hasClass('icon-opened')).toBeTruthy();
-        expect($header.nextUntil('dt', 'dd').length === $header.nextUntil('dt', 'dd:visible').length).toBeTruthy();
+
+        return test_utils.waitUntil(function () {
+            return $group.find('ul.collapsed').length === 1;
+        }, 500).then(function () {
+            expect($toggle.hasClass('icon-closed')).toBeTruthy();
+            expect($toggle.hasClass('icon-opened')).toBeFalsy();
+            $toggle.click();
+            return test_utils.waitUntil(function () {
+                return $group.find('li').length === $group.find('li:visible').length
+            }, 500);
+        }).then(function () {
+            expect($toggle.hasClass('icon-closed')).toBeFalsy();
+            expect($toggle.hasClass('icon-opened')).toBeTruthy();
+        });
     };
 
     describe("The Control Box", function () {
@@ -149,7 +157,7 @@
                     };
 
                     return test_utils.waitUntil(function () {
-                        if (_converse.rosterview.$roster.hasScrollBar()) {
+                        if ($(_converse.rosterview.roster_el).hasScrollBar()) {
                             return $filter.is(':visible');
                         } else {
                             return !$filter.is(':visible');
@@ -165,54 +173,64 @@
                     null, ['rosterGroupsFetched'], {},
                     function (done, _converse) {
 
-                var $filter;
-                var $roster;
                 _converse.roster_groups = true;
                 test_utils.openControlBox();
                 test_utils.createGroupedContacts(_converse);
-                $filter = _converse.rosterview.$('.roster-filter');
-                $roster = _converse.rosterview.$roster;
+                var $filter = _converse.rosterview.$('.roster-filter');
+                var $roster = $(_converse.rosterview.roster_el);
                 _converse.rosterview.filter_view.delegateEvents();
 
                 var promise = test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 15;
-                    }, 500)
-                .then(function (contacts) {
-                    expect($roster.find('dt:visible').length).toBe(5);
+                    return $roster.find('li:visible').length === 15;
+                }, 500).then(function (contacts) {
+                    expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
                     $filter.val("candice");
                     $filter.trigger('keydown');
 
                     return test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 1;
+                        return $roster.find('li:visible').length === 1;
                     }, 500);
                 }).then(function (contacts) {
-                    expect($roster.find('dd:visible').eq(0).text().trim()).toBe('Candice van der Knijff');
-                    expect($roster.find('dt:visible').length).toBe(1);
-                    expect(_.trim($roster.find('dt:visible').eq(0).text())).toBe('colleagues');
+                    // Only one roster contact is now visible
+                    expect($roster.find('li:visible').length).toBe(1);
+                    expect($roster.find('li:visible').eq(0).text().trim()).toBe('Candice van der Knijff');
+                    // Only one foster group is still visible
+                    expect($roster.find('.roster-group:visible').length).toBe(1);
+                    expect(_.trim($roster.find('.roster-group:visible a.group-toggle').eq(0).text())).toBe('colleagues');
+
                     $filter = _converse.rosterview.$('.roster-filter');
                     $filter.val("an");
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 5;
+                        return $roster.find('li:visible').length === 5;
                     }, 500)
                 }).then(function (contacts) {
-                    expect($roster.find('dt:visible').length).toBe(4);
+                    // Five roster contact is now visible
+                    expect($roster.find('li:visible').length).toBe(5);
+                    // Four groups are still visible
+                    var $groups = $roster.find('.roster-group:visible a.group-toggle');
+                    expect($groups.length).toBe(4);
+                    expect(_.trim($groups.eq(0).text())).toBe('colleagues');
+                    expect(_.trim($groups.eq(1).text())).toBe('Family');
+                    expect(_.trim($groups.eq(2).text())).toBe('friends & acquaintences');
+                    expect(_.trim($groups.eq(3).text())).toBe('ænemies');
+
                     $filter = _converse.rosterview.$('.roster-filter');
                     $filter.val("xxx");
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 0;
+                        return $roster.find('li:visible').length === 0;
                     }, 500)
                 }).then(function () {
-                    expect($roster.find('dt:visible').length).toBe(0);
+                    expect($roster.find('ul.roster-group-contacts:visible a.group-toggle').length).toBe(0);
                     $filter = _converse.rosterview.$('.roster-filter');
                     $filter.val("");  // Check that contacts are shown again, when the filter string is cleared.
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 15;
+                        return $roster.find('li:visible').length === 15;
                     }, 500)
                 }).then(function () {
-                    expect($roster.find('dt:visible').length).toBe(5);
+                    expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
                     _converse.roster_groups = false;
                     done();
                 });
@@ -224,46 +242,49 @@
                     function (done, _converse) {
 
                 var $filter;
-                var $roster;
                 var $type;
                 _converse.roster_groups = true;
                 test_utils.openControlBox();
                 test_utils.createGroupedContacts(_converse);
                 _converse.rosterview.filter_view.delegateEvents();
                 $filter = _converse.rosterview.$('.roster-filter');
-                $roster = _converse.rosterview.$roster;
+                var $roster = $(_converse.rosterview.roster_el);
                 $type = _converse.rosterview.$('.filter-type');
                 $type.val('groups');
-                var promise = test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 15;
-                    }, 500); 
-                promise.then(function () {
-                    expect($roster.find('dt:visible').length).toBe(5);
+                test_utils.waitUntil(function () {
+                    return $roster.find('li:visible').length === 15;
+                }, 500).then(function () {
+                    expect($roster.find('div.roster-group:visible a.group-toggle').length).toBe(5);
+
                     $filter.val("colleagues");
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
-                        return $roster.find('dt:visible').length === 1;
+                        return $roster.find('div.roster-group:not(.collapsed) a.group-toggle').length === 1;
                     }, 500);
                 }).then(function () {
-                    expect(_.trim($roster.find('dt:visible').eq(0).text())).toBe('colleagues');
-                    expect($roster.find('dd:visible').length).toBe(3);
+                    expect(_.trim($roster.find('div.roster-group:not(.collapsed) a').eq(0).text())).toBe('colleagues');
+                    expect($roster.find('div.roster-group:not(.collapsed) li:visible').length).toBe(3);
+
                     // Check that all contacts under the group are shown
-                    expect($roster.find('dt:visible').nextUntil('dt', 'dd:hidden').length).toBe(0);
+                    expect($roster.find('div.roster-group:not(.collapsed) li:hidden').length).toBe(0);
+
                     $filter = _converse.rosterview.$('.roster-filter');
                     $filter.val("xxx").trigger('keydown');
                     return test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 0;
+                        return $roster.find('div.roster-group.collapsed a.group-toggle').length === 5;
                     }, 700);
                 }).then(function () {
-                    expect($roster.find('dt:visible').length).toBe(0);
+                    expect($roster.find('div.roster-group:not(.collapsed) a').length).toBe(0);
+
                     $filter = _converse.rosterview.$('.roster-filter');
                     $filter.val(""); // Check that groups are shown again, when the filter string is cleared.
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 15;
+                        return $roster.find('div.roster-group.collapsed a.group-toggle').length === 0;
                     }, 500);
                 }).then(function () {
-                    expect($roster.find('dt:visible').length).toBe(5);
+                    expect($roster.find('div.roster-group:not(collapsed)').length).toBe(5);
+                    expect($roster.find('div.roster-group:not(collapsed) li').length).toBe(15);
                     done();
                 });
             }));
@@ -302,7 +323,6 @@
                     function (done, _converse) {
 
                 var $filter;
-                var $roster;
                 _converse.roster_groups = true;
                 test_utils.createGroupedContacts(_converse);
                 var jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -313,21 +333,20 @@
                 var $type = _converse.rosterview.$('.filter-type');
                 $type.val('state').trigger('change');
                 $filter = _converse.rosterview.$('.state-type');
-                $roster = _converse.rosterview.$roster;
+                var $roster = $(_converse.rosterview.roster_el);
 
                 test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 15;
-                    }, 500)
-                .then(function () {
-                    expect($roster.find('dt:visible').length).toBe(5);
+                        return $roster.find('li:visible').length === 15;
+                }, 500).then(function () {
+                    expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
                     $filter.val("online");
                     $filter.trigger('change');
                     return test_utils.waitUntil(function () {
-                        return $roster.find('dd:visible').length === 1;
+                        return $roster.find('li:visible').length === 1;
                     }, 500)
                 }).then(function () {
-                    expect($roster.find('dd:visible').eq(0).text().trim()).toBe('Rinse Sommer');
-                    expect($roster.find('dt:visible').length).toBe(1);
+                    expect($roster.find('li:visible').eq(0).text().trim()).toBe('Rinse Sommer');
+                    expect($roster.find('ul.roster-group-contacts:visible').length).toBe(1);
                     var $type = _converse.rosterview.$('.filter-type');
                     $type.val('contacts').trigger('change');
                     done();
@@ -346,16 +365,19 @@
                 spyOn(_converse, 'emit');
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 _converse.rosterview.render();
+                test_utils.openControlBox();
                 test_utils.createContacts(_converse, 'pending');
                 test_utils.createContacts(_converse, 'requesting');
                 test_utils.createGroupedContacts(_converse);
                 // Check that the groups appear alphabetically and that
                 // requesting and pending contacts are last.
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
-                    }, 500)
-                .then(function () {
-                    var group_titles = $.map(_converse.rosterview.$el.find('dt'), function (o) { return $(o).text().trim(); });
+                    return _converse.rosterview.$el.find('.roster-group:visible a.group-toggle').length;
+                }, 500).then(function () {
+                    var group_titles = $.map(
+                        _converse.rosterview.$el.find('.roster-group:visible a.group-toggle'),
+                        function (o) { return $(o).text().trim(); }
+                    );
                     expect(group_titles).toEqual([
                         "Contact requests",
                         "colleagues",
@@ -367,7 +389,7 @@
                     ]);
                     // Check that usernames appear alphabetically per group
                     _.each(_.keys(mock.groups), function (name) {
-                        var $contacts = _converse.rosterview.$('dt.roster-group[data-group="'+name+'"]').nextUntil('dt', 'dd');
+                        var $contacts = _converse.rosterview.$('.roster-group[data-group="'+name+'"] ul');
                         var names = $.map($contacts, function (o) { return $(o).text().trim(); });
                         expect(names).toEqual(_.clone(names).sort());
                     });
@@ -384,6 +406,7 @@
                 var groups = ['colleagues', 'friends'];
                 spyOn(_converse, 'emit');
                 spyOn(_converse.rosterview, 'update').and.callThrough();
+                test_utils.openControlBox();
                 _converse.rosterview.render();
                 for (var i=0; i<mock.cur_names.length; i++) {
                     _converse.roster.create({
@@ -395,12 +418,12 @@
                     });
                 }
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dd').length;
+                        return _converse.rosterview.$el.find('li:visible').length;
                     }, 500)
                 .then(function () {
                     // Check that usernames appear alphabetically per group
                     _.each(groups, function (name) {
-                        var $contacts = _converse.rosterview.$('dt.roster-group[data-group="'+name+'"]').nextUntil('dt', 'dd');
+                        var $contacts = _converse.rosterview.$('.roster-group[data-group="'+name+'"] li');
                         var names = $.map($contacts, function (o) { return $(o).text().trim(); });
                         expect(names).toEqual(_.clone(names).sort());
                         expect(names.length).toEqual(mock.cur_names.length);
@@ -415,6 +438,8 @@
                     function (done, _converse) {
 
                 _converse.roster_groups = true;
+                test_utils.openControlBox();
+
                 var i=0, j=0;
                 var groups = {
                     'colleagues': 3,
@@ -437,10 +462,16 @@
                 var $toggle = view.$el.find('a.group-toggle');
                 expect(view.model.get('state')).toBe('opened');
                 $toggle.click();
-                expect(view.model.get('state')).toBe('closed');
-                $toggle.click();
-                expect(view.model.get('state')).toBe('opened');
-                done();
+                return test_utils.waitUntil(function () {
+                    return view.model.get('state') === 'closed';
+                }, 500).then(function () {
+                    $toggle.click();
+                    return test_utils.waitUntil(function () {
+                        return view.model.get('state') === 'opened';
+                    }, 500)
+                }).then(function () {
+                    done();
+                });
             }));
         });
 
@@ -448,7 +479,9 @@
 
             function _addContacts (_converse) {
                 // Must be initialized, so that render is called and documentFragment set up.
-                test_utils.createContacts(_converse, 'pending').openControlBox().openContactsPanel(_converse);
+                test_utils.createContacts(_converse, 'pending');
+                test_utils.openControlBox();
+                test_utils.openContactsPanel(_converse);
             }
 
             it("can be collapsed under their own header", 
@@ -458,11 +491,12 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dd').length;
-                    }, 500)
-                .then(function () {
-                    checkHeaderToggling.apply(_converse, [_converse.rosterview.get('Pending contacts').$el]);
-                    done();
+                    return _converse.rosterview.$el.find('li').length;
+                }, 500).then(function () {
+                    checkHeaderToggling.apply(
+                        _converse,
+                        [_converse.rosterview.get('Pending contacts').$el]
+                    ).then(done);
                 });
             }));
 
@@ -490,16 +524,16 @@
                     function (done, _converse) {
 
                 _converse.show_only_online_users = true;
+                test_utils.openControlBox();
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dd').length;
-                    }, 500)
-                .then(function () {
+                    return _converse.rosterview.$el.find('li').length;
+                }, 500).then(function () {
                     expect(_converse.rosterview.$el.is(':visible')).toEqual(true);
                     expect(_converse.rosterview.update).toHaveBeenCalled();
-                    expect(_converse.rosterview.$el.find('dd:visible').length).toBe(3);
-                    expect(_converse.rosterview.$el.find('dt:visible').length).toBe(1);
+                    expect(_converse.rosterview.$el.find('li:visible').length).toBe(3);
+                    expect(_converse.rosterview.$el.find('ul.roster-group-contacts:visible').length).toBe(1);
                     done();
                 });
             }));
@@ -513,13 +547,13 @@
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dd:visible').length;
+                        return _converse.rosterview.$el.find('li:visible').length;
                     }, 500)
                 .then(function () {
                     expect(_converse.rosterview.update).toHaveBeenCalled();
                     expect(_converse.rosterview.$el.is(':visible')).toBe(true);
-                    expect(_converse.rosterview.$el.find('dd:visible').length).toBe(3);
-                    expect(_converse.rosterview.$el.find('dt:visible').length).toBe(1);
+                    expect(_converse.rosterview.$el.find('li:visible').length).toBe(3);
+                    expect(_converse.rosterview.$el.find('ul.roster-group-contacts:visible').length).toBe(1);
                     done();
                 });
             }));
@@ -601,7 +635,7 @@
                     _converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')")
                         .parent().siblings('.remove-xmpp-contact').click();
                 }
-                expect(_converse.rosterview.$el.find('dt#pending-xmpp-contacts').is(':visible')).toBeFalsy();
+                expect(_converse.rosterview.$el.find('#pending-xmpp-contacts').is(':visible')).toBeFalsy();
                 done();
             }));
 
@@ -623,7 +657,7 @@
                     expect(_converse.rosterview.update).toHaveBeenCalled();
                 }
                 // Check that they are sorted alphabetically
-                t = _.reduce(_converse.rosterview.get('Pending contacts').$el.siblings('dd.pending-xmpp-contact').find('span'), function (result, value) {
+                t = _.reduce(_converse.rosterview.get('Pending contacts').$el.find('.pending-xmpp-contact span'), function (result, value) {
                     return result + _.trim(value.textContent);
                 }, '');
                 expect(t).toEqual(mock.pend_names.slice(0,i+1).sort().join(''));
@@ -643,11 +677,12 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dd:visible').length;
-                    }, 500)
-                .then(function () {
-                    checkHeaderToggling.apply(_converse, [_converse.rosterview.$el.find('dt.roster-group')]);
-                    done();
+                        return _converse.rosterview.$el.find('li:visible').length;
+                }, 500).then(function () {
+                    checkHeaderToggling.apply(
+                        _converse,
+                        [_converse.rosterview.$el.find('.roster-group')]
+                    ).then(done);
                 });
             }));
 
@@ -659,10 +694,10 @@
                 _converse.roster_groups = false;
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dd:visible').length;
+                        return _converse.rosterview.$el.find('li:visible').length;
                     }, 500)
                 .then(function () {
-                    _converse.rosterview.$el.find('dt.roster-group').find('a.group-toggle').click();
+                    _converse.rosterview.$el.find('.roster-group a.group-toggle').click();
                     var name = "Max Mustermann";
                     var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
                     _converse.roster.create({
@@ -694,10 +729,10 @@
                     expect(_converse.rosterview.update).toHaveBeenCalled();
                 }
                 test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('dd').length;
+                    return _converse.rosterview.$el.find('li').length;
                 }).then(function () {
                     // Check that they are sorted alphabetically
-                    var t = _.reduce(_converse.rosterview.$('dt.roster-group').siblings('dd.current-xmpp-contact.offline').find('a.open-chat'), function (result, value) {
+                    var t = _.reduce(_converse.rosterview.$('.roster-group').find('.current-xmpp-contact.offline a.open-chat'), function (result, value) {
                         return result + _.trim(value.textContent);
                     }, '');
                     expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
@@ -712,9 +747,8 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dd').length;
-                    }, 500)
-                .then(function () {
+                    return _converse.rosterview.$el.find('li').length;
+                }, 500).then(function () {
                     var name = mock.cur_names[0];
                     var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
                     var contact = _converse.roster.get(jid);
@@ -748,7 +782,7 @@
                     fullname: name
                 });
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     spyOn(window, 'confirm').and.returnValue(true);
@@ -757,13 +791,13 @@
                         if (typeof callback === "function") { return callback(); }
                     });
 
-                    expect(_converse.rosterview.$el.find('dt.roster-group').css('display')).toEqual('block');
+                    expect(_converse.rosterview.$el.find('.roster-group').css('display')).toEqual('block');
                     _converse.rosterview.$el.find(".open-chat:contains('"+name+"')")
                         .parent().find('.remove-xmpp-contact').click();
                     expect(window.confirm).toHaveBeenCalled();
                     expect(_converse.connection.sendIQ).toHaveBeenCalled();
                     expect(contact.removeFromRoster).toHaveBeenCalled();
-                    expect(_converse.rosterview.$el.find('dt.roster-group').css('display')).toEqual('none');
+                    expect(_converse.rosterview.$el.find('.roster-group').css('display')).toEqual('none');
                     done();
                 });
             }));
@@ -775,7 +809,7 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     var jid, t;
@@ -787,7 +821,7 @@
                         _converse.roster.get(jid).set('chat_status', 'online');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
-                        t = _.reduce($roster.find('dt.roster-group').siblings('dd.current-xmpp-contact.online').find('a.open-chat'), function (result, value) {
+                        t = _.reduce($roster.find('.roster-group').find('.current-xmpp-contact.online a.open-chat'), function (result, value) {
                             return result + _.trim(value.textContent);
                         }, '');
                         expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
@@ -803,7 +837,7 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     var jid, t;
@@ -815,9 +849,10 @@
                         _converse.roster.get(jid).set('chat_status', 'dnd');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
-                        t = _.reduce($roster.find('dt.roster-group').siblings('dd.current-xmpp-contact.dnd').find('a.open-chat'), function (result, value) {
-                            return result + _.trim(value.textContent);
-                        }, '');
+                        t = _.reduce($roster.find('.roster-group .current-xmpp-contact.dnd a.open-chat'),
+                            function (result, value) {
+                                return result + _.trim(value.textContent);
+                            }, '');
                         expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
                     }
                     done();
@@ -831,7 +866,7 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     var jid, t;
@@ -843,9 +878,10 @@
                         _converse.roster.get(jid).set('chat_status', 'away');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
-                        t = _.reduce($roster.find('dt.roster-group').siblings('dd.current-xmpp-contact.away').find('a.open-chat'), function (result, value) {
-                            return result + _.trim(value.textContent);
-                        }, '');
+                        t = _.reduce($roster.find('.roster-group .current-xmpp-contact.away a.open-chat'),
+                            function (result, value) {
+                                return result + _.trim(value.textContent);
+                            }, '');
                         expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
                     }
                     done();
@@ -859,7 +895,7 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     var jid, t;
@@ -871,9 +907,10 @@
                         _converse.roster.get(jid).set('chat_status', 'xa');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
-                        t = _.reduce($roster.find('dt.roster-group').siblings('dd.current-xmpp-contact.xa').find('a.open-chat'), function (result, value) {
-                            return result + _.trim(value.textContent);
-                        }, '');
+                        t = _.reduce($roster.find('.roster-group .current-xmpp-contact.xa a.open-chat'),
+                            function (result, value) {
+                                return result + _.trim(value.textContent);
+                            }, '');
                         expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
                     }
                     done();
@@ -887,7 +924,7 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     var jid, t;
@@ -899,9 +936,10 @@
                         _converse.roster.get(jid).set('chat_status', 'unavailable');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
-                        t = _.reduce($roster.find('dt.roster-group').siblings('dd.current-xmpp-contact.unavailable').find('a.open-chat'), function (result, value) {
-                            return result + _.trim(value.textContent);
-                        }, '');
+                        t = _.reduce($roster.find('.roster-group .current-xmpp-contact.unavailable a.open-chat'),
+                            function (result, value) {
+                                return result + _.trim(value.textContent);
+                            }, '');
                         expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
                     }
                     done();
@@ -915,7 +953,7 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     var i, jid;
@@ -940,7 +978,7 @@
                         _converse.roster.get(jid).set('chat_status', 'unavailable');
                     }
 
-                    var contacts = _converse.rosterview.$el.find('dd.current-xmpp-contact');
+                    var contacts = _converse.rosterview.$el.find('.current-xmpp-contact');
                     for (i=0; i<3; i++) {
                         expect($(contacts[i]).hasClass('online')).toBeTruthy();
                         expect($(contacts[i]).hasClass('both')).toBeTruthy();
@@ -1029,7 +1067,7 @@
                 }
                 expect(_converse.rosterview.update).toHaveBeenCalled();
                 // Check that they are sorted alphabetically
-                children = _converse.rosterview.get('Contact requests').$el.siblings('dd.requesting-xmpp-contact').find('span');
+                children = _converse.rosterview.get('Contact requests').$el.find('.requesting-xmpp-contact span');
                 names = [];
                 children.each(addName);
                 expect(names.join('')).toEqual(mock.req_names.slice(0,mock.req_names.length+1).sort().join(''));
@@ -1052,7 +1090,7 @@
                     fullname: name
                 });
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     expect(_converse.rosterview.get('Contact requests').$el.is(':visible')).toEqual(true);
@@ -1072,11 +1110,12 @@
 
                 test_utils.createContacts(_converse, 'requesting').openControlBox();
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
-                    }, 500)
-                .then(function () {
-                    checkHeaderToggling.apply(_converse, [_converse.rosterview.get('Contact requests').$el]);
-                    done();
+                    return _converse.rosterview.$el.find('.roster-group').length;
+                }, 500).then(function () {
+                    checkHeaderToggling.apply(
+                        _converse,
+                        [_converse.rosterview.get('Contact requests').$el]
+                    ).then(done);
                 });
             }));
 
@@ -1087,7 +1126,7 @@
 
                 test_utils.createContacts(_converse, 'requesting').openControlBox();
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     // TODO: Testing can be more thorough here, the user is
@@ -1116,7 +1155,7 @@
 
                 test_utils.createContacts(_converse, 'requesting').openControlBox();
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
@@ -1220,14 +1259,14 @@
                 test_utils.createContacts(_converse, 'all').openControlBox();
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('dt').length;
+                        return _converse.rosterview.$el.find('.roster-group').length;
                     }, 500)
                 .then(function () {
                     var jid, name, i;
                     for (i=0; i<mock.cur_names.length; i++) {
                         name = mock.cur_names[i];
                         jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
-                        var $dd = _converse.rosterview.$el.find("dd:contains('"+name+"')").children().first();
+                        var $dd = _converse.rosterview.$el.find("li:contains('"+name+"')").children().first();
                         var dd_text = $dd.text();
                         var dd_title = $dd.attr('title');
                         expect(_.trim(dd_text)).toBe(name);
@@ -1279,11 +1318,10 @@
                 fullname: mock.pend_names[0]
             });
             test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('dt').length;
-                }, 500)
-            .then(function () {
+                return _converse.rosterview.$el.find('.roster-group').length;
+            }, 500).then(function () {
                 // Checking that only one entry is created because both JID is same (Case sensitive check)
-                expect(_converse.rosterview.$el.find('dd:visible').length).toBe(1);
+                expect(_converse.rosterview.$el.find('li:visible').length).toBe(1);
                 expect(_converse.rosterview.update).toHaveBeenCalled();
                 done();
             });

+ 7 - 7
spec/protocol.js

@@ -229,15 +229,15 @@
                     // Check that the user is now properly shown as a pending
                     // contact in the roster.
             
+                    var $header = $('a:contains("Pending contacts")');
                     return test_utils.waitUntil(function () {
-                        return $('a:contains("Pending contacts")').length;
+                        return $('a:contains("Pending contacts")').length && $header.is(":visible");
                     }, 300);
                 }).then(function () {
                     var $header = $('a:contains("Pending contacts")');
-                    expect($header.length).toBe(1);
-                    expect($header.is(":visible")).toBeTruthy();
-                    var $contacts = $header.parent().nextUntil('dt', 'dd');
+                    var $contacts = $header.parent().find('li');
                     expect($contacts.length).toBe(1);
+                    expect($contacts.is(':visible')).toBeTruthy();
 
                     spyOn(contact, "ackSubscribe").and.callThrough();
                     /* Here we assume the "happy path" that the contact
@@ -300,7 +300,7 @@
                     $header = $('a:contains("My contacts")');
                     expect($header.length).toBe(1);
                     expect($header.is(":visible")).toBeTruthy();
-                    $contacts = $header.parent().nextUntil('dt', 'dd');
+                    $contacts = $header.parent().find('li');
                     expect($contacts.length).toBe(1);
                     // Check that it has the right classes and text
                     expect($contacts.hasClass('to')).toBeTruthy();
@@ -488,7 +488,7 @@
                 }).then(function () {
                     var $header = $('a:contains("My contacts")');
                     // remove the first user
-                    $($header.parent().nextUntil('dt', 'dd').find('.remove-xmpp-contact').get(0)).click();
+                    $($header.parent().find('li .remove-xmpp-contact').get(0)).click();
                     expect(window.confirm).toHaveBeenCalled();
 
                     /* Section 8.6 Removing a Roster Item and Cancelling All
@@ -554,7 +554,7 @@
                     var $header = $('a:contains("Contact requests")');
                     expect($header.length).toBe(1);
                     expect($header.is(":visible")).toBeTruthy();
-                    var $contacts = $header.parent().nextUntil('dt', 'dd');
+                    var $contacts = $header.parent().find('li');
                     expect($contacts.length).toBe(1);
                     done();
                 });

+ 3 - 1
src/converse-core.js

@@ -1109,7 +1109,8 @@
                  */
                 return new Promise((resolve, reject) => {
                     this.fetch({
-                        add: true,
+                        'add': true,
+                        'silent': true,
                         success (collection) {
                             if (collection.length === 0) {
                                 _converse.send_initial_presence = true;
@@ -1429,6 +1430,7 @@
 
 
         this.RosterGroup = Backbone.Model.extend({
+
             initialize (attributes) {
                 this.set(_.assignIn({
                     description: __('Click to hide these contacts'),

+ 148 - 176
src/converse-rosterview.js

@@ -7,8 +7,7 @@
 /*global define */
 
 (function (root, factory) {
-    define(["jquery.noconflict",
-            "converse-core",
+    define(["converse-core",
             "tpl!group_header",
             "tpl!pending_contact",
             "tpl!requesting_contact",
@@ -18,7 +17,6 @@
             "converse-chatboxes"
     ], factory);
 }(this, function (
-            $,
             converse, 
             tpl_group_header,
             tpl_pending_contact,
@@ -27,7 +25,8 @@
             tpl_roster_filter,
             tpl_roster_item) {
     "use strict";
-    const { Backbone, utils, Strophe, $iq, b64_sha1, sizzle, _ } = converse.env;
+    const { Backbone, Strophe, $iq, b64_sha1, sizzle, _ } = converse.env;
+    const u = converse.env.utils;
 
 
     converse.plugins.add('converse-rosterview', {
@@ -246,14 +245,14 @@
                 },
 
                 show () {
-                    if (utils.isVisible(this.el)) { return this; }
+                    if (u.isVisible(this.el)) { return this; }
                     this.el.classList.add('fade-in');
                     this.el.classList.remove('hidden');
                     return this;
                 },
 
                 hide () {
-                    if (!utils.isVisible(this.el)) { return this; }
+                    if (!u.isVisible(this.el)) { return this; }
                     this.model.save({
                         'filter_text': '',
                         'chat_state': ''
@@ -278,22 +277,24 @@
                 id: 'converse-roster',
 
                 initialize () {
-                    _converse.roster.on("add", this.onContactAdd, this);
+                    _converse.roster.on("add", this.onContactAdded, this);
                     _converse.roster.on('change', this.onContactChange, this);
                     _converse.roster.on("destroy", this.update, this);
                     _converse.roster.on("remove", this.update, this);
-                    this.model.on("add", this.onGroupAdd, this);
+                    this.model.on("add", this.onGroupAdded, this);
                     this.model.on("reset", this.reset, this);
                     _converse.on('rosterGroupsFetched', this.positionFetchedGroups, this);
-                    _converse.on('rosterContactsFetched', this.update, this);
+                    _converse.on('rosterContactsFetched', () => {
+                        _converse.roster.each(this.onContactAdded.bind(this));
+                        this.update();
+                    });
                     this.createRosterFilter();
                 },
 
                 render () {
-                    this.renderRoster();
                     this.el.innerHTML = "";
                     this.el.appendChild(this.filter_view.render().el);
-
+                    this.renderRoster();
                     if (!_converse.allow_contact_requests) {
                         // XXX: if we ever support live editing of config then
                         // we'll need to be able to remove this class on the fly.
@@ -303,8 +304,10 @@
                 },
 
                 renderRoster () {
-                    this.$roster = $(tpl_roster());
-                    this.roster = this.$roster[0];
+                    const div = document.createElement('div');
+                    div.insertAdjacentHTML('beforeend', tpl_roster());
+                    this.roster_el = div.firstChild;
+                    this.el.insertAdjacentElement('beforeend', this.roster_el);
                 },
 
                 createRosterFilter () {
@@ -334,14 +337,14 @@
                 }, 100),
 
                 update: _.debounce(function () {
-                    if (_.isNull(this.roster.parentElement)) {
-                        this.$el.append(this.$roster.show());
+                    if (!u.isVisible(this.roster_el)) {
+                        u.showElement(this.roster_el);
                     }
                     return this.showHideFilter();
                 }, _converse.animate ? 100 : 0),
 
                 showHideFilter () {
-                    if (!utils.isVisible(this.el)) {
+                    if (!u.isVisible(this.el)) {
                         return;
                     }
                     this.filter_view.showOrHide();
@@ -361,9 +364,9 @@
                     if (type === 'groups') {
                         _.each(this.getAll(), function (view, idx) {
                             if (!_.includes(view.model.get('name').toLowerCase(), query.toLowerCase())) {
-                                view.hide();
+                                u.slideIn(view.el);
                             } else if (view.model.contacts.length > 0) {
-                                view.show();
+                                u.slideOut(view.el);
                             }
                         });
                     } else {
@@ -376,18 +379,17 @@
                 reset () {
                     _converse.roster.reset();
                     this.removeAll();
-                    this.renderRoster();
                     this.render().update();
                     return this;
                 },
 
-                onGroupAdd (group) {
+                onGroupAdded (group) {
                     const view = new _converse.RosterGroupView({model: group});
-                    this.add(group.get('name'), view.render());
-                    this.positionGroup(view);
+                    this.add(group.get('name'), view);
+                    this.positionGroup(group);
                 },
 
-                onContactAdd (contact) {
+                onContactAdded (contact) {
                     this.addRosterContact(contact).update();
                     this.updateFilter();
                 },
@@ -435,47 +437,28 @@
                      * positioned aren't already in inserted into the
                      * roster DOM element.
                      */
-                    const that = this;
                     this.model.sort();
-                    this.model.each(function (group, idx) {
-                        let view = that.get(group.get('name'));
-                        if (!view) {
-                            view = new _converse.RosterGroupView({model: group});
-                            that.add(group.get('name'), view.render());
-                        }
-                        if (idx === 0) {
-                            that.$roster.append(view.$el);
-                        } else {
-                            that.appendGroup(view);
-                        }
-                    });
+                    this.model.each(this.onGroupAdded.bind(this));
                 },
 
-                positionGroup (view) {
+                positionGroup (group) {
                     /* Place the group's DOM element in the correct alphabetical
                      * position amongst the other groups in the roster.
+                     *
+                     * NOTE: relies on the assumption that it will be called in
+                     * the right order of appearance of groups.
                      */
-                    const $groups = this.$roster.find('.roster-group'),
-                        index = $groups.length ? this.model.indexOf(view.model) : 0;
+                    const view = this.get(group.get('name'));
+                    view.render();
+                    const list = this.roster_el,
+                          index = this.model.indexOf(view.model);
                     if (index === 0) {
-                        this.$roster.prepend(view.$el);
+                        list.insertAdjacentElement('afterbegin', view.el);
                     } else if (index === (this.model.length-1)) {
-                        this.appendGroup(view);
+                        list.insertAdjacentElement('beforeend', view.el);
                     } else {
-                        $($groups.eq(index)).before(view.$el);
-                    }
-                    return this;
-                },
-
-                appendGroup (view) {
-                    /* Add the group at the bottom of the roster
-                     */
-                    const $last = this.$roster.find('.roster-group').last();
-                    const $siblings = $last.siblings('dd');
-                    if ($siblings.length > 0) {
-                        $siblings.last().after(view.$el);
-                    } else {
-                        $last.after(view.$el);
+                        const neighbour_el = list.querySelector('div:nth-child('+index+')');
+                        neighbour_el.insertAdjacentElement('afterend', view.el);
                     }
                     return this;
                 },
@@ -524,7 +507,8 @@
 
 
             _converse.RosterContactView = Backbone.View.extend({
-                tagName: 'dd',
+                tagName: 'li',
+                className: 'hidden',
 
                 events: {
                     "click .accept-xmpp-request": "acceptRequest",
@@ -543,7 +527,7 @@
                 render () {
                     const that = this;
                     if (!this.mayBeShown()) {
-                        this.$el.hide();
+                        u.hideElement(this.el);
                         return this;
                     }
                     const item = this.model,
@@ -564,7 +548,8 @@
                                 that.el.classList.remove(cls);
                             }
                         });
-                    this.$el.addClass(chat_status).data('status', chat_status);
+                    this.el.classList.add(chat_status);
+                    this.el.setAttribute('data-status', chat_status);
 
                     if ((ask === 'subscribe') || (subscription === 'from')) {
                         /* ask === 'subscribe'
@@ -579,21 +564,21 @@
                          *  So in both cases the user is a "pending" contact.
                          */
                         this.el.classList.add('pending-xmpp-contact');
-                        this.$el.html(tpl_pending_contact(
+                        this.el.innerHTML = tpl_pending_contact(
                             _.extend(item.toJSON(), {
                                 'desc_remove': __('Click to remove %1$s as a contact', item.get('fullname')),
                                 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts
                             })
-                        ));
+                        );
                     } else if (requesting === true) {
                         this.el.classList.add('requesting-xmpp-contact');
-                        this.$el.html(tpl_requesting_contact(
+                        this.el.innerHTML = tpl_requesting_contact(
                             _.extend(item.toJSON(), {
                                 'desc_accept': __("Click to accept the contact request from %1$s", item.get('fullname')),
                                 'desc_decline': __("Click to decline the contact request from %1$s", item.get('fullname')),
                                 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts
                             })
-                        ));
+                        );
                     } else if (subscription === 'both' || subscription === 'to') {
                         this.el.classList.add('current-xmpp-contact');
                         this.el.classList.remove(_.without(['both', 'to'], subscription)[0]);
@@ -604,44 +589,25 @@
                 },
 
                 renderRosterItem (item) {
-                    const chat_status = item.get('chat_status');
-                    this.$el.html(tpl_roster_item(
+                    this.el.innerHTML = tpl_roster_item(
                         _.extend(item.toJSON(), {
-                            'desc_status': STATUSES[chat_status||'offline'],
+                            'desc_status': STATUSES[item.get('chat_status')||'offline'],
                             'desc_chat': __('Click to chat with this contact'),
                             'desc_remove': __('Click to remove %1$s as a contact', item.get('fullname')),
                             'title_fullname': __('Name'),
                             'allow_contact_removal': _converse.allow_contact_removal,
                             'num_unread': item.get('num_unread') || 0
                         })
-                    ));
+                    );
                     return this;
                 },
 
-                isGroupCollapsed () {
-                    /* Check whether the group in which this contact appears is
-                     * collapsed.
-                     */
-                    // XXX: this sucks and is fragile.
-                    // It's because I tried to do the "right thing"
-                    // and use definition lists to represent roster groups.
-                    // If roster group items were inside the group elements, we
-                    // would simplify things by not having to check whether the
-                    // group is collapsed or not.
-                    const name = this.$el.prevAll('dt:first').data('group');
-                    const group = _.head(_converse.rosterview.model.where({'name': name.toString()}));
-                    if (group.get('state') === _converse.CLOSED) {
-                        return true;
-                    }
-                    return false;
-                },
-
                 mayBeShown () {
                     /* Return 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 (see isGroupCollapsed).
+                     * the group it's in is collapsed.
                      */
                     const chatStatus = this.model.get('chat_status');
                     if ((_converse.show_only_online_users && chatStatus !== 'online') ||
@@ -705,19 +671,17 @@
 
 
             _converse.RosterGroupView = Backbone.Overview.extend({
-                tagName: 'dt',
+                tagName: 'div',
                 className: 'roster-group',
                 events: {
                     "click a.group-toggle": "toggle"
                 },
 
                 initialize () {
-                    this.model.contacts.on("add", this.addContact, this);
+                    this.model.contacts.on("add", this.onContactAdded, this);
                     this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this);
                     this.model.contacts.on("change:requesting", this.onContactRequestChange, this);
                     this.model.contacts.on("change:chat_status", function (contact) {
-                        // This might be optimized by instead of first sorting,
-                        // finding the correct position in positionContact
                         this.model.contacts.sort();
                         this.positionContact(contact).render();
                     }, this);
@@ -728,25 +692,27 @@
 
                 render () {
                     this.el.setAttribute('data-group', this.model.get('name'));
-                    const html = tpl_group_header({
-                        label_group: this.model.get('name'),
-                        desc_group_toggle: this.model.get('description'),
-                        toggle_state: this.model.get('state')
+                    this.el.innerHTML = tpl_group_header({
+                        'label_group': this.model.get('name'),
+                        'desc_group_toggle': this.model.get('description'),
+                        'toggle_state': this.model.get('state'),
+                        '_converse': _converse
                     });
-                    this.el.innerHTML = html;
+                    this.contacts_el = this.el.querySelector('.roster-group-contacts');
                     return this;
                 },
 
-                addContact (contact) {
-                    let view = new _converse.RosterContactView({model: contact});
-                    this.add(contact.get('id'), view);
-                    view = this.positionContact(contact).render();
-                    if (view.mayBeShown()) {
+                onContactAdded (contact) {
+                    let contact_view = new _converse.RosterContactView({model: contact});
+                    this.add(contact.get('id'), contact_view);
+                    contact_view = this.positionContact(contact).render();
+                    if (contact_view.mayBeShown()) {
                         if (this.model.get('state') === _converse.CLOSED) {
-                            if (view.$el[0].style.display !== "none") { view.$el.hide(); }
-                            if (!this.$el.is(':visible')) { this.$el.show(); }
+                            u.hideElement(contact_view.el);
+                            u.showElement(this.el);
                         } else {
-                            if (this.$el[0].style.display !== "block") { this.show(); }
+                            u.showElement(contact_view.el);
+                            u.showElement(this.el);
                         }
                     }
                 },
@@ -756,112 +722,118 @@
                      * position amongst the other contacts in this group.
                      */
                     const view = this.get(contact.get('id'));
+                    view.render();
+                    const list = this.contacts_el;
                     const index = this.model.contacts.indexOf(contact);
-                    view.$el.detach();
                     if (index === 0) {
-                        this.$el.after(view.$el);
+                        list.insertAdjacentElement('afterbegin', view.el);
                     } else if (index === (this.model.contacts.length-1)) {
-                        this.$el.nextUntil('dt').last().after(view.$el);
+                        list.insertAdjacentElement('beforeend', view.el);
                     } else {
-                        this.$el.nextUntil('dt').eq(index).before(view.$el);
+                        const neighbour_el = list.querySelector('li:nth-child('+index+')');
+                        neighbour_el.insertAdjacentElement('afterend', view.el);
                     }
                     return view;
                 },
 
                 show () {
-                    this.$el.show();
-                    _.each(this.getAll(), function (view) {
-                        if (view.mayBeShown() && !view.isGroupCollapsed()) {
-                            view.$el.show();
+                    u.showElement(this.el);
+                    _.each(this.getAll(), (contact_view) => {
+                        if (contact_view.mayBeShown() && this.model.get('state') === _converse.OPENED) {
+                            u.showElement(contact_view.el);
                         }
                     });
                     return this;
                 },
 
-                hide () {
-                    this.$el.nextUntil('dt').addBack().hide();
+                collapse () {
+                    return u.slideIn(this.contacts_el);
                 },
 
-                filter (q, type) {
-                    /* Filter the group's contacts based on the query "q".
-                     * The query is matched against the contact's full name.
-                     * If all contacts are filtered out (i.e. hidden), then the
-                     * group must be filtered out as well.
+                filterOutContacts (contacts=[]) {
+                    /* 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.
                      */
-                    let matches;
-                    if (q.length === 0) {
-                        if (this.model.get('state') === _converse.OPENED) {
-                            this.model.contacts.each(
-                                (item) => {
-                                    const view = this.get(item.get('id'));
-                                    if (view.mayBeShown() && !view.isGroupCollapsed()) {
-                                        view.$el.show();
-                                    }
-                                }
-                            );
+                    let shown = 0;
+                    const all_contact_views = this.getAll();
+                    _.each(this.model.contacts.models, (contact) => {
+                        const contact_view = this.get(contact.get('id'));
+                        if (_.includes(contacts, contact)) {
+                            u.hideElement(contact_view.el);
+                        } else if (contact_view.mayBeShown()) {
+                            u.showElement(contact_view.el);
+                            shown += 1;
                         }
-                        this.showIfNecessary();
+                    });
+                    if (shown) {
+                        u.showElement(this.el);
                     } else {
-                        q = q.toLowerCase();
-                        if (type === 'state') {
-                            if (this.model.get('name') === HEADER_REQUESTING_CONTACTS) {
-                                // When filtering by chat state, we still want to
-                                // show requesting contacts, even though they don't
-                                // have the state in question.
-                                matches = this.model.contacts.filter(
-                                    (contact) => utils.contains.not('chat_status', q)(contact) && !contact.get('requesting')
-                                );
-                            } else if (q === 'unread_messages') {
-                                matches = this.model.contacts.filter({'num_unread': 0});
-                            } else {
-                                matches = this.model.contacts.filter(
-                                    utils.contains.not('chat_status', q)
-                                );
-                            }
-                        } else  {
+                        u.hideElement(this.el);
+                    }
+                },
+
+                getFilterMatches (q, type) {
+                    /* Given the filter query "q" and the filter type "type",
+                     * return a list of contacts that need to be filtered out.
+                     */
+                    if (q.length === 0) {
+                        return [];
+                    }
+                    let matches;
+                    q = q.toLowerCase();
+                    if (type === 'state') {
+                        if (this.model.get('name') === HEADER_REQUESTING_CONTACTS) {
+                            // When filtering by chat state, we still want to
+                            // show requesting contacts, even though they don't
+                            // have the state in question.
                             matches = this.model.contacts.filter(
-                                utils.contains.not('fullname', q)
+                                (contact) => u.contains.not('chat_status', q)(contact) && !contact.get('requesting')
                             );
-                        }
-                        if (matches.length === this.model.contacts.length) {
-                            // hide the whole group
-                            this.hide();
+                        } else if (q === 'unread_messages') {
+                            matches = this.model.contacts.filter({'num_unread': 0});
                         } else {
-                            _.each(matches, (item) => {
-                                this.get(item.get('id')).$el.hide();
-                            });
-                            if (this.model.get('state') === _converse.OPENED) {
-                                _.each(this.model.contacts.reject(
-                                    utils.contains.not('fullname', q)),
-                                    (item) => {
-                                        this.get(item.get('id')).$el.show();
-                                    });
-                            }
-                            this.showIfNecessary();
+                            matches = this.model.contacts.filter(
+                                u.contains.not('chat_status', q)
+                            );
                         }
+                    } else  {
+                        matches = this.model.contacts.filter(
+                            u.contains.not('fullname', q)
+                        );
                     }
+                    return matches;
                 },
 
-                showIfNecessary () {
-                    if (!this.$el.is(':visible') && this.model.contacts.length > 0) {
-                        this.$el.show();
-                    }
+                filter (q, type) {
+                    /* Filter the group's contacts based on the query "q".
+                     * The query is matched against the contact's full name.
+                     * If all contacts are filtered out (i.e. hidden), then the
+                     * group must be filtered out as well.
+                     */
+                    this.filterOutContacts(this.getFilterMatches(q, type));
                 },
 
                 toggle (ev) {
                     if (ev && ev.preventDefault) { ev.preventDefault(); }
-                    const $el = $(ev.target);
-                    if ($el.hasClass("icon-opened")) {
-                        this.$el.nextUntil('dt').slideUp();
+                    if (_.includes(ev.target.classList, "icon-opened")) {
                         this.model.save({state: _converse.CLOSED});
-                        $el.removeClass("icon-opened").addClass("icon-closed");
+                        this.collapse().then(() => {
+                            ev.target.classList.remove("icon-opened");
+                            ev.target.classList.add("icon-closed");
+                        });
                     } else {
-                        $el.removeClass("icon-closed").addClass("icon-opened");
+                        ev.target.classList.remove("icon-closed");
+                        ev.target.classList.add("icon-opened");
                         this.model.save({state: _converse.OPENED});
                         this.filter(
-                            _converse.rosterview.$('.roster-filter').val() || '',
-                            _converse.rosterview.$('.filter-type').val()
+                            _converse.rosterview.el.querySelector('.roster-filter').value,
+                            _converse.rosterview.el.querySelector('.filter-type').value
                         );
+                        u.showElement(this.el);
+                        u.slideOut(this.contacts_el);
                     }
                 },
 
@@ -872,7 +844,7 @@
                     if (in_this_group && !in_this_overview) {
                         this.model.contacts.remove(cid);
                     } else if (!in_this_group && in_this_overview) {
-                        this.addContact(contact);
+                        this.onContactAdded(contact);
                     }
                 },
 
@@ -899,7 +871,7 @@
                 onRemove (contact) {
                     this.remove(contact.get('id'));
                     if (this.model.contacts.length === 0) {
-                        this.$el.hide();
+                        u.hideElement(this.el);
                     }
                 }
             });
@@ -931,7 +903,7 @@
                     return; // The message has no text
                 }
                 if (chatbox.get('type') !== 'chatroom' &&
-                    utils.isNewMessage(data.stanza) &&
+                    u.isNewMessage(data.stanza) &&
                     chatbox.newMessageWillBeHidden()) {
 
                     const contact = _.head(_converse.roster.where({'jid': chatbox.get('jid')}));

+ 1 - 0
src/templates/group_header.html

@@ -1 +1,2 @@
 <a href="#" class="group-toggle icon-{{{o.toggle_state}}}" title="{{{o.desc_group_toggle}}}">{{{o.label_group}}}</a>
+<ul class="roster-group-contacts {[ if (o.toggle_state === o._converse.CLOSED) { ]} collapsed {[ } ]}"></ul>

+ 1 - 1
src/templates/roster.html

@@ -1 +1 @@
-<dl class="roster-contacts"></dl>
+<div class="roster-contacts"></div>