浏览代码

Work on turning chat views into custom elements

The eventual goal is to avoid UI-related stanza processing if the relevant chats
aren't in the DOM.

With the current architecture, chatboxes are created (and the stanzas
related to them processed) even if `#conversejs` isn't in the DOM.

* Initial work on making controlbox an element
* Create a shared base class
* Ceate ChatBoxViews proxy
* Update sass now that certain classes are moved to converse-chats element
JC Brand 4 年之前
父节点
当前提交
1949356ede
共有 84 个文件被更改,包括 2263 次插入2200 次删除
  1. 3 0
      CHANGES.md
  2. 19 19
      package-lock.json
  3. 15 13
      sass/_autocomplete.scss
  4. 12 12
      sass/_chatbox.scss
  5. 94 92
      sass/_chatrooms.scss
  6. 171 170
      sass/_controlbox.scss
  7. 25 25
      sass/_core.scss
  8. 52 52
      sass/_emoji.scss
  9. 11 8
      sass/_messages.scss
  10. 78 76
      sass/_minimized_chats.scss
  11. 15 13
      sass/_toolbar.scss
  12. 41 41
      spec/autocomplete.js
  13. 26 26
      spec/bookmarks.js
  14. 51 51
      spec/chatbox.js
  15. 20 20
      spec/controlbox.js
  16. 2 2
      spec/converse.js
  17. 75 75
      spec/corrections.js
  18. 47 47
      spec/emojis.js
  19. 5 5
      spec/hats.js
  20. 9 9
      spec/headline.js
  21. 20 20
      spec/http-file-upload.js
  22. 12 12
      spec/login.js
  23. 2 2
      spec/mam.js
  24. 13 13
      spec/markers.js
  25. 22 22
      spec/me-messages.js
  26. 14 14
      spec/mentions.js
  27. 43 43
      spec/messages.js
  28. 18 18
      spec/minchats.js
  29. 3 3
      spec/mock.js
  30. 3 3
      spec/modtools.js
  31. 1 1
      spec/muc-mentions.js
  32. 145 145
      spec/muc.js
  33. 27 27
      spec/muc_messages.js
  34. 27 24
      spec/muclist.js
  35. 4 4
      spec/notification.js
  36. 24 24
      spec/omemo.js
  37. 2 2
      spec/presence.js
  38. 1 1
      spec/protocol.js
  39. 2 2
      spec/rai.js
  40. 3 3
      spec/receipts.js
  41. 38 38
      spec/register.js
  42. 69 69
      spec/retractions.js
  43. 1 1
      spec/room_registration.js
  44. 27 27
      spec/roster.js
  45. 23 23
      spec/spoilers.js
  46. 29 29
      spec/styling.js
  47. 6 5
      spec/user-details-modal.js
  48. 3 3
      spec/xss.js
  49. 5 5
      src/components/converse.js
  50. 4 2
      src/converse.js
  51. 1 1
      src/headless/log.js
  52. 2 2
      src/modals/base.js
  53. 1 1
      src/plugins/bookmark-views/mixins.js
  54. 45 0
      src/plugins/chatboxviews/container.js
  55. 8 14
      src/plugins/chatboxviews/index.js
  56. 25 0
      src/plugins/chatboxviews/templates/chats.js
  57. 22 58
      src/plugins/chatboxviews/view.js
  58. 0 21
      src/plugins/chatview/index.js
  59. 71 455
      src/plugins/chatview/view.js
  60. 2 20
      src/plugins/controlbox/index.js
  61. 6 2
      src/plugins/controlbox/templates/controlbox.js
  62. 3 3
      src/plugins/controlbox/templates/toggle.js
  63. 21 28
      src/plugins/controlbox/toggle.js
  64. 30 51
      src/plugins/controlbox/view.js
  65. 2 0
      src/plugins/dragresize/index.js
  66. 8 8
      src/plugins/dragresize/mixin.js
  67. 2 2
      src/plugins/dragresize/utils.js
  68. 2 2
      src/plugins/headlines-view/index.js
  69. 14 19
      src/plugins/headlines-view/panel.js
  70. 6 0
      src/plugins/headlines-view/view.js
  71. 2 1
      src/plugins/minimize/index.js
  72. 0 1
      src/plugins/minimize/mixins.js
  73. 7 11
      src/plugins/minimize/utils.js
  74. 11 16
      src/plugins/muc-views/index.js
  75. 120 117
      src/plugins/muc-views/muc.js
  76. 1 1
      src/plugins/muc-views/rooms-panel.js
  77. 9 9
      src/plugins/omemo.js
  78. 2 1
      src/plugins/register/controlbox-mixin.js
  79. 31 0
      src/plugins/rootview/index.js
  80. 31 0
      src/plugins/rootview/view.js
  81. 4 4
      src/plugins/rosterview/rosterview.js
  82. 391 0
      src/shared/chatview.js
  83. 10 4
      src/templates/avatar.js
  84. 11 7
      src/templates/converse.js

+ 3 - 0
CHANGES.md

@@ -24,6 +24,9 @@ The [afterMessageBodyTransformed](https://conversejs.org/docs/html/api/-_convers
 When leaving a MUC, the message history is deleted. This means that decrypted
 OMEMO messages are gone and cannot be recovered on that device. See [muc_clear_messages_on_leave](https://conversejs.org/docs/html/configuration.html#muc-clear-messages-on-leave).
 
+Removed events:
+* `chatBoxInsertedIntoDOM`
+
 ## 7.0.2 (2020-11-23)
 
 - Updated translations: de, nb, gl, tr

+ 19 - 19
package-lock.json

@@ -4483,9 +4483,9 @@
 			}
 		},
 		"@octokit/openapi-types": {
-			"version": "2.0.0",
-			"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.0.tgz",
-			"integrity": "sha512-J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnPw==",
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.1.tgz",
+			"integrity": "sha512-9AuC04PUnZrjoLiw3uPtwGh9FE4Q3rTqs51oNlQ0rkwgE8ftYsOC+lsrQyvCvWm85smBbSc0FNRKKumvGyb44Q==",
 			"dev": true
 		},
 		"@octokit/plugin-enterprise-rest": {
@@ -4629,12 +4629,12 @@
 			}
 		},
 		"@octokit/types": {
-			"version": "6.1.1",
-			"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.1.tgz",
-			"integrity": "sha512-btm3D6S7VkRrgyYF31etUtVY/eQ1KzrNRqhFt25KSe2mKlXuLXJilglRC6eDA2P6ou94BUnk/Kz5MPEolXgoiw==",
+			"version": "6.1.2",
+			"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.2.tgz",
+			"integrity": "sha512-LPCpcLbcky7fWfHCTuc7tMiSHFpFlrThJqVdaHgowBTMS0ijlZFfonQC/C1PrZOjD4xRCYgBqH9yttEATGE/nw==",
 			"dev": true,
 			"requires": {
-				"@octokit/openapi-types": "^2.0.0",
+				"@octokit/openapi-types": "^2.0.1",
 				"@types/node": ">= 8"
 			}
 		},
@@ -7099,9 +7099,9 @@
 			"dev": true
 		},
 		"conventional-changelog-writer": {
-			"version": "4.0.18",
-			"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.18.tgz",
-			"integrity": "sha512-mAQDCKyB9HsE8Ko5cCM1Jn1AWxXPYV0v8dFPabZRkvsiWUul2YyAqbIaoMKF88Zf2ffnOPSvKhboLf3fnjo5/A==",
+			"version": "4.1.0",
+			"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz",
+			"integrity": "sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw==",
 			"dev": true,
 			"requires": {
 				"compare-func": "^2.0.0",
@@ -11272,9 +11272,9 @@
 			}
 		},
 		"git-url-parse": {
-			"version": "11.4.0",
-			"resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.0.tgz",
-			"integrity": "sha512-KlIa5jvMYLjXMQXkqpFzobsyD/V2K5DRHl5OAf+6oDFPlPLxrGDVQlIdI63c4/Kt6kai4kALENSALlzTGST3GQ==",
+			"version": "11.4.3",
+			"resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.3.tgz",
+			"integrity": "sha512-LZTTk0nqJnKN48YRtOpR8H5SEfp1oM2tls90NuZmBxN95PnCvmuXGzqQ4QmVirBgKx2KPYfPGteX3/raWjKenQ==",
 			"dev": true,
 			"requires": {
 				"git-up": "^4.0.0"
@@ -13709,9 +13709,9 @@
 			}
 		},
 		"meow": {
-			"version": "8.0.0",
-			"resolved": "https://registry.npmjs.org/meow/-/meow-8.0.0.tgz",
-			"integrity": "sha512-nbsTRz2fwniJBFgUkcdISq8y/q9n9VbiHYbfwklFh5V4V2uAcxtKQkDc0yCLPM/kP0d+inZBewn3zJqewHE7kg==",
+			"version": "8.1.0",
+			"resolved": "https://registry.npmjs.org/meow/-/meow-8.1.0.tgz",
+			"integrity": "sha512-fNWkgM1UVMey2kf24yLiccxLihc5W+6zVus3/N0b+VfnJgxV99E9u04X6NAiKdg6ED7DAQBX5sy36NM0QJZkWA==",
 			"dev": true,
 			"requires": {
 				"@types/minimist": "^1.2.0",
@@ -22460,9 +22460,9 @@
 			},
 			"dependencies": {
 				"ws": {
-					"version": "7.4.1",
-					"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
-					"integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==",
+					"version": "7.4.2",
+					"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
+					"integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==",
 					"optional": true
 				}
 			}

+ 15 - 13
sass/_autocomplete.scss

@@ -100,7 +100,7 @@
             transition: .3s cubic-bezier(.4,.2,.5,1.4);
             transform-origin: 1.43em -.43em;
         }
-        
+
         .suggestion-box > ul[hidden],
         .suggestion-box > ul:empty {
             opacity: 0;
@@ -109,31 +109,33 @@
             transition-timing-function: ease;
         }
     }
-    
+
     .suggestion-box > ul > li[aria-selected="true"] {
         background: var(--completion-dark-color);
         color: var(--inverse-link-color);
     }
-    
+
     .suggestion-box li:hover mark {
         background: var(--completion-light-color);
         color: var(--inverse-link-color);
     }
-    
+
     .suggestion-box li[aria-selected="true"] mark {
         background: var(--completion-normal-color);
         color: inherit;
     }
-}
 
-#conversejs.converse-fullscreen {
-    .suggestion-box__results--above {
-        bottom: 4.5em;
-    }
-}
+    converse-chats {
+        .converse-fullscreen {
+            .suggestion-box__results--above {
+                bottom: 4.5em;
+            }
+        }
 
-#conversejs.converse-overlayed {
-    .suggestion-box__results--above {
-        bottom: 3.5em;
+        .converse-overlayed {
+            .suggestion-box__results--above {
+                bottom: 3.5em;
+            }
+        }
     }
 }

+ 12 - 12
sass/_chatbox.scss

@@ -396,8 +396,8 @@
 
 /* ******************* Overlay and embedded styles *************************** */
 
-#conversejs.converse-embedded,
-#conversejs.converse-overlayed {
+converse-chats.converse-embedded,
+converse-chats.converse-overlayed {
     .controlbox-head {
         padding: 0.5em;
     }
@@ -416,7 +416,7 @@
     }
 }
 
-#conversejs.converse-overlayed  {
+converse-chats.converse-overlayed  {
 
     .chat-head, .box-flyout {
         border-top-left-radius: var(--chatbox-border-radius);
@@ -467,7 +467,7 @@
 }
 
 @include media-breakpoint-down(sm) {
-    #conversejs.converse-overlayed {
+    converse-chats.converse-overlayed {
         > .row {
             flex-direction: column;
             &.no-gutters {
@@ -478,8 +478,8 @@
 }
 
 
-#conversejs.converse-embedded,
-#conversejs.converse-fullscreen  {
+converse-chats.converse-embedded,
+converse-chats.converse-fullscreen  {
     .flyout {
         border-radius: 0;
         border:none;
@@ -527,7 +527,7 @@
     }
 }
 
-#conversejs.converse-embedded {
+converse-chats.converse-embedded {
     .chat-head {
         font-size: var(--font-size-huge);
     }
@@ -558,7 +558,7 @@
 
 /* ******************* Fullpage styles *************************** */
 
-#conversejs.converse-fullscreen  {
+converse-chats.converse-fullscreen  {
     .chatbox-btn {
         font-size: var(--fullpage-chatbox-button-size);
         margin: 0 0.3em;
@@ -595,7 +595,7 @@
 }
 
 @include media-breakpoint-down(sm) {
-    #conversejs:not(.converse-embedded)  {
+    converse-chats:not(.converse-embedded)  {
         > .row {
             flex-direction: row-reverse;
         }
@@ -617,9 +617,9 @@
         }
     }
 
-    #conversejs.converse-mobile,
-    #conversejs.converse-overlayed,
-    #conversejs.converse-fullscreen {
+    converse-chats.converse-mobile,
+    converse-chats.converse-overlayed,
+    converse-chats.converse-fullscreen {
         .chatbox {
             .box-flyout {
                 .chatbox-navback {

+ 94 - 92
sass/_chatrooms.scss

@@ -396,126 +396,128 @@
 
 /* ******************* Overlay  styles *************************** */
 
-#conversejs.converse-overlayed {
-    .chatbox {
-        &.chatroom {
-            min-width: var(--chatroom-width) !important;
-            width: var(--chatroom-width);
-            .box-flyout {
+converse-chats {
+    .converse-overlayed {
+        .chatbox {
+            &.chatroom {
                 min-width: var(--chatroom-width) !important;
                 width: var(--chatroom-width);
-            }
-            .chatbox-title__text {
-                @include make-col(7);
-            }
-            .chatbox-title__buttons {
-                @include make-col(5);
-            }
+                .box-flyout {
+                    min-width: var(--chatroom-width) !important;
+                    width: var(--chatroom-width);
+                }
+                .chatbox-title__text {
+                    @include make-col(7);
+                }
+                .chatbox-title__buttons {
+                    @include make-col(5);
+                }
 
-            .chat-head__desc {
-                font-size: 80%;
-                margin-bottom: 1em;
-            }
-            .chatroom-body {
-                .occupants {
-                    .occupants-heading {
-                        padding: 0;
-                    }
-                    .occupant-list {
-                        border-bottom: none;
-                    }
-                    ul {
-                        .occupant {
-                            .occupant-nick-badge {
-                                .occupant-badges {
-                                    display: none;
+                .chat-head__desc {
+                    font-size: 80%;
+                    margin-bottom: 1em;
+                }
+                .chatroom-body {
+                    .occupants {
+                        .occupants-heading {
+                            padding: 0;
+                        }
+                        .occupant-list {
+                            border-bottom: none;
+                        }
+                        ul {
+                            .occupant {
+                                .occupant-nick-badge {
+                                    .occupant-badges {
+                                        display: none;
+                                    }
+                                }
+                                .occupant-status {
+                                    margin-top: 6px;
                                 }
-                            }
-                            .occupant-status {
-                                margin-top: 6px;
                             }
                         }
                     }
-                }
-                .chat-area {
-                    min-width: var(--overlayed-chat-width);
+                    .chat-area {
+                        min-width: var(--overlayed-chat-width);
+                    }
                 }
             }
         }
     }
-}
 
-#conversejs.converse-embedded,
-#conversejs.converse-fullscreen,
-#conversejs.converse-mobile {
+    .converse-embedded,
+    .converse-fullscreen,
+    .converse-mobile {
 
-    .chatroom {
-        .box-flyout {
-            width: 100%;
+        .chatroom {
+            .box-flyout {
+                width: 100%;
 
-            .chatroom-body {
-                .chat-area {
-                    &.full {
-                        .new-msgs-indicator {
-                            max-width: 100%;
+                .chatroom-body {
+                    .chat-area {
+                        &.full {
+                            .new-msgs-indicator {
+                                max-width: 100%;
+                            }
                         }
                     }
-                }
-                .occupants {
-                    padding: var(--occupants-padding);
-                    .occupants-heading {
-                        font-size: var(--font-size-large);
-                    }
-                    ul {
-                        &.occupant-list {
-                            li {
-                                font-size: var(--font-size-small);
+                    .occupants {
+                        padding: var(--occupants-padding);
+                        .occupants-heading {
+                            font-size: var(--font-size-large);
+                        }
+                        ul {
+                            &.occupant-list {
+                                li {
+                                    font-size: var(--font-size-small);
+                                }
                             }
                         }
                     }
                 }
             }
-        }
-        .room-invite {
-            span {
-                .invited-contact {
-                    margin: 0 0 0.5em -1px;
+            .room-invite {
+                span {
+                    .invited-contact {
+                        margin: 0 0 0.5em -1px;
+                    }
                 }
             }
         }
     }
-}
 
-#conversejs.converse-embedded {
-    .chatroom {
-        margin: 0;
-        width: 100%;
-        .box-flyout {
-            .occupants-heading {
-                font-size: 120%;
-            }
-            .chat-content {
-                .chat-message {
-                    margin: 0.5em;
+    .converse-embedded {
+        .chatroom {
+            margin: 0;
+            width: 100%;
+            .box-flyout {
+                .occupants-heading {
                     font-size: 120%;
                 }
-            }
-            .sendXMPPMessage {
-                .chat-textarea {
-                    padding: 0.5em;
-                    font-size: 110%;
+                .chat-content {
+                    .chat-message {
+                        margin: 0.5em;
+                        font-size: 120%;
+                    }
                 }
-            }
-            .chatroom-body {
-                height: 100%;
-                .chatroom-form-container {
+                .sendXMPPMessage {
+                    .chat-textarea {
+                        padding: 0.5em;
+                        font-size: 110%;
+                    }
+                }
+                .chatroom-body {
                     height: 100%;
-                    position: relative;
+                    .chatroom-form-container {
+                        height: 100%;
+                        position: relative;
+                    }
                 }
-            }
-            .occupants {
-                .occupant-list {
-                    padding-left: 0.3em;
+                .occupants {
+                    .occupant-list {
+                        padding-left: 0.3em;
+                    }
                 }
             }
         }
@@ -524,9 +526,9 @@
 
 
 @include media-breakpoint-down(sm) {
-    #conversejs.converse-mobile,
-    #conversejs.converse-overlayed,
-    #conversejs.converse-fullscreen {
+    converse-chats.converse-mobile,
+    converse-chats.converse-overlayed,
+    converse-chats.converse-fullscreen {
         .chatbox {
             .box-flyout {
                 .chat-head-chatroom {

+ 171 - 170
sass/_controlbox.scss

@@ -384,192 +384,218 @@
     }
 }
 
-
-#conversejs.converse-overlayed {
-    .toggle-controlbox {
-        order: -1;
-        text-align: center;
-        background-color: var(--link-color);
-        border-top-left-radius: var(--button-border-radius);
-        border-top-right-radius: var(--button-border-radius);
-        color: #0a0a0a;
-        float: right;
-        height: 100%;
-        margin: 0 var(--chat-gutter);
-        padding: 1em;
-        span {
-            color: var(--inverse-link-color);
+converse-chats {
+    .converse-overlayed {
+        .toggle-controlbox {
+            order: -1;
+            text-align: center;
+            background-color: var(--link-color);
+            border-top-left-radius: var(--button-border-radius);
+            border-top-right-radius: var(--button-border-radius);
+            color: #0a0a0a;
+            float: right;
+            height: 100%;
+            margin: 0 var(--chat-gutter);
+            padding: 1em;
+            span {
+                color: var(--inverse-link-color);
+            }
         }
-    }
 
-    #controlbox {
-        min-width: var(--controlbox-width) !important;
-        width: var(--controlbox-width);
-        .box-flyout {
+        #controlbox {
             min-width: var(--controlbox-width) !important;
             width: var(--controlbox-width);
-        }
+            .box-flyout {
+                min-width: var(--controlbox-width) !important;
+                width: var(--controlbox-width);
+            }
 
-        .login-trusted {
-            white-space: nowrap;
-            font-size: 90%;
-        }
+            .login-trusted {
+                white-space: nowrap;
+                font-size: 90%;
+            }
 
-        #converse-login-trusted {
-            margin-top: 0.5em;
-        }
-        &:not(.logged-out) {
-            .controlbox-head {
-                height: 15px;
+            #converse-login-trusted {
+                margin-top: 0.5em;
+            }
+            &:not(.logged-out) {
+                .controlbox-head {
+                    height: 15px;
+                }
             }
-        }
 
-        .controlbox-head {
-            display: flex;
-            flex-direction: row-reverse;
-            flex-wrap: nowrap;
-            justify-content: space-between;
-            min-height: 0;
+            .controlbox-head {
+                display: flex;
+                flex-direction: row-reverse;
+                flex-wrap: nowrap;
+                justify-content: space-between;
+                min-height: 0;
 
-            .brand-heading {
-                color: var(--controlbox-text-color);
-                font-size: 2em;
-            }
-            .chatbox-btn {
-                color: var(--controlbox-head-color);
-                margin: 0;
+                .brand-heading {
+                    color: var(--controlbox-text-color);
+                    font-size: 2em;
+                }
+                .chatbox-btn {
+                    color: var(--controlbox-head-color);
+                    margin: 0;
+                }
             }
-        }
 
-        #converse-register, #converse-login {
-            @include make-col(12);
-            padding-bottom: 0;
-        }
+            #converse-register, #converse-login {
+                @include make-col(12);
+                padding-bottom: 0;
+            }
 
-        #converse-register {
-            .button-cancel {
-                font-size: 90%;
+            #converse-register {
+                .button-cancel {
+                    font-size: 90%;
+                }
             }
-        }
 
-        .controlbox-panes {
-            border-radius: var(--chatbox-border-radius);
+            .controlbox-panes {
+                border-radius: var(--chatbox-border-radius);
+            }
         }
     }
-}
-
-#conversejs.converse-embedded,
-#conversejs.converse-fullscreen{
-    .toggle-controlbox {
-        display: none;
-    }
-}
 
-#conversejs.converse-embedded,
-#conversejs.converse-fullscreen,
-#conversejs.converse-mobile {
-    #controlbox {
-        @include make-col-ready();
-
-        @include media-breakpoint-up(md) {
-            @include make-col(4);
-        }
-        @include media-breakpoint-up(lg) {
-            @include make-col(3);
-        }
-        @include media-breakpoint-up(xl) {
-            @include make-col(2);
+    .converse-embedded,
+    .converse-fullscreen{
+        .toggle-controlbox {
+            display: none;
         }
+    }
 
-        &.logged-out {
-            @include make-col(12);
-        }
+    .converse-embedded,
+    .converse-fullscreen,
+    .converse-mobile {
+        #controlbox {
+            @include make-col-ready();
 
-        margin: 0;
+            @include media-breakpoint-up(md) {
+                @include make-col(4);
+            }
+            @include media-breakpoint-up(lg) {
+                @include make-col(3);
+            }
+            @include media-breakpoint-up(xl) {
+                @include make-col(2);
+            }
 
-        .controlbox-pane {
-            border-radius: 0;
-        }
+            &.logged-out {
+                @include make-col(12);
+            }
 
-        .flyout {
-            border-radius: 0;
-        }
+            margin: 0;
 
-        #converse-login-panel {
-            border-radius: 0;
-            .converse-form {
-                padding: 3em 2em 3em;
+            .controlbox-pane {
+                border-radius: 0;
             }
-        }
 
-        .toggle-register-login {
-            line-height: var(--line-height-huge);
-        }
+            .flyout {
+                border-radius: 0;
+            }
 
-        converse-brand-logo {
-            @include make-col(12);
-            margin-top: 5em;
-            margin-bottom: 1em;
-            .brand-heading {
-                width: 100%;
-                font-size: 500%;
-                padding: 0.7em 0 0 0;
-                opacity: 0.8;
-                color: var(--brand-heading-color);
+            #converse-login-panel {
+                border-radius: 0;
+                .converse-form {
+                    padding: 3em 2em 3em;
+                }
             }
-            .brand-subtitle {
-                font-size: 90%;
-                padding: 0.5em;
+
+            .toggle-register-login {
+                line-height: var(--line-height-huge);
             }
-            @media screen and (max-width: $mobile-portrait-length) {
+
+            converse-brand-logo {
+                @include make-col(12);
+                margin-top: 5em;
+                margin-bottom: 1em;
                 .brand-heading {
-                    font-size: 300%;
+                    width: 100%;
+                    font-size: 500%;
+                    padding: 0.7em 0 0 0;
+                    opacity: 0.8;
+                    color: var(--brand-heading-color);
+                }
+                .brand-subtitle {
+                    font-size: 90%;
+                    padding: 0.5em;
+                }
+                @media screen and (max-width: $mobile-portrait-length) {
+                    .brand-heading {
+                        font-size: 300%;
+                    }
                 }
             }
-        }
 
-        &.logged-out {
-            @include make-col(12);
-            @include fade-in;
-            width: 100%;
-            .box-flyout {
+            &.logged-out {
+                @include make-col(12);
+                @include fade-in;
                 width: 100%;
+                .box-flyout {
+                    width: 100%;
+                }
             }
-        }
-        .box-flyout {
-            border: 0;
-            width: 100%;
-            z-index: 1;
-            background-color: var(--controlbox-head-color);
+            .box-flyout {
+                border: 0;
+                width: 100%;
+                z-index: 1;
+                background-color: var(--controlbox-head-color);
 
-            .controlbox-head {
-                display: none;
+                .controlbox-head {
+                    display: none;
+                }
             }
-        }
 
-        #converse-register, #converse-login {
-            @include make-col-ready();
-            @include make-col(8);
-            @include make-col-offset(2);
-
-            @include media-breakpoint-up(sm) {
-                @include make-col(8);
-                @include make-col-offset(2);
-            }
-            @include media-breakpoint-up(md) {
+            #converse-register, #converse-login {
+                @include make-col-ready();
                 @include make-col(8);
                 @include make-col-offset(2);
+
+                @include media-breakpoint-up(sm) {
+                    @include make-col(8);
+                    @include make-col-offset(2);
+                }
+                @include media-breakpoint-up(md) {
+                    @include make-col(8);
+                    @include make-col-offset(2);
+                }
+                @include media-breakpoint-up(lg) {
+                    @include make-col(6);
+                    @include make-col-offset(3);
+                }
+                .title, .instructions {
+                    margin: 1em 0;
+                }
+                input[type=submit],
+                input[type=button] {
+                    width: auto;
+                }
             }
-            @include media-breakpoint-up(lg) {
-                @include make-col(6);
-                @include make-col-offset(3);
-            }
-            .title, .instructions {
-                margin: 1em 0;
+        }
+    }
+
+    .converse-fullscreen {
+        .controlbox-panes {
+            padding-top: 1em;
+        }
+    }
+
+    .converse-overlayed {
+        .brand-heading {
+            padding-top: 0.8rem;
+            padding-left: 0.8rem;
+            width: 100%;
+        }
+        .converse-svg-logo {
+            height: 1em;
+        }
+        #controlbox {
+            #converse-login-panel {
+                height: 100%;
             }
-            input[type=submit],
-            input[type=button] {
-                width: auto;
+            .controlbox-panes {
+                margin-top: 0.5em;
             }
         }
     }
@@ -628,28 +654,3 @@
         }
     }
 }
-
-#conversejs.converse-fullscreen {
-    .controlbox-panes {
-        padding-top: 1em;
-    }
-}
-
-#conversejs.converse-overlayed {
-    .brand-heading {
-        padding-top: 0.8rem;
-        padding-left: 0.8rem;
-        width: 100%;
-    }
-    .converse-svg-logo {
-        height: 1em;
-    }
-    #controlbox {
-        #converse-login-panel {
-            height: 100%;
-        }
-        .controlbox-panes {
-            margin-top: 0.5em;
-        }
-    }
-}

+ 25 - 25
sass/_core.scss

@@ -114,32 +114,32 @@ body.converse-fullscreen {
       overflow-y: none;
     }
 
-    &.converse-overlayed {
-        > .row {
-            flex-direction: row-reverse;
-        }
-    }
+    converse-chats {
+      .converse-overlayed {
+          height: 3em;
+          > .row {
+              flex-direction: row-reverse;
+          }
+      }
 
-    &.converse-fullscreen,
-    &.converse-mobile {
-        .converse-chatboxes {
-            width: 100vw;
-            left: -15px; // Hack due to padding added by bootstrap
-        }
-    }
-    &.converse-overlayed {
-        height: 3em;
-    }
-    &.converse-embedded {
-        box-sizing: border-box;
-        *, *:before, *:after {
-            box-sizing: border-box;
-        }
-        bottom: auto;
-        height: 100%; // When embedded, it fills the containing element
-        position: relative;
-        right: auto;
-        width: 100%;
+      .converse-fullscreen,
+      .converse-mobile {
+          .converse-chatboxes {
+              width: 100vw;
+              left: -15px; // Hack due to padding added by bootstrap
+          }
+      }
+      .converse-embedded {
+          box-sizing: border-box;
+          *, *:before, *:after {
+              box-sizing: border-box;
+          }
+          bottom: auto;
+          height: 100%; // When embedded, it fills the containing element
+          position: relative;
+          right: auto;
+          width: 100%;
+      }
     }
 
     converse-brand-heading {

+ 52 - 52
sass/_emoji.scss

@@ -185,73 +185,74 @@
     }
 }
 
-
-#conversejs.converse-overlayed  {
-    converse-emoji-dropdown {
-        .dropdown-menu {
-            min-width: 18em;
-        }
-    }
-    .chatbox {
-        .emoji-picker__header {
-            .emoji-category {
-                img {
-                    height: var(--font-size) !important;
-                    width: var(--font-size) !important;
-                }
+converse-chats {
+    .converse-overlayed  {
+        converse-emoji-dropdown {
+            .dropdown-menu {
+                min-width: 18em;
             }
         }
-        converse-emoji-picker {
-            .emoji-picker {
-                .insert-emoji {
-                    a {
-                        font-size: var(--font-size);
-                        line-height: calc(var(--font-size) * 1.5);
-                        padding: 0;
-                        height: calc(var(--font-size) * 1.5);
-                        width: calc(var(--font-size) * 1.5);
-                    }
+        .chatbox {
+            .emoji-picker__header {
+                .emoji-category {
                     img {
-                        height: var(--font-size);
-                        width: var(--font-size);
+                        height: var(--font-size) !important;
+                        width: var(--font-size) !important;
                     }
                 }
             }
-            .emoji-skintone-picker {
-                font-size: var(--font-size-small);
-            }
-            .emoji-picker__header {
-                .emoji-category {
+            converse-emoji-picker {
+                .emoji-picker {
+                    .insert-emoji {
+                        a {
+                            font-size: var(--font-size);
+                            line-height: calc(var(--font-size) * 1.5);
+                            padding: 0;
+                            height: calc(var(--font-size) * 1.5);
+                            width: calc(var(--font-size) * 1.5);
+                        }
+                        img {
+                            height: var(--font-size);
+                            width: var(--font-size);
+                        }
+                    }
+                }
+                .emoji-skintone-picker {
                     font-size: var(--font-size-small);
                 }
-            }
-            .emoji-picker__lists {
-                height: 7em;
+                .emoji-picker__header {
+                    .emoji-category {
+                        font-size: var(--font-size-small);
+                    }
+                }
+                .emoji-picker__lists {
+                    height: 7em;
+                }
             }
         }
     }
-}
 
-#conversejs.converse-embedded {
-    converse-emoji-dropdown {
-        .dropdown-menu {
-            min-width: 20em;
+    .converse-embedded {
+        converse-emoji-dropdown {
+            .dropdown-menu {
+                min-width: 20em;
+            }
         }
     }
-}
 
-#conversejs.converse-fullscreen {
-    converse-emoji-dropdown {
-        .dropdown-menu {
-            min-width: 22em;
-        }
-    }
-    .chatbox {
-        .toggle-smiley {
+    .converse-fullscreen {
+        converse-emoji-dropdown {
+            .dropdown-menu {
+                min-width: 22em;
+            }
         }
-        converse-emoji-picker {
-            .emoji-picker__lists {
-                height: 12em;
+        .chatbox {
+            .toggle-smiley {
+            }
+            converse-emoji-picker {
+                .emoji-picker__lists {
+                    height: 12em;
+                }
             }
         }
     }
@@ -266,4 +267,3 @@
         }
     }
 }
-

+ 11 - 8
sass/_messages.scss

@@ -383,22 +383,25 @@
             border-bottom: var(--chatroom-separator-border-bottom);
         }
     }
-}
 
-#conversejs.converse-overlayed {
-    .message {
-        &.chat-msg {
-            &.chat-msg--followup {
-                .chat-msg__content {
-                    margin-left: 0;
+    converse-chats {
+        .converse-overlayed {
+            .message {
+                &.chat-msg {
+                    &.chat-msg--followup {
+                        .chat-msg__content {
+                            margin-left: 0;
+                        }
+                    }
                 }
             }
         }
     }
 }
 
+
 @media screen and (max-width: 767px) {
-    #conversejs:not(.converse-embedded)  {
+    converse-chats:not(.converse-embedded)  {
         .message {
             &.chat-msg {
                 .chat-msg__author {

+ 78 - 76
sass/_minimized_chats.scss

@@ -1,90 +1,92 @@
-#conversejs.converse-overlayed {
-    #minimized-chats {
-        order: 100;
+converse-chats {
+    .converse-overlayed {
+        #minimized-chats {
+            order: 100;
 
-        width: var(--minimized-chats-width);
-        margin-bottom: 0;
-        border-top-left-radius: var(--chatbox-border-radius);
-        border-top-right-radius: var(--chatbox-border-radius);
-        color: var(--inverse-link-color);
-        margin-right: var(--chat-gutter);
-        padding: 0;
-
-        .badge {
-            bottom: 8px;
-            border: 1px solid var(--overlayed-badge-color);
-        }
-
-        #toggle-minimized-chats {
+            width: var(--minimized-chats-width);
+            margin-bottom: 0;
             border-top-left-radius: var(--chatbox-border-radius);
             border-top-right-radius: var(--chatbox-border-radius);
-            background-color: var(--link-color);
-            padding: 1em 0 0 0;
-            text-align: center;
-            color: white;
-            white-space: nowrap;
-            overflow-y: hidden;
-            text-overflow: ellipsis;
-            display: block;
-            height: 45px;
-            width: 9em;
-        }
+            color: var(--inverse-link-color);
+            margin-right: var(--chat-gutter);
+            padding: 0;
 
-        a.restore-chat {
-            cursor: pointer;
-            padding: 1px 0 1px 5px;
-            color: var(--chat-head-text-color);
-            line-height: 15px;
-            display: block;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            white-space: nowrap;
-            &:hover {
-                text-decoration: none;
+            .badge {
+                bottom: 8px;
+                border: 1px solid var(--overlayed-badge-color);
             }
-        }
 
-        a.restore-chat:visited {
-            color: var(--chat-head-text-color);
-        }
+            #toggle-minimized-chats {
+                border-top-left-radius: var(--chatbox-border-radius);
+                border-top-right-radius: var(--chatbox-border-radius);
+                background-color: var(--link-color);
+                padding: 1em 0 0 0;
+                text-align: center;
+                color: white;
+                white-space: nowrap;
+                overflow-y: hidden;
+                text-overflow: ellipsis;
+                display: block;
+                height: 45px;
+                width: 9em;
+            }
 
-        .minimized-chats-flyout {
-            flex-direction: column-reverse;
-            bottom: 45px;
-            width: var(--minimized-chats-width);
+            a.restore-chat {
+                cursor: pointer;
+                padding: 1px 0 1px 5px;
+                color: var(--chat-head-text-color);
+                line-height: 15px;
+                display: block;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                &:hover {
+                    text-decoration: none;
+                }
+            }
 
-            .chat-head {
-                min-height: 0;
-                padding: 0.3em;
-                border-radius: var(--chatbox-border-radius);
-                height: 35px;
-                margin-bottom: 0.2em;
-                width: 100%;
-                max-width: 9em;
-                flex-wrap: nowrap;
+            a.restore-chat:visited {
+                color: var(--chat-head-text-color);
             }
-            &.minimized {
-                height: auto;
+
+            .minimized-chats-flyout {
+                flex-direction: column-reverse;
+                bottom: 45px;
+                width: var(--minimized-chats-width);
+
+                .chat-head {
+                    min-height: 0;
+                    padding: 0.3em;
+                    border-radius: var(--chatbox-border-radius);
+                    height: 35px;
+                    margin-bottom: 0.2em;
+                    width: 100%;
+                    max-width: 9em;
+                    flex-wrap: nowrap;
+                }
+                &.minimized {
+                    height: auto;
+                }
             }
-        }
 
-        .unread-message-count {
-            font-weight: bold;
-            background-color: white;
-            border: 1px solid;
-            text-shadow: 1px 1px 0 var(--text-shadow-color);
-            color: var(--warning-color);
-            border-radius: 5px;
-            padding: 2px 4px;
-            font-size: 16px;
-            text-align: center;
-            position: absolute;
-            right: 116px;
-            bottom: 10px;
-        }
-        .unread-message-count-hidden,
-        .chat-head-message-count-hidden {
-            display: none;
+            .unread-message-count {
+                font-weight: bold;
+                background-color: white;
+                border: 1px solid;
+                text-shadow: 1px 1px 0 var(--text-shadow-color);
+                color: var(--warning-color);
+                border-radius: 5px;
+                padding: 2px 4px;
+                font-size: 16px;
+                text-align: center;
+                position: absolute;
+                right: 116px;
+                bottom: 10px;
+            }
+            .unread-message-count-hidden,
+            .chat-head-message-count-hidden {
+                display: none;
+            }
         }
     }
 }

+ 15 - 13
sass/_toolbar.scss

@@ -187,21 +187,23 @@
             }
         }
     }
-}
 
-#conversejs.converse-overlayed  {
-    .chat-toolbar {
-        li {
-            .toolbar-menu {
-                min-width: 235px;
+    converse-chats {
+        .converse-overlayed  {
+            .chat-toolbar {
+                li {
+                    .toolbar-menu {
+                        min-width: 235px;
+                    }
+                }
             }
-        }
-    }
-    .chatroom {
-        .chat-toolbar {
-            li {
-                .toolbar-menu {
-                    min-width: 280px;
+            .chatroom {
+                .chat-toolbar {
+                    li {
+                        .toolbar-menu {
+                            min-width: 280px;
+                        }
+                    }
                 }
             }
         }

+ 41 - 41
spec/autocomplete.js

@@ -41,7 +41,7 @@ describe("The nickname autocomplete feature", function () {
         await u.waitUntil(() => view.model.messages.last()?.get('received'));
 
         // Test that pressing @ brings up all options
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         const at_event = {
             'target': textarea,
             'preventDefault': function preventDefault () {},
@@ -53,11 +53,11 @@ describe("The nickname autocomplete feature", function () {
         textarea.value = '@';
         view.onKeyUp(at_event);
 
-        await u.waitUntil(() => view.el.querySelectorAll('.suggestion-box__results li').length === 4);
-        expect(view.el.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
-        expect(view.el.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
-        expect(view.el.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
-        expect(view.el.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
+        await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
+        expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
+        expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
+        expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
+        expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
         done();
     }));
 
@@ -95,7 +95,7 @@ describe("The nickname autocomplete feature", function () {
         await u.waitUntil(() => view.model.messages.last()?.get('received'));
 
         // Test that pressing @ brings up all options
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         const at_event = {
             'target': textarea,
             'preventDefault': function preventDefault () {},
@@ -108,11 +108,11 @@ describe("The nickname autocomplete feature", function () {
         textarea.value = '\n@';
         view.onKeyUp(at_event);
 
-        await u.waitUntil(() => view.el.querySelectorAll('.suggestion-box__results li').length === 4);
-        expect(view.el.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
-        expect(view.el.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
-        expect(view.el.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
-        expect(view.el.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
+        await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
+        expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
+        expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
+        expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
+        expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
         done();
     }));
 
@@ -150,7 +150,7 @@ describe("The nickname autocomplete feature", function () {
         await u.waitUntil(() => view.model.messages.last()?.get('received'));
 
         // Test that pressing @ brings up all options
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         const at_event = {
             'target': textarea,
             'preventDefault': function preventDefault () {},
@@ -163,11 +163,11 @@ describe("The nickname autocomplete feature", function () {
         textarea.value = '(@';
         view.onKeyUp(at_event);
 
-        await u.waitUntil(() => view.el.querySelectorAll('.suggestion-box__results li').length === 4);
-        expect(view.el.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
-        expect(view.el.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
-        expect(view.el.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
-        expect(view.el.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
+        await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
+        expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
+        expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
+        expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
+        expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
         done();
     }));
 
@@ -191,7 +191,7 @@ describe("The nickname autocomplete feature", function () {
                         })));
             });
 
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            const textarea = view.querySelector('textarea.chat-textarea');
             const at_event = {
                 'target': textarea,
                 'preventDefault': function preventDefault() { },
@@ -204,17 +204,17 @@ describe("The nickname autocomplete feature", function () {
             view.onKeyDown(at_event);
             textarea.value = '@ber';
             view.onKeyUp(at_event);
-            await u.waitUntil(() => view.el.querySelectorAll('.suggestion-box__results li').length === 3);
-            expect(view.el.querySelector('.suggestion-box__results li:first-child').textContent).toBe('bernard');
-            expect(view.el.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('naber');
-            expect(view.el.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('helberlo');
+            await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 3);
+            expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('bernard');
+            expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('naber');
+            expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('helberlo');
 
             // Test that when the query index is equal, results should be sorted by length
             textarea.value = '@jo';
             view.onKeyUp(at_event);
-            await u.waitUntil(() => view.el.querySelectorAll('.suggestion-box__results li').length === 2);
-            expect(view.el.querySelector('.suggestion-box__results li:first-child').textContent).toBe('john');
-            expect(view.el.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('jones');
+            await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 2);
+            expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('john');
+            expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('jones');
             done();
     }));
 
@@ -239,7 +239,7 @@ describe("The nickname autocomplete feature", function () {
         _converse.connection._dataRecv(mock.createRequest(presence));
         expect(view.model.occupants.length).toBe(2);
 
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         textarea.value = "hello som";
 
         // Press tab
@@ -252,9 +252,9 @@ describe("The nickname autocomplete feature", function () {
         }
         view.onKeyDown(tab_event);
         view.onKeyUp(tab_event);
-        await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false);
-        expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(1);
-        expect(view.el.querySelector('.suggestion-box__results li').textContent).toBe('some1');
+        await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
+        expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
+        expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1');
 
         const backspace_event = {
             'target': textarea,
@@ -267,7 +267,7 @@ describe("The nickname autocomplete feature", function () {
             textarea.value = textarea.value.slice(0, textarea.value.length-1)
             view.onKeyUp(backspace_event);
         }
-        await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === true);
+        await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === true);
 
         presence = $pres({
                 'to': 'romeo@montague.lit/orchard',
@@ -284,8 +284,8 @@ describe("The nickname autocomplete feature", function () {
         textarea.value = "hello s s";
         view.onKeyDown(tab_event);
         view.onKeyUp(tab_event);
-        await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false);
-        expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2);
+        await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
+        expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2);
 
         const up_arrow_event = {
             'target': textarea,
@@ -295,9 +295,9 @@ describe("The nickname autocomplete feature", function () {
         }
         view.onKeyDown(up_arrow_event);
         view.onKeyUp(up_arrow_event);
-        expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2);
-        expect(view.el.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1');
-        expect(view.el.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2');
+        expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2);
+        expect(view.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1');
+        expect(view.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2');
 
         view.onKeyDown({
             'target': textarea,
@@ -322,7 +322,7 @@ describe("The nickname autocomplete feature", function () {
         textarea.value = "hello z";
         view.onKeyDown(tab_event);
         view.onKeyUp(tab_event);
-        await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false);
+        await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
 
         view.onKeyDown(tab_event);
         view.onKeyUp(tab_event);
@@ -351,7 +351,7 @@ describe("The nickname autocomplete feature", function () {
         _converse.connection._dataRecv(mock.createRequest(presence));
         expect(view.model.occupants.length).toBe(2);
 
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         textarea.value = "hello @some1 ";
 
         // Press backspace
@@ -365,9 +365,9 @@ describe("The nickname autocomplete feature", function () {
         view.onKeyDown(backspace_event);
         textarea.value = "hello @some1"; // Mimic backspace
         view.onKeyUp(backspace_event);
-        await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false);
-        expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(1);
-        expect(view.el.querySelector('.suggestion-box__results li').textContent).toBe('some1');
+        await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
+        expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
+        expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1');
         done();
     }));
 });

+ 26 - 26
spec/bookmarks.js

@@ -28,13 +28,13 @@ describe("A chat room", function () {
 
         spyOn(view, 'renderBookmarkForm').and.callThrough();
         spyOn(view, 'closeForm').and.callThrough();
-        await u.waitUntil(() => view.el.querySelector('.toggle-bookmark') !== null);
-        const toggle = view.el.querySelector('.toggle-bookmark');
+        await u.waitUntil(() => view.querySelector('.toggle-bookmark') !== null);
+        const toggle = view.querySelector('.toggle-bookmark');
         expect(toggle.title).toBe('Bookmark this groupchat');
         toggle.click();
         expect(view.renderBookmarkForm).toHaveBeenCalled();
 
-        view.el.querySelector('.button-cancel').click();
+        view.querySelector('.button-cancel').click();
         expect(view.closeForm).toHaveBeenCalled();
         expect(u.hasClass('on-button', toggle), false);
         expect(toggle.title).toBe('Bookmark this groupchat');
@@ -74,13 +74,13 @@ describe("A chat room", function () {
          *  </iq>
          */
         expect(view.model.get('bookmarked')).toBeFalsy();
-        const form = view.el.querySelector('.chatroom-form');
+        const form = view.querySelector('.chatroom-form');
         form.querySelector('input[name="name"]').value = 'Play&apos;s the Thing';
         form.querySelector('input[name="autojoin"]').checked = 'checked';
         form.querySelector('input[name="nick"]').value = 'JC';
 
         const IQ_stanzas = _converse.connection.IQ_stanzas;
-        view.el.querySelector('.muc-bookmark-form .btn-primary').click();
+        view.querySelector('.muc-bookmark-form .btn-primary').click();
 
         const sent_stanza = await u.waitUntil(
             () => IQ_stanzas.filter(s => sizzle('iq publish[node="storage:bookmarks"]', s).length).pop());
@@ -124,8 +124,8 @@ describe("A chat room", function () {
         _converse.connection._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => view.model.get('bookmarked'));
         expect(view.model.get('bookmarked')).toBeTruthy();
-        await u.waitUntil(() => view.el.querySelector('.toggle-bookmark')?.title === 'Unbookmark this groupchat');
-        expect(u.hasClass('on-button', view.el.querySelector('.toggle-bookmark')), true);
+        await u.waitUntil(() => view.querySelector('.toggle-bookmark')?.title === 'Unbookmark this groupchat');
+        expect(u.hasClass('on-button', view.querySelector('.toggle-bookmark')), true);
         // We ignore this IQ stanza... (unless it's an error stanza), so
         // nothing to test for here.
         done();
@@ -211,7 +211,7 @@ describe("A chat room", function () {
             );
             await _converse.api.rooms.open(`lounge@montague.lit`);
             const view = _converse.chatboxviews.get('lounge@montague.lit');
-            expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null);
+            expect(view.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null);
             _converse.bookmarks.create({
                 'jid': view.model.get('jid'),
                 'autojoin': false,
@@ -219,9 +219,9 @@ describe("A chat room", function () {
                 'nick': ' some1'
             });
             view.model.set('bookmarked', true);
-            await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') !== null);
+            await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') !== null);
             view.model.set('bookmarked', false);
-            await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') === null);
+            await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') === null);
             done();
         }));
 
@@ -233,7 +233,7 @@ describe("A chat room", function () {
             const muc_jid = 'theplay@conference.shakespeare.lit';
             await _converse.api.rooms.open(muc_jid);
             const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
+            await u.waitUntil(() => view.querySelector('.toggle-bookmark'));
 
             spyOn(view, 'toggleBookmark').and.callThrough();
             spyOn(_converse.bookmarks, 'sendBookmarkStanza').and.callThrough();
@@ -249,12 +249,12 @@ describe("A chat room", function () {
             expect(_converse.bookmarks.length).toBe(1);
             await u.waitUntil(() => _converse.chatboxes.length >= 1);
             expect(view.model.get('bookmarked')).toBeTruthy();
-            await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') !== null);
+            await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') !== null);
             spyOn(_converse.connection, 'getUniqueId').and.callThrough();
-            const bookmark_icon = view.el.querySelector('.toggle-bookmark');
+            const bookmark_icon = view.querySelector('.toggle-bookmark');
             bookmark_icon.click();
             expect(view.toggleBookmark).toHaveBeenCalled();
-            await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') === null);
+            await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') === null);
             expect(_converse.bookmarks.length).toBe(0);
 
             // Check that an IQ stanza is sent out, containing no
@@ -594,20 +594,20 @@ describe("Bookmarks", function () {
                                     'jid': 'first@conference.shakespeare.lit'
                                 }).c('nick').t('JC');
             _converse.connection._dataRecv(mock.createRequest(stanza));
-            await u.waitUntil(() => view.el.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length);
-            expect(view.el.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length).toBe(2);
-            view.el.querySelector('.bookmarks.rooms-list .open-room').click();
+            await u.waitUntil(() => view.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length);
+            expect(view.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length).toBe(2);
+            view.querySelector('.bookmarks.rooms-list .open-room').click();
             await u.waitUntil(() => _converse.chatboxes.length === 2);
             expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false);
 
-            await u.waitUntil(() => view.el.querySelectorAll('.list-container--bookmarks .available-chatroom:not(.hidden)').length === 1);
-            view.el.querySelector('.list-container--bookmarks .available-chatroom:not(.hidden) .open-room').click();
+            await u.waitUntil(() => view.querySelectorAll('.list-container--bookmarks .available-chatroom:not(.hidden)').length === 1);
+            view.querySelector('.list-container--bookmarks .available-chatroom:not(.hidden) .open-room').click();
             await u.waitUntil(() => _converse.chatboxes.length === 3);
             expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(true);
             expect((await api.rooms.get('theplay@conference.shakespeare.lit')).get('hidden')).toBe(false);
 
-            view.el.querySelector('.list-container--openrooms .open-room:first-child').click();
-            await u.waitUntil(() => view.el.querySelector('.list-item.open').getAttribute('data-room-jid') === 'first@conference.shakespeare.lit');
+            view.querySelector('.list-container--openrooms .open-room:first-child').click();
+            await u.waitUntil(() => view.querySelector('.list-item.open').getAttribute('data-room-jid') === 'first@conference.shakespeare.lit');
             expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false);
             expect((await api.rooms.get('theplay@conference.shakespeare.lit')).get('hidden')).toBe(true);
             done();
@@ -689,19 +689,19 @@ describe("When hide_open_bookmarks is true and a bookmarked room is opened", fun
 
         const u = converse.env.utils;
         const bmarks_view = _converse.bookmarksview;
-        await u.waitUntil(() => bmarks_view.el.querySelectorAll(".open-room").length, 500);
-        const room_els = bmarks_view.el.querySelectorAll(".open-room");
+        await u.waitUntil(() => bmarks_view.querySelectorAll(".open-room").length, 500);
+        const room_els = bmarks_view.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
 
-        const bookmark = _converse.bookmarksview.el.querySelector(".open-room");
+        const bookmark = _converse.bookmarksview.querySelector(".open-room");
         bookmark.click();
         await u.waitUntil(() => _converse.chatboxviews.get(jid));
 
-        expect(u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))).toBeTruthy();
+        expect(u.hasClass('hidden', _converse.bookmarksview.querySelector(".available-chatroom"))).toBeTruthy();
         // Check that it reappears once the room is closed
         const view = _converse.chatboxviews.get(jid);
         view.close();
-        await u.waitUntil(() => !u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom")));
+        await u.waitUntil(() => !u.hasClass('hidden', _converse.bookmarksview.querySelector(".available-chatroom")));
         done();
     }));
 });

+ 51 - 51
spec/chatbox.js

@@ -40,7 +40,7 @@ describe("Chatboxes", function () {
             await _converse.handleMessageStanza(msg);
             await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
             const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__body';
-            await u.waitUntil(() => view.el.querySelector(msg_txt_sel).textContent.trim() === 'hello world');
+            await u.waitUntil(() => view.querySelector(msg_txt_sel).textContent.trim() === 'hello world');
             done();
         }));
 
@@ -58,7 +58,7 @@ describe("Chatboxes", function () {
             }
             await u.waitUntil(() => sizzle('converse-chat-message', view.el).length === 10);
 
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = '/clear';
             view.onKeyDown({
                 target: textarea,
@@ -84,8 +84,8 @@ describe("Chatboxes", function () {
             spyOn(_converse.minimize, 'trimChats');
             expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
 
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700);
-            const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group li').length, 700);
+            const online_contacts = _converse.rosterview.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
             expect(online_contacts.length).toBe(17);
             let el = online_contacts[0];
             el.click();
@@ -153,7 +153,7 @@ describe("Chatboxes", function () {
             const view = await mock.openChatBoxFor(_converse, contact_jid);
             const el = sizzle('a.open-chat:contains("'+view.model.getDisplayName()+'")', _converse.rosterview.el).pop();
             await u.waitUntil(() => u.isVisible(el));
-            const textarea = view.el.querySelector('.chat-textarea');
+            const textarea = view.querySelector('.chat-textarea');
             await u.waitUntil(() => u.isVisible(textarea));
             textarea.blur();
             spyOn(view.model, 'maybeShow').and.callThrough();
@@ -209,14 +209,14 @@ describe("Chatboxes", function () {
 
             await mock.waitForRoster(_converse, 'current');
             const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
             await mock.openChatBoxFor(_converse, contact_jid);
             const chatview = _converse.chatboxviews.get(contact_jid);
             spyOn(chatview, 'close').and.callThrough();
             spyOn(_converse.api, "trigger").and.callThrough();
             // We need to rebind all events otherwise our spy won't be called
             chatview.delegateEvents();
-            chatview.el.querySelector('.close-chatbox-button').click();
+            chatview.querySelector('.close-chatbox-button').click();
             expect(chatview.close).toHaveBeenCalled();
             await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve));
             expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
@@ -231,7 +231,7 @@ describe("Chatboxes", function () {
             spyOn(_converse.minimize, 'trimChats');
             await mock.waitForRoster(_converse, 'current');
             await mock.openControlBox(_converse);
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
             spyOn(_converse.api, "trigger").and.callThrough();
 
             mock.closeControlBox();
@@ -274,19 +274,19 @@ describe("Chatboxes", function () {
                 const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                 await mock.openChatBoxFor(_converse, contact_jid);
                 const view = _converse.chatboxviews.get(contact_jid);
-                const toolbar = view.el.querySelector('.chat-toolbar');
+                const toolbar = view.querySelector('.chat-toolbar');
                 const counter = toolbar.querySelector('.message-limit');
                 expect(counter.textContent).toBe('200');
                 view.insertIntoTextArea('hello world');
                 expect(counter.textContent).toBe('188');
 
                 toolbar.querySelector('.toggle-emojis').click();
-                const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__lists'));
+                const picker = await u.waitUntil(() => view.querySelector('.emoji-picker__lists'));
                 const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a'));
                 item.click()
                 expect(counter.textContent).toBe('179');
 
-                const textarea = view.el.querySelector('.chat-textarea');
+                const textarea = view.querySelector('.chat-textarea');
                 const ev = {
                     target: textarea,
                     preventDefault: function preventDefault () {},
@@ -314,7 +314,7 @@ describe("Chatboxes", function () {
                 const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                 await mock.openChatBoxFor(_converse, contact_jid);
                 const view = _converse.chatboxviews.get(contact_jid);
-                const counter = view.el.querySelector('.chat-toolbar .message-limit');
+                const counter = view.querySelector('.chat-toolbar .message-limit');
                 expect(counter).toBe(null);
                 done();
             }));
@@ -336,7 +336,7 @@ describe("Chatboxes", function () {
                 _converse.visible_toolbar_buttons.call = false;
                 await mock.openChatBoxFor(_converse, contact_jid);
                 let view = _converse.chatboxviews.get(contact_jid);
-                toolbar = view.el.querySelector('.chat-toolbar');
+                toolbar = view.querySelector('.chat-toolbar');
                 call_button = toolbar.querySelector('.toggle-call');
                 expect(call_button === null).toBeTruthy();
                 view.close();
@@ -345,7 +345,7 @@ describe("Chatboxes", function () {
                 _converse.visible_toolbar_buttons.call = true; // enable the button
                 await mock.openChatBoxFor(_converse, contact_jid);
                 view = _converse.chatboxviews.get(contact_jid);
-                toolbar = view.el.querySelector('.chat-toolbar');
+                toolbar = view.querySelector('.chat-toolbar');
                 call_button = toolbar.querySelector('.toggle-call');
                 call_button.click();
                 expect(_converse.api.trigger).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object));
@@ -390,7 +390,7 @@ describe("Chatboxes", function () {
                     await mock.waitForRoster(_converse, 'current');
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     await mock.openControlBox(_converse);
-                    u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                    u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
                     spyOn(_converse.connection, 'send');
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
@@ -413,7 +413,7 @@ describe("Chatboxes", function () {
                     await mock.openControlBox(_converse);
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
-                    await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
                     view.model.minimize();
@@ -447,14 +447,14 @@ describe("Chatboxes", function () {
                     await mock.openControlBox(_converse);
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
-                    await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     var view = _converse.chatboxviews.get(contact_jid);
                     expect(view.model.get('chat_state')).toBe('active');
                     spyOn(_converse.connection, 'send');
                     spyOn(_converse.api, "trigger").and.callThrough();
                     view.onKeyDown({
-                        target: view.el.querySelector('textarea.chat-textarea'),
+                        target: view.querySelector('textarea.chat-textarea'),
                         keyCode: 1
                     });
                     expect(view.model.get('chat_state')).toBe('composing');
@@ -469,7 +469,7 @@ describe("Chatboxes", function () {
 
                     // The notification is not sent again
                     view.onKeyDown({
-                        target: view.el.querySelector('textarea.chat-textarea'),
+                        target: view.querySelector('textarea.chat-textarea'),
                         keyCode: 1
                     });
                     expect(view.model.get('chat_state')).toBe('composing');
@@ -486,14 +486,14 @@ describe("Chatboxes", function () {
                     await mock.openControlBox(_converse);
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
-                    await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     var view = _converse.chatboxviews.get(contact_jid);
                     expect(view.model.get('chat_state')).toBe('active');
                     spyOn(_converse.connection, 'send');
                     spyOn(_converse.api, "trigger").and.callThrough();
                     view.onKeyDown({
-                        target: view.el.querySelector('textarea.chat-textarea'),
+                        target: view.querySelector('textarea.chat-textarea'),
                         keyCode: 1
                     });
                     expect(view.model.get('chat_state')).toBe('composing');
@@ -511,7 +511,7 @@ describe("Chatboxes", function () {
 
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                    await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, sender_jid);
 
                     // <composing> state
@@ -525,7 +525,7 @@ describe("Chatboxes", function () {
                     _converse.connection._dataRecv(mock.createRequest(msg));
                     const view = _converse.chatboxviews.get(sender_jid);
                     let csn = mock.cur_names[1] + ' is typing';
-                    await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn);
+                    await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === csn);
                     expect(view.model.messages.length).toEqual(0);
 
                     // <paused> state
@@ -537,7 +537,7 @@ describe("Chatboxes", function () {
                         }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
                     _converse.connection._dataRecv(mock.createRequest(msg));
                     csn = mock.cur_names[1] + ' has stopped typing';
-                    await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn);
+                    await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === csn);
 
                     msg = $msg({
                             from: sender_jid,
@@ -547,7 +547,7 @@ describe("Chatboxes", function () {
                         }).c('body').t('hello world').tree();
                     await _converse.handleMessageStanza(msg);
                     const msg_el = await u.waitUntil(() => view.content.querySelector('.chat-msg'));
-                    await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === '');
+                    await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === '');
                     expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('hello world');
                     done();
                 }));
@@ -584,7 +584,7 @@ describe("Chatboxes", function () {
 
                     await u.waitUntil(() => u.shouldCreateMessage.calls.count());
                     expect(view.model.messages.length).toEqual(0);
-                    const el = view.el.querySelector('.chat-content__notifications');
+                    const el = view.querySelector('.chat-content__notifications');
                     expect(el.textContent).toBe('');
                     done();
                 }));
@@ -600,7 +600,7 @@ describe("Chatboxes", function () {
                     await mock.waitForRoster(_converse, 'current');
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     await mock.openControlBox(_converse);
-                    await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700);
+                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group li').length, 700);
                     _converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
@@ -608,7 +608,7 @@ describe("Chatboxes", function () {
                     spyOn(view.model, 'setChatState').and.callThrough();
                     expect(view.model.get('chat_state')).toBe('active');
                     view.onKeyDown({
-                        target: view.el.querySelector('textarea.chat-textarea'),
+                        target: view.querySelector('textarea.chat-textarea'),
                         keyCode: 1
                     });
                     expect(view.model.get('chat_state')).toBe('composing');
@@ -632,14 +632,14 @@ describe("Chatboxes", function () {
                     // out if the user simply types longer than the
                     // timeout.
                     view.onKeyDown({
-                        target: view.el.querySelector('textarea.chat-textarea'),
+                        target: view.querySelector('textarea.chat-textarea'),
                         keyCode: 1
                     });
                     expect(view.model.setChatState).toHaveBeenCalled();
                     expect(view.model.get('chat_state')).toBe('composing');
 
                     view.onKeyDown({
-                        target: view.el.querySelector('textarea.chat-textarea'),
+                        target: view.querySelector('textarea.chat-textarea'),
                         keyCode: 1
                     });
                     expect(view.model.get('chat_state')).toBe('composing');
@@ -653,7 +653,7 @@ describe("Chatboxes", function () {
 
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openControlBox(_converse);
-                    await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
                     // TODO: only show paused state if the previous state was composing
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     spyOn(_converse.api, "trigger").and.callThrough();
@@ -669,7 +669,7 @@ describe("Chatboxes", function () {
 
                     _converse.connection._dataRecv(mock.createRequest(msg));
                     const csn = mock.cur_names[1] +  ' has stopped typing';
-                    await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn);
+                    await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === csn);
                     expect(view.model.messages.length).toEqual(0);
                     done();
                 }));
@@ -703,7 +703,7 @@ describe("Chatboxes", function () {
                     _converse.connection._dataRecv(mock.createRequest(msg));
                     await u.waitUntil(() => u.shouldCreateMessage.calls.count());
                     expect(view.model.messages.length).toEqual(0);
-                    const el = view.el.querySelector('.chat-content__notifications');
+                    const el = view.querySelector('.chat-content__notifications');
                     expect(el.textContent).toBe('');
                     done();
                     done();
@@ -725,7 +725,7 @@ describe("Chatboxes", function () {
                     await mock.waitForRoster(_converse, 'current');
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     await mock.openControlBox(_converse);
-                    await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 1000);
+                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 1000);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
                     await u.waitUntil(() => view.model.get('chat_state') === 'active');
@@ -733,7 +733,7 @@ describe("Chatboxes", function () {
                     expect(messages.length).toBe(1);
                     expect(view.model.get('chat_state')).toBe('active');
                     view.onKeyDown({
-                        target: view.el.querySelector('textarea.chat-textarea'),
+                        target: view.querySelector('textarea.chat-textarea'),
                         keyCode: 1
                     });
                     await u.waitUntil(() => view.model.get('chat_state') === 'composing', 600);
@@ -804,7 +804,7 @@ describe("Chatboxes", function () {
                     await mock.waitForRoster(_converse, 'current');
                     const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                     await mock.openControlBox(_converse);
-                    await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
                     const view = await mock.openChatBoxFor(_converse, contact_jid);
                     expect(view.model.get('chat_state')).toBe('active');
                     spyOn(_converse.connection, 'send');
@@ -831,7 +831,7 @@ describe("Chatboxes", function () {
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     await mock.openChatBoxFor(_converse, sender_jid);
                     const view = _converse.chatboxviews.get(sender_jid);
-                    expect(view.el.querySelectorAll('.chat-event').length).toBe(0);
+                    expect(view.querySelectorAll('.chat-event').length).toBe(0);
                     // Insert <composing> message, to also check that
                     // text messages are inserted correctly with
                     // temporary chat events in the chat contents.
@@ -843,7 +843,7 @@ describe("Chatboxes", function () {
                         .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
                         .tree();
                     _converse.connection._dataRecv(mock.createRequest(msg));
-                    const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
+                    const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent);
                     expect(csntext).toEqual(mock.cur_names[1] + ' is typing');
                     expect(view.model.messages.length).toBe(0);
 
@@ -855,7 +855,7 @@ describe("Chatboxes", function () {
                         }).c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree();
                     _converse.connection._dataRecv(mock.createRequest(msg));
 
-                    await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent);
+                    await u.waitUntil(() => !view.querySelector('.chat-content__notifications').textContent);
                     done();
                 }));
             });
@@ -881,7 +881,7 @@ describe("Chatboxes", function () {
                     _converse.connection._dataRecv(mock.createRequest(msg));
 
                     const view = _converse.chatboxviews.get(sender_jid);
-                    const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
+                    const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent);
                     expect(csntext).toEqual(mock.cur_names[1] + ' has gone away');
                     done();
                 }));
@@ -899,7 +899,7 @@ describe("Chatboxes", function () {
 
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
                     const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                    await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                    await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, sender_jid);
 
                     // Original message
@@ -928,7 +928,7 @@ describe("Chatboxes", function () {
                     }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
                     _converse.connection._dataRecv(mock.createRequest(msg));
 
-                    const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
+                    const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent);
                     expect(csntext).toEqual(mock.cur_names[1] + ' is typing');
 
                     // Edited message
@@ -943,7 +943,7 @@ describe("Chatboxes", function () {
                         .c('replace', {'xmlns': Strophe.NS.MESSAGE_CORRECT, 'id': original_id }).tree();
 
                     await _converse.handleMessageStanza(edited);
-                    await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent);
+                    await u.waitUntil(() => !view.querySelector('.chat-content__notifications').textContent);
                     done();
                 }));
             });
@@ -970,16 +970,16 @@ describe("Chatboxes", function () {
             expect(view.model.messages.length === 1).toBeTruthy();
             let stored_messages = await view.model.messages.browserStorage.findAll();
             expect(stored_messages.length).toBe(1);
-            await u.waitUntil(() => view.el.querySelector('.chat-msg'));
+            await u.waitUntil(() => view.querySelector('.chat-msg'));
 
             message = '/clear';
             spyOn(view, 'clearMessages').and.callThrough();
             spyOn(window, 'confirm').and.callFake(function () {
                 return true;
             });
-            view.el.querySelector('.chat-textarea').value = message;
+            view.querySelector('.chat-textarea').value = message;
             view.onKeyDown({
-                target: view.el.querySelector('textarea.chat-textarea'),
+                target: view.querySelector('textarea.chat-textarea'),
                 preventDefault: function preventDefault () {},
                 keyCode: 13
             });
@@ -1158,7 +1158,7 @@ describe("Chatboxes", function () {
             await mock.waitForRoster(_converse, 'current', 1);
             let msg, indicator_el;
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             chatbox.save('scrolled', true);
@@ -1185,7 +1185,7 @@ describe("Chatboxes", function () {
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
             let indicator_el, msg;
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             var chatboxview = _converse.chatboxviews.get(sender_jid);
@@ -1214,7 +1214,7 @@ describe("Chatboxes", function () {
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
@@ -1240,7 +1240,7 @@ describe("Chatboxes", function () {
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
@@ -1264,7 +1264,7 @@ describe("Chatboxes", function () {
 
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);

+ 20 - 20
spec/controlbox.js

@@ -45,7 +45,7 @@ describe("The Controlbox", function () {
         // We need to rebind all events otherwise our spy won't be called
         controlview.delegateEvents();
 
-        controlview.el.querySelector('.close-chatbox-button').click();
+        controlview.querySelector('.close-chatbox-button').click();
         expect(controlview.close).toHaveBeenCalled();
         await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve));
         expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
@@ -76,9 +76,9 @@ describe("The Controlbox", function () {
                 ask: 'subscribe',
                 fullname: mock.pend_names[0]
             });
-            await u.waitUntil(() => _.filter(_converse.rosterview.el.querySelectorAll('.roster-group li'), u.isVisible).length, 700);
+            await u.waitUntil(() => _.filter(_converse.rosterview.querySelectorAll('.roster-group li'), u.isVisible).length, 700);
             // Checking that only one entry is created because both JID is same (Case sensitive check)
-            expect(_.filter(_converse.rosterview.el.querySelectorAll('li'), u.isVisible).length).toBe(1);
+            expect(_.filter(_converse.rosterview.querySelectorAll('li'), u.isVisible).length).toBe(1);
             expect(_converse.rosterview.update).toHaveBeenCalled();
             done();
         }));
@@ -98,7 +98,7 @@ describe("The Controlbox", function () {
             chatview.model.set({'minimized': true});
 
             expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count') === null).toBeTruthy();
-            expect(_converse.rosterview.el.querySelector('.msgs-indicator') === null).toBeTruthy();
+            expect(_converse.rosterview.querySelector('.msgs-indicator') === null).toBeTruthy();
 
             let msg = $msg({
                     from: sender_jid,
@@ -108,10 +108,10 @@ describe("The Controlbox", function () {
                 }).c('body').t('hello').up()
                 .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
             _converse.handleMessageStanza(msg);
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll(".msgs-indicator").length);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll(".msgs-indicator").length);
             spyOn(chatview.model, 'handleUnreadMessage').and.callThrough();
             await u.waitUntil(() => _converse.chatboxviews.el.querySelector('.restore-chat .message-count')?.textContent === '1');
-            expect(_converse.rosterview.el.querySelector('.msgs-indicator').textContent).toBe('1');
+            expect(_converse.rosterview.querySelector('.msgs-indicator').textContent).toBe('1');
 
             msg = $msg({
                     from: sender_jid,
@@ -123,10 +123,10 @@ describe("The Controlbox", function () {
             _converse.handleMessageStanza(msg);
             await u.waitUntil(() => chatview.model.handleUnreadMessage.calls.count());
             await u.waitUntil(() => _converse.chatboxviews.el.querySelector('.restore-chat .message-count')?.textContent === '2');
-            expect(_converse.rosterview.el.querySelector('.msgs-indicator').textContent).toBe('2');
+            expect(_converse.rosterview.querySelector('.msgs-indicator').textContent).toBe('2');
             chatview.model.set({'minimized': false});
             expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count')).toBe(null);
-            await u.waitUntil(() => _converse.rosterview.el.querySelector('.msgs-indicator') === null);
+            await u.waitUntil(() => _converse.rosterview.querySelector('.msgs-indicator') === null);
             done();
         }));
     });
@@ -140,8 +140,8 @@ describe("The Controlbox", function () {
 
             mock.openControlBox(_converse);
             var view = _converse.xmppstatusview;
-            expect(u.hasClass('online', view.el.querySelector('.xmpp-status span:first-child'))).toBe(true);
-            expect(view.el.querySelector('.xmpp-status span.online').textContent.trim()).toBe('I am online');
+            expect(u.hasClass('online', view.querySelector('.xmpp-status span:first-child'))).toBe(true);
+            expect(view.querySelector('.xmpp-status span.online').textContent.trim()).toBe('I am online');
             done();
         }));
 
@@ -152,7 +152,7 @@ describe("The Controlbox", function () {
 
             await mock.openControlBox(_converse);
             var cbview = _converse.chatboxviews.get('controlbox');
-            cbview.el.querySelector('.change-status').click()
+            cbview.querySelector('.change-status').click()
             const modal = _converse.api.modal.get('modal-status-change');
             await u.waitUntil(() => u.isVisible(modal.el), 1000);
             const view = _converse.xmppstatusview;
@@ -166,10 +166,10 @@ describe("The Controlbox", function () {
                     `<priority>0</priority>`+
                     `<c hash="sha-1" node="https://conversejs.org" ver="PxXfr6uz8ClMWIga0OB/MhKNH/M=" xmlns="http://jabber.org/protocol/caps"/>`+
                 `</presence>`);
-            const first_child = view.el.querySelector('.xmpp-status span:first-child');
+            const first_child = view.querySelector('.xmpp-status span:first-child');
             expect(u.hasClass('online', first_child)).toBe(false);
             expect(u.hasClass('dnd', first_child)).toBe(true);
-            expect(view.el.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe('I am busy');
+            expect(view.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe('I am busy');
             done();
         }));
 
@@ -180,7 +180,7 @@ describe("The Controlbox", function () {
 
             await mock.openControlBox(_converse);
             const cbview = _converse.chatboxviews.get('controlbox');
-            cbview.el.querySelector('.change-status').click()
+            cbview.querySelector('.change-status').click()
             const modal = _converse.api.modal.get('modal-status-change');
 
             await u.waitUntil(() => u.isVisible(modal.el), 1000);
@@ -197,9 +197,9 @@ describe("The Controlbox", function () {
                     `<c hash="sha-1" node="https://conversejs.org" ver="PxXfr6uz8ClMWIga0OB/MhKNH/M=" xmlns="http://jabber.org/protocol/caps"/>`+
                 `</presence>`);
 
-            const first_child = view.el.querySelector('.xmpp-status span:first-child');
+            const first_child = view.querySelector('.xmpp-status span:first-child');
             expect(u.hasClass('online', first_child)).toBe(true);
-            expect(view.el.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe(msg);
+            expect(view.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe(msg);
             done();
         }));
     });
@@ -216,7 +216,7 @@ describe("The 'Add Contact' widget", function () {
         await mock.openControlBox(_converse);
 
         const cbview = _converse.chatboxviews.get('controlbox');
-        cbview.el.querySelector('.add-contact').click()
+        cbview.querySelector('.add-contact').click()
         const modal = _converse.api.modal.get('add-contact-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 1000);
         expect(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null);
@@ -249,7 +249,7 @@ describe("The 'Add Contact' widget", function () {
         await mock.waitForRoster(_converse, 'all', 0);
         mock.openControlBox(_converse);
         const cbview = _converse.chatboxviews.get('controlbox');
-        cbview.el.querySelector('.add-contact').click()
+        cbview.querySelector('.add-contact').click()
         const modal = _converse.api.modal.get('add-contact-modal');
         expect(modal.jid_auto_complete).toBe(undefined);
         expect(modal.name_auto_complete).toBe(undefined);
@@ -296,7 +296,7 @@ describe("The 'Add Contact' widget", function () {
         XMLHttpRequest.and.callFake(() => xhr);
 
         const cbview = _converse.chatboxviews.get('controlbox');
-        cbview.el.querySelector('.add-contact').click()
+        cbview.querySelector('.add-contact').click()
         const modal = _converse.api.modal.get('add-contact-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 1000);
 
@@ -366,7 +366,7 @@ describe("The 'Add Contact' widget", function () {
         XMLHttpRequest.and.callFake(() => xhr);
 
         const cbview = _converse.chatboxviews.get('controlbox');
-        cbview.el.querySelector('.add-contact').click()
+        cbview.querySelector('.add-contact').click()
         modal = _converse.api.modal.get('add-contact-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 1000);
 

+ 2 - 2
spec/converse.js

@@ -292,7 +292,7 @@ describe("Converse", function() {
             expect(chat.get('box_id')).toBe(`box-${jid}`);
 
             const view = _converse.chatboxviews.get(jid);
-            await u.waitUntil(() => u.isVisible(view.el));
+            await u.waitUntil(() => u.isVisible(view));
             // Test for multiple JIDs
             mock.openChatBoxFor(_converse, jid2);
             await u.waitUntil(() => _converse.chatboxes.length == 3);
@@ -325,7 +325,7 @@ describe("Converse", function() {
                 ['close', 'endOTR', 'focus', 'get', 'initiateOTR', 'is_chatroom', 'maximize', 'minimize', 'open', 'set']
             );
             const view = _converse.chatboxviews.get(jid);
-            await u.waitUntil(() => u.isVisible(view.el));
+            await u.waitUntil(() => u.isVisible(view));
             // Test for multiple JIDs
             const list = await _converse.api.chats.open([jid, jid2]);
             expect(Array.isArray(list)).toBeTruthy();

+ 75 - 75
spec/corrections.js

@@ -15,7 +15,7 @@ describe("A Chat Message", function () {
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         await mock.openChatBoxFor(_converse, contact_jid)
         const view = _converse.api.chatviews.get(contact_jid);
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         expect(textarea.value).toBe('');
         view.onKeyDown({
             target: textarea,
@@ -30,8 +30,8 @@ describe("A Chat Message", function () {
             keyCode: 13 // Enter
         });
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        expect(view.el.querySelector('.chat-msg__text').textContent)
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelector('.chat-msg__text').textContent)
             .toBe('But soft, what light through yonder airlock breaks?');
 
         const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
@@ -42,8 +42,8 @@ describe("A Chat Message", function () {
         });
         expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
         expect(view.model.messages.at(0).get('correcting')).toBe(true);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
 
         spyOn(_converse.connection, 'send');
         textarea.value = 'But soft, what light through yonder window breaks?';
@@ -76,8 +76,8 @@ describe("A Chat Message", function () {
         expect(keys.length).toBe(1);
         expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
 
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => (u.hasClass('correcting', view.querySelector('.chat-msg')) === false), 500);
 
         // Test that pressing the down arrow cancels message correction
         await u.waitUntil(() => textarea.value === '')
@@ -87,8 +87,8 @@ describe("A Chat Message", function () {
         });
         expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
         expect(view.model.messages.at(0).get('correcting')).toBe(true);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
         expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
         view.onKeyDown({
             target: textarea,
@@ -96,8 +96,8 @@ describe("A Chat Message", function () {
         });
         expect(textarea.value).toBe('');
         expect(view.model.messages.at(0).get('correcting')).toBe(false);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => (u.hasClass('correcting', view.querySelector('.chat-msg')) === false), 500);
 
         textarea.value = 'It is the east, and Juliet is the one.';
         view.onKeyDown({
@@ -106,7 +106,7 @@ describe("A Chat Message", function () {
             keyCode: 13 // Enter
         });
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(2);
 
         textarea.value =  'Arise, fair sun, and kill the envious moon';
         view.onKeyDown({
@@ -115,7 +115,7 @@ describe("A Chat Message", function () {
             keyCode: 13 // Enter
         });
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(3);
 
         view.onKeyDown({
             target: textarea,
@@ -147,7 +147,7 @@ describe("A Chat Message", function () {
         });
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         await u.waitUntil(() => textarea.value === '');
-        const messages = view.el.querySelectorAll('.chat-msg');
+        const messages = view.querySelectorAll('.chat-msg');
         expect(messages.length).toBe(3);
         expect(messages[0].querySelector('.chat-msg__text').textContent)
             .toBe('But soft, what light through yonder window breaks?');
@@ -173,7 +173,7 @@ describe("A Chat Message", function () {
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         await mock.openChatBoxFor(_converse, contact_jid);
         const view = _converse.api.chatviews.get(contact_jid);
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
 
         textarea.value = 'But soft, what light through yonder airlock breaks?';
         view.onKeyDown({
@@ -183,14 +183,14 @@ describe("A Chat Message", function () {
         });
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
 
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        expect(view.el.querySelector('.chat-msg__text').textContent)
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelector('.chat-msg__text').textContent)
             .toBe('But soft, what light through yonder airlock breaks?');
         expect(textarea.value).toBe('');
 
         const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .chat-msg__action').length === 2);
-        let action = view.el.querySelector('.chat-msg .chat-msg__action');
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg .chat-msg__action').length === 2);
+        let action = view.querySelector('.chat-msg .chat-msg__action');
         expect(action.textContent.trim()).toBe('Edit');
 
         action.style.opacity = 1;
@@ -198,8 +198,8 @@ describe("A Chat Message", function () {
 
         expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
         expect(view.model.messages.at(0).get('correcting')).toBe(true);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')));
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')));
 
         spyOn(_converse.connection, 'send');
         textarea.value = 'But soft, what light through yonder window breaks?';
@@ -231,26 +231,26 @@ describe("A Chat Message", function () {
         expect(keys.length).toBe(1);
         expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
 
-        await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')) === false);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
 
         // Test that clicking the pencil icon a second time cancels editing.
-        action = view.el.querySelector('.chat-msg .chat-msg__action');
+        action = view.querySelector('.chat-msg .chat-msg__action');
         action.style.opacity = 1;
         action.click();
 
         expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
         expect(view.model.messages.at(0).get('correcting')).toBe(true);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')) === true);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')) === true);
 
-        action = view.el.querySelector('.chat-msg .chat-msg__action');
+        action = view.querySelector('.chat-msg .chat-msg__action');
         action.style.opacity = 1;
         action.click();
         expect(textarea.value).toBe('');
         expect(view.model.messages.at(0).get('correcting')).toBe(false);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => (u.hasClass('correcting', view.querySelector('.chat-msg')) === false), 500);
 
         // Test that messages from other users don't have the pencil icon
         _converse.handleMessageStanza(
@@ -263,12 +263,12 @@ describe("A Chat Message", function () {
             .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
         );
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
+        expect(view.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
 
         // Test confirmation dialog
         spyOn(window, 'confirm').and.returnValue(true);
         textarea.value = 'But soft, what light through yonder airlock breaks?';
-        action = view.el.querySelector('.chat-msg .chat-msg__action');
+        action = view.querySelector('.chat-msg .chat-msg__action');
         action.style.opacity = 1;
         action.click();
         expect(window.confirm).toHaveBeenCalledWith(
@@ -307,8 +307,8 @@ describe("A Chat Message", function () {
                     'id': msg_id,
                 }).c('body').t('But soft, what light through yonder airlock breaks?').tree());
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-            expect(view.el.querySelector('.chat-msg__text').textContent)
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.querySelector('.chat-msg__text').textContent)
                 .toBe('But soft, what light through yonder airlock breaks?');
 
             _converse.handleMessageStanza($msg({
@@ -320,10 +320,10 @@ describe("A Chat Message", function () {
                 .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
 
-            expect(view.el.querySelector('.chat-msg__text').textContent)
+            expect(view.querySelector('.chat-msg__text').textContent)
                 .toBe('But soft, what light through yonder chimney breaks?');
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-            expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
             expect(view.model.messages.models.length).toBe(1);
 
             _converse.handleMessageStanza($msg({
@@ -335,11 +335,11 @@ describe("A Chat Message", function () {
                 .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
 
-            expect(view.el.querySelector('.chat-msg__text').textContent)
+            expect(view.querySelector('.chat-msg__text').textContent)
                 .toBe('But soft, what light through yonder window breaks?');
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-            expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
-            view.el.querySelector('.chat-msg__content .fa-edit').click();
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
+            view.querySelector('.chat-msg__content .fa-edit').click();
 
             const modal = _converse.api.modal.get('message-versions-modal');
             await u.waitUntil(() => u.isVisible(modal.el), 1000);
@@ -382,9 +382,9 @@ describe("A Groupchat Message", function () {
                 'id': msg_id,
             }).c('body').t('But soft, what light through yonder airlock breaks?').tree());
 
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        expect(view.el.querySelector('.chat-msg__text').textContent)
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelector('.chat-msg__text').textContent)
             .toBe('But soft, what light through yonder airlock breaks?');
 
         await view.model.handleMessageStanza($msg({
@@ -394,10 +394,10 @@ describe("A Groupchat Message", function () {
                 'id': u.getUniqueId(),
             }).c('body').t('But soft, what light through yonder chimney breaks?').up()
                 .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
-        await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
+        await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent ===
             'But soft, what light through yonder chimney breaks?', 500);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit'));
 
         await view.model.handleMessageStanza($msg({
                 'from': 'lounge@montague.lit/newguy',
@@ -407,11 +407,11 @@ describe("A Groupchat Message", function () {
             }).c('body').t('But soft, what light through yonder window breaks?').up()
                 .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
 
-        await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
+        await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent ===
             'But soft, what light through yonder window breaks?', 500);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
-        const edit = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
+        const edit = await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit'));
         edit.click();
         const modal = _converse.api.modal.get('message-versions-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 1000);
@@ -461,11 +461,11 @@ describe("A Groupchat Message", function () {
             'id': msg_id,
         }).c('body').t('But soft, what light through yonder airlock breaks?').tree());
 
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
-        expect(view.el.querySelectorAll('.chat-msg__text')[0].textContent)
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(2);
+        expect(view.querySelectorAll('.chat-msg__text')[0].textContent)
             .toBe('But soft, what light through yonder airlock breaks?');
-        expect(view.el.querySelectorAll('.chat-msg__text')[1].textContent)
+        expect(view.querySelectorAll('.chat-msg__text')[1].textContent)
         .toBe('But soft, what light through yonder airlock breaks?');
 
         // First message correction
@@ -477,10 +477,10 @@ describe("A Groupchat Message", function () {
             }).c('body').t('But soft, what light through yonder chimney breaks?').up()
                 .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
 
-        await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
+        await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent ===
             'But soft, what light through yonder chimney breaks?', 500);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
-        await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
+        expect(view.querySelectorAll('.chat-msg').length).toBe(2);
+        await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit'));
 
         // Second message correction
         await view.model.handleMessageStanza($msg({
@@ -499,15 +499,15 @@ describe("A Groupchat Message", function () {
             'id': u.getUniqueId(),
         }).c('body').t('But soft, what light through yonder window breaks?').tree());
 
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text')[0].textContent ===
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__text')[0].textContent ===
             'But soft, what light through yonder window breaks?', 500);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 3);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text')[2].textContent ===
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__text')[2].textContent ===
             'But soft, what light through yonder window breaks?', 500);
 
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
-        expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
-        const edit = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
+        expect(view.querySelectorAll('.chat-msg').length).toBe(3);
+        expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
+        const edit = await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit'));
         edit.click();
         const modal = _converse.api.modal.get('message-versions-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 1000);
@@ -528,7 +528,7 @@ describe("A Groupchat Message", function () {
         const muc_jid = 'lounge@montague.lit';
         await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
         const view = _converse.api.chatviews.get(muc_jid);
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         expect(textarea.value).toBe('');
         view.onKeyDown({
             target: textarea,
@@ -542,8 +542,8 @@ describe("A Groupchat Message", function () {
             preventDefault: function preventDefault () {},
             keyCode: 13 // Enter
         });
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
-        expect(view.el.querySelector('.chat-msg__text').textContent)
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
+        expect(view.querySelector('.chat-msg__text').textContent)
             .toBe('But soft, what light through yonder airlock breaks?');
 
         const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
@@ -554,8 +554,8 @@ describe("A Groupchat Message", function () {
         });
         expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
         expect(view.model.messages.at(0).get('correcting')).toBe(true);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')));
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')));
 
         spyOn(_converse.connection, 'send');
         textarea.value = 'But soft, what light through yonder window breaks?';
@@ -588,8 +588,8 @@ describe("A Groupchat Message", function () {
         expect(keys.length).toBe(1);
         expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
 
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(u.hasClass('correcting', view.querySelector('.chat-msg'))).toBe(false);
 
         // Check that messages from other users are skipped
         await view.model.handleMessageStanza($msg({
@@ -599,7 +599,7 @@ describe("A Groupchat Message", function () {
             'type': 'groupchat'
         }).c('body').t('Hello world').tree());
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(2);
 
         // Test that pressing the down arrow cancels message correction
         expect(textarea.value).toBe('');
@@ -609,8 +609,8 @@ describe("A Groupchat Message", function () {
         });
         expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
         expect(view.model.messages.at(0).get('correcting')).toBe(true);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
-        await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(2);
+        await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
         expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
         view.onKeyDown({
             target: textarea,
@@ -618,8 +618,8 @@ describe("A Groupchat Message", function () {
         });
         expect(textarea.value).toBe('');
         expect(view.model.messages.at(0).get('correcting')).toBe(false);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
-        await u.waitUntil(() => !u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(2);
+        await u.waitUntil(() => !u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
         done();
     }));
 });

+ 47 - 47
spec/emojis.js

@@ -21,12 +21,12 @@ describe("Emojis", function () {
             await mock.openControlBox(_converse);
             await mock.openChatBoxFor(_converse, contact_jid);
             const view = _converse.chatboxviews.get(contact_jid);
-            const toolbar = await u.waitUntil(() => view.el.querySelector('converse-chat-toolbar'));
+            const toolbar = await u.waitUntil(() => view.querySelector('converse-chat-toolbar'));
             toolbar.querySelector('.toggle-emojis').click();
-            await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')), 1000);
-            const item = view.el.querySelector('.emoji-picker li.insert-emoji a');
+            await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')), 1000);
+            const item = view.querySelector('.emoji-picker li.insert-emoji a');
             item.click()
-            expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
+            expect(view.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
             toolbar.querySelector('.toggle-emojis').click(); // Close the panel again
             done();
         }));
@@ -39,8 +39,8 @@ describe("Emojis", function () {
             const muc_jid = 'lounge@montague.lit';
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => view.el.querySelector('converse-emoji-dropdown'));
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = ':gri';
 
             // Press tab
@@ -52,14 +52,14 @@ describe("Emojis", function () {
                 'key': 'Tab'
             }
             view.onKeyDown(tab_event);
-            await u.waitUntil(() => view.el.querySelector('converse-emoji-picker .emoji-search').value === ':gri');
-            await u.waitUntil(() =>  sizzle('.emojis-lists__container--search .insert-emoji', view.el).length === 3, 1000);
-            let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', view.el);
+            await u.waitUntil(() => view.querySelector('converse-emoji-picker .emoji-search').value === ':gri');
+            await u.waitUntil(() =>  sizzle('.emojis-lists__container--search .insert-emoji', view).length === 3, 1000);
+            let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', view);
             expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':grimacing:');
             expect(visible_emojis[1].getAttribute('data-emoji')).toBe(':grin:');
             expect(visible_emojis[2].getAttribute('data-emoji')).toBe(':grinning:');
 
-            const picker = view.el.querySelector('converse-emoji-picker');
+            const picker = view.querySelector('converse-emoji-picker');
             const input = picker.querySelector('.emoji-search');
             // Test that TAB autocompletes the to first match
             input.dispatchEvent(new KeyboardEvent('keydown', tab_event));
@@ -92,7 +92,7 @@ describe("Emojis", function () {
 
             textarea.value = ':use';
             view.onKeyDown(tab_event);
-            await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')));
+            await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
             await u.waitUntil(() => input.value === ':use');
             visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker);
             expect(visible_emojis.length).toBe(0);
@@ -107,8 +107,8 @@ describe("Emojis", function () {
             const muc_jid = 'lounge@montague.lit';
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => view.el.querySelector('converse-emoji-dropdown'));
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = ':';
             // Press tab
             const tab_event = {
@@ -119,9 +119,9 @@ describe("Emojis", function () {
                 'key': 'Tab'
             }
             view.onKeyDown(tab_event);
-            await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')));
+            await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
 
-            const picker = view.el.querySelector('converse-emoji-picker');
+            const picker = view.querySelector('converse-emoji-picker');
             const input = picker.querySelector('.emoji-search');
             expect(input.value).toBe(':');
             input.value = ':gri';
@@ -131,8 +131,8 @@ describe("Emojis", function () {
                 'stopPropagation': function stopPropagation () {}
             };
             input.dispatchEvent(new KeyboardEvent('keydown', event));
-            await u.waitUntil(() =>  sizzle('.emojis-lists__container--search .insert-emoji', view.el).length === 3, 1000);
-            let emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view.el).pop();
+            await u.waitUntil(() =>  sizzle('.emojis-lists__container--search .insert-emoji', view).length === 3, 1000);
+            let emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view).pop();
             emoji.click();
             await u.waitUntil(() => textarea.value === ':grinning: ');
             textarea.value = ':grinning: :';
@@ -141,8 +141,8 @@ describe("Emojis", function () {
             await u.waitUntil(() => input.value === ':');
             input.value = ':grimacing';
             input.dispatchEvent(new KeyboardEvent('keydown', event));
-            await u.waitUntil(() =>  sizzle('.emojis-lists__container--search .insert-emoji', view.el).length === 1, 1000);
-            emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view.el).pop();
+            await u.waitUntil(() =>  sizzle('.emojis-lists__container--search .insert-emoji', view).length === 1, 1000);
+            emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view).pop();
             emoji.click();
             await u.waitUntil(() => textarea.value === ':grinning: :grimacing: ');
             done();
@@ -157,8 +157,8 @@ describe("Emojis", function () {
             const muc_jid = 'lounge@montague.lit';
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => view.el.querySelector('converse-emoji-dropdown'));
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = ':gri';
 
             // Press tab
@@ -171,8 +171,8 @@ describe("Emojis", function () {
             }
             textarea.value = ':';
             view.onKeyDown(tab_event);
-            await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')));
-            const picker = view.el.querySelector('converse-emoji-picker');
+            await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
+            const picker = view.querySelector('converse-emoji-picker');
             const input = picker.querySelector('.emoji-search');
             input.dispatchEvent(new KeyboardEvent('keydown', tab_event));
             await u.waitUntil(() => input.value === ':100:');
@@ -182,12 +182,12 @@ describe("Emojis", function () {
 
             textarea.value = ':';
             view.onKeyDown(tab_event);
-            await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')));
+            await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
             await u.waitUntil(() => input.value === ':');
             input.dispatchEvent(new KeyboardEvent('keydown', tab_event));
             await u.waitUntil(() => input.value === ':100:');
-            await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view.el).length === 1, 1000);
-            const emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view.el).pop();
+            await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view).length === 1, 1000);
+            const emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view).pop();
             emoji.click();
             expect(textarea.value).toBe(':100: ');
             done();
@@ -202,13 +202,13 @@ describe("Emojis", function () {
             const muc_jid = 'lounge@montague.lit';
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => view.el.querySelector('converse-emoji-dropdown'));
-            const toolbar = view.el.querySelector('converse-chat-toolbar');
+            await u.waitUntil(() => view.querySelector('converse-emoji-dropdown'));
+            const toolbar = view.querySelector('converse-chat-toolbar');
             toolbar.querySelector('.toggle-emojis').click();
-            await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')));
-            await u.waitUntil(() => sizzle('converse-chat-toolbar .insert-emoji:not(.hidden)', view.el).length === 1589);
+            await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
+            await u.waitUntil(() => sizzle('converse-chat-toolbar .insert-emoji:not(.hidden)', view).length === 1589);
 
-            const input = view.el.querySelector('.emoji-search');
+            const input = view.querySelector('.emoji-search');
             input.value = 'smiley';
             const event = {
                 'target': input,
@@ -217,8 +217,8 @@ describe("Emojis", function () {
             };
             input.dispatchEvent(new KeyboardEvent('keydown', event));
 
-            await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view.el).length === 2, 1000);
-            let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view.el);
+            await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view).length === 2, 1000);
+            let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view);
             expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:');
             expect(visible_emojis[1].getAttribute('data-emoji')).toBe(':smiley_cat:');
 
@@ -230,25 +230,25 @@ describe("Emojis", function () {
             // Check that search results update when chars are deleted
             input.value = 'sm';
             input.dispatchEvent(new KeyboardEvent('keydown', event));
-            await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view.el).length === 25, 1000);
+            await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view).length === 25, 1000);
 
             input.value = 'smiley';
             input.dispatchEvent(new KeyboardEvent('keydown', event));
-            await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view.el).length === 2, 1000);
+            await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden)', view).length === 2, 1000);
 
             // Test that TAB autocompletes the to first match
             const tab_event = Object.assign({}, event, {'keyCode': 9, 'key': 'Tab'});
             input.dispatchEvent(new KeyboardEvent('keydown', tab_event));
 
             await u.waitUntil(() => input.value === ':smiley:');
-            await u.waitUntil(() => sizzle(".emojis-lists__container--search .insert-emoji:not('.hidden')", view.el).length === 1, 1000);
-            visible_emojis = sizzle(".emojis-lists__container--search .insert-emoji:not('.hidden')", view.el);
+            await u.waitUntil(() => sizzle(".emojis-lists__container--search .insert-emoji:not('.hidden')", view).length === 1, 1000);
+            visible_emojis = sizzle(".emojis-lists__container--search .insert-emoji:not('.hidden')", view);
             expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:');
 
             // Check that ENTER now inserts the match
             input.dispatchEvent(new KeyboardEvent('keydown', enter_event));
             await u.waitUntil(() => input.value === '');
-            expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
+            expect(view.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
             done();
         }));
     });
@@ -288,7 +288,7 @@ describe("Emojis", function () {
 
             // Test that a modified message that no longer contains only
             // emojis now renders normally again.
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = ':poop: :innocent:';
             view.onKeyDown({
                 target: textarea,
@@ -296,7 +296,7 @@ describe("Emojis", function () {
                 keyCode: 13 // Enter
             });
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(3);
             const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
             await u.waitUntil(() => view.content.querySelector(last_msg_sel).textContent === '💩 😇');
 
@@ -308,7 +308,7 @@ describe("Emojis", function () {
             expect(textarea.value).toBe('💩 😇');
             expect(view.model.messages.at(2).get('correcting')).toBe(true);
             sel = 'converse-chat-message:last-child .chat-msg'
-            await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector(sel)), 500);
+            await u.waitUntil(() => u.hasClass('correcting', view.querySelector(sel)), 500);
             textarea.value = textarea.value += 'This is no longer an emoji-only message';
             view.onKeyDown({
                 target: textarea,
@@ -368,7 +368,7 @@ describe("Emojis", function () {
             expect(imgs.length).toBe(1);
             expect(imgs[0].src).toBe(_converse.api.settings.get('emoji_image_path')+'/72x72/1f607.png');
 
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = ':poop: :innocent:';
             view.onKeyDown({
                 target: textarea,
@@ -411,15 +411,15 @@ describe("Emojis", function () {
             await mock.openChatBoxFor(_converse, contact_jid);
             const view = _converse.api.chatviews.get(contact_jid);
 
-            const toolbar = await u.waitUntil(() => view.el.querySelector('.chat-toolbar'));
+            const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
             toolbar.querySelector('.toggle-emojis').click();
-            await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')), 1000);
-            const picker = await u.waitUntil(() => view.el.querySelector('converse-emoji-picker'), 1000);
+            await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')), 1000);
+            const picker = await u.waitUntil(() => view.querySelector('converse-emoji-picker'), 1000);
             const custom_category = picker.querySelector('.pick-category[data-category="custom"]');
             expect(custom_category.innerHTML.replace(/<!---->/g, '').trim()).toBe(
                 '<img class="emoji" draggable="false" title=":xmpp:" alt=":xmpp:" src="/dist/images/custom_emojis/xmpp.png">');
 
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = 'Running tests for :converse:';
             view.onKeyDown({
                 target: textarea,
@@ -427,7 +427,7 @@ describe("Emojis", function () {
                 keyCode: 13 // Enter
             });
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            const body = view.el.querySelector('converse-chat-message-body');
+            const body = view.querySelector('converse-chat-message-body');
             await u.waitUntil(() => body.innerHTML.replace(/<!---->/g, '').trim() ===
                 'Running tests for <img class="emoji" draggable="false" title=":converse:" alt=":converse:" src="/dist/images/custom_emojis/converse.png">');
             done();

+ 5 - 5
spec/hats.js

@@ -25,7 +25,7 @@ describe("A XEP-0317 MUC Hat", function () {
                 </hats>
             </presence>
         `)));
-        await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+        await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent.trim() ===
             "romeo and Terry have entered the groupchat");
 
         let hats = view.model.getOccupant("Terry").get('hats');
@@ -38,7 +38,7 @@ describe("A XEP-0317 MUC Hat", function () {
             </message>
         `)));
 
-        const msg_el = await u.waitUntil(() => view.el.querySelector('.chat-msg'));
+        const msg_el = await u.waitUntil(() => view.querySelector('.chat-msg'));
         let badges = Array.from(msg_el.querySelectorAll('.badge'));
         expect(badges.length).toBe(2);
         expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage");
@@ -60,8 +60,8 @@ describe("A XEP-0317 MUC Hat", function () {
         await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 3);
         hats = view.model.getOccupant("Terry").get('hats');
         expect(hats.map(h => h.title).join(' ')).toBe("Teacher's Assistant Dark Mage Mad hatter");
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 3, 1000);
-        badges = Array.from(view.el.querySelectorAll('.chat-msg .badge'));
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg .badge').length === 3, 1000);
+        badges = Array.from(view.querySelectorAll('.chat-msg .badge'));
         expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage Mad hatter");
 
         _converse.connection._dataRecv(mock.createRequest(u.toStanza(`
@@ -72,7 +72,7 @@ describe("A XEP-0317 MUC Hat", function () {
             </presence>
         `)));
         await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 0);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 0);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg .badge').length === 0);
         done();
     }));
 })

+ 9 - 9
spec/headline.js

@@ -62,7 +62,7 @@ describe("A headlines box", function () {
         await u.waitUntil(() => _converse.chatboxviews.keys().includes('notify.example.com'));
         const view = _converse.chatboxviews.get('notify.example.com');
         expect(view.model.get('show_avatar')).toBeFalsy();
-        expect(view.el.querySelector('img.avatar')).toBe(null);
+        expect(view.querySelector('img.avatar')).toBe(null);
         done();
     }));
 
@@ -96,9 +96,9 @@ describe("A headlines box", function () {
 
         _converse.connection._dataRecv(mock.createRequest(stanza));
         const view = _converse.chatboxviews.get('controlbox');
-        await u.waitUntil(() => view.el.querySelectorAll(".open-headline").length);
-        expect(view.el.querySelectorAll('.open-headline').length).toBe(1);
-        expect(view.el.querySelector('.open-headline').text).toBe('notify.example.com');
+        await u.waitUntil(() => view.querySelectorAll(".open-headline").length);
+        expect(view.querySelectorAll('.open-headline').length).toBe(1);
+        expect(view.querySelector('.open-headline').text).toBe('notify.example.com');
         done();
     }));
 
@@ -133,13 +133,13 @@ describe("A headlines box", function () {
 
         _converse.connection._dataRecv(mock.createRequest(stanza));
         const cbview = _converse.chatboxviews.get('controlbox');
-        await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length);
+        await u.waitUntil(() => cbview.querySelectorAll(".open-headline").length);
         const hlview = _converse.chatboxviews.get('notify.example.com');
-        await u.isVisible(hlview.el);
-        const close_el = await u.waitUntil(() => hlview.el.querySelector('.close-chatbox-button'));
+        await u.isVisible(hlview);
+        const close_el = await u.waitUntil(() => hlview.querySelector('.close-chatbox-button'));
         close_el.click();
-        await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length === 0);
-        expect(cbview.el.querySelectorAll('.open-headline').length).toBe(0);
+        await u.waitUntil(() => cbview.querySelectorAll(".open-headline").length === 0);
+        expect(cbview.querySelectorAll('.open-headline').length).toBe(0);
         done();
     }));
 

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

@@ -158,7 +158,7 @@ describe("XEP-0363: HTTP File Upload", function () {
 
                 await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], [], 'items');
                 const view = _converse.chatboxviews.get(contact_jid);
-                expect(view.el.querySelector('.chat-toolbar .fileupload')).toBe(null);
+                expect(view.querySelector('.chat-toolbar .fileupload')).toBe(null);
                 done();
             }));
 
@@ -174,7 +174,7 @@ describe("XEP-0363: HTTP File Upload", function () {
 
                 await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], [], 'items');
                 const view = _converse.chatboxviews.get('lounge@montague.lit');
-                await u.waitUntil(() => view.el.querySelector('.chat-toolbar .fileupload') === null);
+                await u.waitUntil(() => view.querySelector('.chat-toolbar .fileupload') === null);
                 expect(1).toBe(1);
                 done();
             }));
@@ -198,7 +198,7 @@ describe("XEP-0363: HTTP File Upload", function () {
                 const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
                 await mock.openChatBoxFor(_converse, contact_jid);
                 const view = _converse.chatboxviews.get(contact_jid);
-                const el = await u.waitUntil(() => view.el.querySelector('.chat-toolbar .fileupload'));
+                const el = await u.waitUntil(() => view.querySelector('.chat-toolbar .fileupload'));
                 expect(el).not.toEqual(null);
                 done();
             }));
@@ -217,7 +217,7 @@ describe("XEP-0363: HTTP File Upload", function () {
                 await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
                 await u.waitUntil(() => _converse.chatboxviews.get('lounge@montague.lit').el.querySelector('.fileupload'));
                 const view = _converse.chatboxviews.get('lounge@montague.lit');
-                expect(view.el.querySelector('.chat-toolbar .fileupload')).not.toBe(null);
+                expect(view.querySelector('.chat-toolbar .fileupload')).not.toBe(null);
                 done();
             }));
 
@@ -283,12 +283,12 @@ describe("XEP-0363: HTTP File Upload", function () {
 
                     spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
                         const message = view.model.messages.at(0);
-                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                        expect(view.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
                         message.set('progress', 0.5);
-                        u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
+                        u.waitUntil(() => view.querySelector('.chat-content progress').getAttribute('value') === '0.5')
                         .then(() => {
                             message.set('progress', 1);
-                            u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1')
+                            u.waitUntil(() => view.querySelector('.chat-content progress').getAttribute('value') === '1')
                         }).then(() => {
                             message.save({
                                 'upload': _converse.SUCCESS,
@@ -317,13 +317,13 @@ describe("XEP-0363: HTTP File Upload", function () {
                                 `</x>`+
                                 `<origin-id id="${sent_stanza.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
                         `</message>`);
-                    const img_link_el = await u.waitUntil(() => view.el.querySelector('converse-chat-message-body .chat-image__link'), 1000);
+                    const img_link_el = await u.waitUntil(() => view.querySelector('converse-chat-message-body .chat-image__link'), 1000);
                     // Check that the image renders
                     expect(img_link_el.outerHTML.replace(/<!---->/g, '').trim()).toEqual(
                         `<a class="chat-image__link" target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
                         `<img class="chat-image img-thumbnail" src="${base_url}/logo/conversejs-filled.svg"></a>`);
 
-                    expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!---->/g, '').trim()).toEqual(
+                    expect(view.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!---->/g, '').trim()).toEqual(
                         `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
                         `Download image file "conversejs-filled.svg"</a>`);
                     XMLHttpRequest.prototype.send = send_backup;
@@ -391,12 +391,12 @@ describe("XEP-0363: HTTP File Upload", function () {
 
                     spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
                         const message = view.model.messages.at(0);
-                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                        expect(view.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
                         message.set('progress', 0.5);
-                        u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
+                        u.waitUntil(() => view.querySelector('.chat-content progress').getAttribute('value') === '0.5')
                         .then(() => {
                             message.set('progress', 1);
-                            u.waitUntil(() => view.el.querySelector('.chat-content progress')?.getAttribute('value') === '1')
+                            u.waitUntil(() => view.querySelector('.chat-content progress')?.getAttribute('value') === '1')
                         }).then(() => {
                             message.save({
                                 'upload': _converse.SUCCESS,
@@ -425,13 +425,13 @@ describe("XEP-0363: HTTP File Upload", function () {
                                 `</x>`+
                                 `<origin-id id="${sent_stanza.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
                         `</message>`);
-                    const img_link_el = await u.waitUntil(() => view.el.querySelector('converse-chat-message-body .chat-image__link'), 1000);
+                    const img_link_el = await u.waitUntil(() => view.querySelector('converse-chat-message-body .chat-image__link'), 1000);
                     // Check that the image renders
                     expect(img_link_el.outerHTML.replace(/<!---->/g, '').trim()).toEqual(
                         `<a class="chat-image__link" target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
                         `<img class="chat-image img-thumbnail" src="${base_url}/logo/conversejs-filled.svg"></a>`);
 
-                    expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!---->/g, '').trim()).toEqual(
+                    expect(view.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!---->/g, '').trim()).toEqual(
                         `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
                         `Download image file "conversejs-filled.svg"</a>`);
 
@@ -548,8 +548,8 @@ describe("XEP-0363: HTTP File Upload", function () {
                         'name': "my-juliet.jpg"
                     };
                     view.model.sendFiles([file]);
-                    await u.waitUntil(() => view.el.querySelectorAll('.message').length)
-                    const messages = view.el.querySelectorAll('.message.chat-error');
+                    await u.waitUntil(() => view.querySelectorAll('.message').length)
+                    const messages = view.querySelectorAll('.message.chat-error');
                     expect(messages.length).toBe(1);
                     expect(messages[0].textContent.trim()).toBe(
                         'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.');
@@ -618,12 +618,12 @@ describe("XEP-0363: HTTP File Upload", function () {
 
                 spyOn(XMLHttpRequest.prototype, 'send').and.callFake(async () => {
                     const message = view.model.messages.at(0);
-                    expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                    expect(view.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
                     message.set('progress', 0.5);
-                    await u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5');
+                    await u.waitUntil(() => view.querySelector('.chat-content progress').getAttribute('value') === '0.5');
                     message.set('progress', 1);
-                    await u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1');
-                    expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
+                    await u.waitUntil(() => view.querySelector('.chat-content progress').getAttribute('value') === '1');
+                    expect(view.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
                     done();
                 });
                 _converse.connection._dataRecv(mock.createRequest(stanza));

+ 12 - 12
spec/login.js

@@ -13,25 +13,25 @@ describe("The Login Form", function () {
 
         const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
         mock.toggleControlBox();
-        const checkboxes = cbview.el.querySelectorAll('input[type="checkbox"]');
+        const checkboxes = cbview.querySelectorAll('input[type="checkbox"]');
         expect(checkboxes.length).toBe(1);
 
         const checkbox = checkboxes[0];
-        const label = cbview.el.querySelector(`label[for="${checkbox.getAttribute('id')}"]`);
+        const label = cbview.querySelector(`label[for="${checkbox.getAttribute('id')}"]`);
         expect(label.textContent).toBe('This is a trusted device');
         expect(checkbox.checked).toBe(true);
 
-        cbview.el.querySelector('input[name="jid"]').value = 'romeo@montague.lit';
-        cbview.el.querySelector('input[name="password"]').value = 'secret';
+        cbview.querySelector('input[name="jid"]').value = 'romeo@montague.lit';
+        cbview.querySelector('input[name="password"]').value = 'secret';
 
         expect(_converse.config.get('trusted')).toBe(true);
         expect(_converse.getDefaultStore()).toBe('persistent');
-        cbview.el.querySelector('input[type="submit"]').click();
+        cbview.querySelector('input[type="submit"]').click();
         expect(_converse.config.get('trusted')).toBe(true);
         expect(_converse.getDefaultStore()).toBe('persistent');
 
         checkbox.click();
-        cbview.el.querySelector('input[type="submit"]').click();
+        cbview.querySelector('input[type="submit"]').click();
         expect(_converse.config.get('trusted')).toBe(false);
         expect(_converse.getDefaultStore()).toBe('session');
         done();
@@ -48,23 +48,23 @@ describe("The Login Form", function () {
         await u.waitUntil(() => _converse.chatboxviews.get('controlbox'))
         const cbview = _converse.chatboxviews.get('controlbox');
         mock.toggleControlBox();
-        const checkboxes = cbview.el.querySelectorAll('input[type="checkbox"]');
+        const checkboxes = cbview.querySelectorAll('input[type="checkbox"]');
         expect(checkboxes.length).toBe(1);
 
         const checkbox = checkboxes[0];
-        const label = cbview.el.querySelector(`label[for="${checkbox.getAttribute('id')}"]`);
+        const label = cbview.querySelector(`label[for="${checkbox.getAttribute('id')}"]`);
         expect(label.textContent).toBe('This is a trusted device');
         expect(checkbox.checked).toBe(false);
 
-        cbview.el.querySelector('input[name="jid"]').value = 'romeo@montague.lit';
-        cbview.el.querySelector('input[name="password"]').value = 'secret';
+        cbview.querySelector('input[name="jid"]').value = 'romeo@montague.lit';
+        cbview.querySelector('input[name="password"]').value = 'secret';
 
-        cbview.el.querySelector('input[type="submit"]').click();
+        cbview.querySelector('input[type="submit"]').click();
         expect(_converse.config.get('trusted')).toBe(false);
         expect(_converse.getDefaultStore()).toBe('session');
 
         checkbox.click();
-        cbview.el.querySelector('input[type="submit"]').click();
+        cbview.querySelector('input[type="submit"]').click();
         expect(_converse.config.get('trusted')).toBe(true);
         expect(_converse.getDefaultStore()).toBe('persistent');
         done();

+ 2 - 2
spec/mam.js

@@ -1175,7 +1175,7 @@ describe("Chatboxes", function () {
             expect(view.model.messages.at(0).get('type')).toBe('error');
             expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.');
 
-            let err_message = await u.waitUntil(() => view.el.querySelector('.message.chat-error'));
+            let err_message = await u.waitUntil(() => view.querySelector('.message.chat-error'));
             err_message.querySelector('.retry').click();
 
             while (_converse.connection.IQ_stanzas.length) {
@@ -1226,7 +1226,7 @@ describe("Chatboxes", function () {
                         .c('count').t('2');
             _converse.connection._dataRecv(mock.createRequest(stanza));
             await u.waitUntil(() => view.model.messages.length === 2, 500);
-            err_message = view.el.querySelector('.message.chat-error');
+            err_message = view.querySelector('.message.chat-error');
             expect(err_message).toBe(null);
             done();
         }));

+ 13 - 13
spec/markers.js

@@ -98,7 +98,7 @@ describe("A XEP-0333 Chat Marker", function () {
             </message>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
         expect(view.model.messages.length).toBe(1);
 
         stanza = u.toStanza(
@@ -116,7 +116,7 @@ describe("A XEP-0333 Chat Marker", function () {
         spyOn(_converse.api, "trigger").and.callThrough();
         _converse.connection._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => _converse.api.trigger.calls.count(), 500);
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
         expect(view.model.messages.length).toBe(1);
         done();
     }));
@@ -131,7 +131,7 @@ describe("A XEP-0333 Chat Marker", function () {
         const muc_jid = 'lounge@montague.lit';
         await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
         const view = _converse.api.chatviews.get(muc_jid);
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         textarea.value = 'But soft, what light through yonder airlock breaks?';
         view.onKeyDown({
             target: textarea,
@@ -139,8 +139,8 @@ describe("A XEP-0333 Chat Marker", function () {
             keyCode: 13 // Enter
         });
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim())
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelector('.chat-msg .chat-msg__body').textContent.trim())
             .toBe("But soft, what light through yonder airlock breaks?");
 
         const msg_obj = view.model.messages.at(0);
@@ -150,8 +150,8 @@ describe("A XEP-0333 Chat Marker", function () {
                 <received xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
             </message>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
-        expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
+        expect(view.querySelectorAll('.chat-msg__receipt').length).toBe(0);
 
         stanza = u.toStanza(`
             <message xml:lang="en" to="romeo@montague.lit/orchard"
@@ -159,8 +159,8 @@ describe("A XEP-0333 Chat Marker", function () {
                 <displayed xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
             </message>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelectorAll('.chat-msg__receipt').length).toBe(0);
 
         stanza = u.toStanza(`
             <message xml:lang="en" to="romeo@montague.lit/orchard"
@@ -169,8 +169,8 @@ describe("A XEP-0333 Chat Marker", function () {
             </message>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
 
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-        expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelectorAll('.chat-msg__receipt').length).toBe(0);
 
         stanza = u.toStanza(`
             <message xml:lang="en" to="romeo@montague.lit/orchard"
@@ -179,8 +179,8 @@ describe("A XEP-0333 Chat Marker", function () {
                 <markable xmlns="urn:xmpp:chat-markers:0"/>
             </message>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
-        expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2);
+        expect(view.querySelectorAll('.chat-msg__receipt').length).toBe(0);
         done();
     }));
 });

+ 22 - 22
spec/me-messages.js

@@ -14,7 +14,7 @@ describe("A Groupchat Message", function () {
         await mock.waitForRoster(_converse, 'current');
         await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
         const view = _converse.chatboxviews.get('lounge@montague.lit');
-        if (!view.el.querySelectorAll('.chat-area').length) {
+        if (!view.querySelectorAll('.chat-area').length) {
             view.renderChatArea();
         }
         let message = '/me is tired';
@@ -27,8 +27,8 @@ describe("A Groupchat Message", function () {
             }).c('body').t(message).tree();
         await view.model.handleMessageStanza(msg);
         await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop());
-        expect(view.el.querySelector('.chat-msg__author').textContent.includes('**Dyon van de Wege')).toBeTruthy();
-        expect(view.el.querySelector('.chat-msg__text').textContent.trim()).toBe('is tired');
+        expect(view.querySelector('.chat-msg__author').textContent.includes('**Dyon van de Wege')).toBeTruthy();
+        expect(view.querySelector('.chat-msg__text').textContent.trim()).toBe('is tired');
 
         message = '/me is as well';
         msg = $msg({
@@ -38,9 +38,9 @@ describe("A Groupchat Message", function () {
             type: 'groupchat'
         }).c('body').t(message).tree();
         await view.model.handleMessageStanza(msg);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
-        expect(sizzle('.chat-msg__author:last', view.el).pop().textContent.includes('**Romeo Montague')).toBeTruthy();
-        expect(sizzle('.chat-msg__text:last', view.el).pop().textContent.trim()).toBe('is as well');
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2);
+        expect(sizzle('.chat-msg__author:last', view).pop().textContent.includes('**Romeo Montague')).toBeTruthy();
+        expect(sizzle('.chat-msg__text:last', view).pop().textContent.trim()).toBe('is as well');
 
         // Check rendering of a mention inside a me message
         const msg_text = "/me mentions romeo";
@@ -52,8 +52,8 @@ describe("A Groupchat Message", function () {
             }).c('body').t(msg_text).up()
                 .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'13', 'end':'19', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).nodeTree;
         await view.model.handleMessageStanza(msg);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 3);
-        await u.waitUntil(() => sizzle('.chat-msg__text:last', view.el).pop().innerHTML.replace(/<!---->/g, '') ===
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
+        await u.waitUntil(() => sizzle('.chat-msg__text:last', view).pop().innerHTML.replace(/<!---->/g, '') ===
             'mentions <span class="mention mention--self badge badge-info">romeo</span>');
         done();
     }));
@@ -79,16 +79,16 @@ describe("A Message", function () {
 
         await _converse.handleMessageStanza(msg);
         const view = _converse.chatboxviews.get(sender_jid);
-        await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
-        expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
-        expect(view.el.querySelector('.chat-msg__author').textContent.includes('**Mercutio')).toBeTruthy();
-        expect(view.el.querySelector('.chat-msg__text').textContent).toBe('is tired');
+        await u.waitUntil(() => view.querySelector('.chat-msg__text'));
+        expect(view.querySelectorAll('.chat-msg--action').length).toBe(1);
+        expect(view.querySelector('.chat-msg__author').textContent.includes('**Mercutio')).toBeTruthy();
+        expect(view.querySelector('.chat-msg__text').textContent).toBe('is tired');
 
         message = '/me is as well';
         await mock.sendMessage(view, message);
-        expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
-        await u.waitUntil(() => sizzle('.chat-msg__author:last', view.el).pop().textContent.trim() === '**Romeo Montague');
-        const last_el = sizzle('.chat-msg__text:last', view.el).pop();
+        expect(view.querySelectorAll('.chat-msg--action').length).toBe(2);
+        await u.waitUntil(() => sizzle('.chat-msg__author:last', view).pop().textContent.trim() === '**Romeo Montague');
+        const last_el = sizzle('.chat-msg__text:last', view).pop();
         await u.waitUntil(() => last_el.textContent === 'is as well');
         expect(u.hasClass('chat-msg--followup', last_el)).toBe(false);
 
@@ -97,18 +97,18 @@ describe("A Message", function () {
         message = 'This a normal message';
         await mock.sendMessage(view, message);
         const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__text';
-        await u.waitUntil(() => view.el.querySelector(msg_txt_sel).textContent.trim() === message);
-        let el = view.el.querySelector('converse-chat-message:last-child .chat-msg__body');
+        await u.waitUntil(() => view.querySelector(msg_txt_sel).textContent.trim() === message);
+        let el = view.querySelector('converse-chat-message:last-child .chat-msg__body');
         expect(u.hasClass('chat-msg--followup', el)).toBeFalsy();
 
         message = '/me wrote a 3rd person message';
         await mock.sendMessage(view, message);
-        await u.waitUntil(() => view.el.querySelector(msg_txt_sel).textContent.trim() === message.replace('/me ', ''));
-        el = view.el.querySelector('converse-chat-message:last-child .chat-msg__body');
-        expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3);
+        await u.waitUntil(() => view.querySelector(msg_txt_sel).textContent.trim() === message.replace('/me ', ''));
+        el = view.querySelector('converse-chat-message:last-child .chat-msg__body');
+        expect(view.querySelectorAll('.chat-msg--action').length).toBe(3);
 
-        expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe('wrote a 3rd person message');
-        expect(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy();
+        expect(sizzle('.chat-msg__text:last', view).pop().textContent).toBe('wrote a 3rd person message');
+        expect(u.isVisible(sizzle('.chat-msg__author:last', view).pop())).toBeTruthy();
         done();
     }));
 });

+ 14 - 14
spec/mentions.js

@@ -14,7 +14,7 @@ describe("An incoming groupchat message", function () {
         const muc_jid = 'lounge@montague.lit';
         await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
         const view = _converse.api.chatviews.get(muc_jid);
-        if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
+        if (!view.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
         const message = 'romeo: Your attention is required';
         const nick = mock.chatroom_names[0],
             msg = $msg({
@@ -25,7 +25,7 @@ describe("An incoming groupchat message", function () {
             }).c('body').t(message).tree();
         await view.model.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy();
+        expect(u.hasClass('mentioned', view.querySelector('.chat-msg'))).toBeTruthy();
         done();
     }));
 
@@ -62,7 +62,7 @@ describe("An incoming groupchat message", function () {
                 .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up()
                 .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree;
         await view.model.handleMessageStanza(msg);
-        let message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
+        let message = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
         expect(message.classList.length).toEqual(1);
         expect(message.innerHTML.replace(/<!---->/g, '')).toBe(
             'hello <span class="mention">z3r0</span> '+
@@ -77,7 +77,7 @@ describe("An incoming groupchat message", function () {
             }).c('body').t('https://conversejs.org/@gibson').up()
                 .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'23', 'end':'29', 'type':'mention', 'uri':'xmpp:gibson@montague.lit'}).nodeTree;
         await view.model.handleMessageStanza(msg);
-        message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
+        message = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
         expect(message.classList.length).toEqual(1);
         expect(message.innerHTML.replace(/<!---->/g, '')).toBe(
             'hello <span class="mention">z3r0</span> '+
@@ -119,7 +119,7 @@ describe("An incoming groupchat message", function () {
                 .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'16', 'end':'24', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree;
 
         await view.model.handleMessageStanza(msg);
-        const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
+        const message = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
         expect(message.classList.length).toEqual(1);
         expect(message.innerHTML.replace(/<!---->/g, '')).toBe(
             '<blockquote>hello <span class="mention">z3r0</span> <span class="mention mention--self badge badge-info">tom</span> <span class="mention">mr.robot</span>, how are you?</blockquote>');
@@ -318,7 +318,7 @@ describe("A sent groupchat message", function () {
                 })));
             await u.waitUntil(() => view.model.occupants.length === 2);
 
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = 'hello @Link Mauve'
             const enter_event = {
                 'target': textarea,
@@ -379,7 +379,7 @@ describe("A sent groupchat message", function () {
             });
             await u.waitUntil(() => view.model.occupants.length === 5);
 
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?'
             const enter_event = {
                 'target': textarea,
@@ -410,19 +410,19 @@ describe("A sent groupchat message", function () {
                             `<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
                         `</message>`);
 
-            const action = await u.waitUntil(() => view.el.querySelector('.chat-msg .chat-msg__action'));
+            const action = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__action'));
             action.style.opacity = 1;
             action.click();
 
             expect(textarea.value).toBe('hello @z3r0 @gibson @mr.robot, how are you?');
             expect(view.model.messages.at(0).get('correcting')).toBe(true);
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-            await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+            await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
             await u.waitUntil(() => _converse.connection.send.calls.count() === 2);
 
             textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?';
             view.onKeyDown(enter_event);
-            await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
+            await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent ===
                 'hello z3r0 gibson sw0rdf1sh, how are you?', 500);
 
             const correction = _converse.connection.send.calls.all()[2].args[0];
@@ -465,7 +465,7 @@ describe("A sent groupchat message", function () {
             await u.waitUntil(() => view.model.occupants.length === 5);
 
             spyOn(_converse.connection, 'send');
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?'
             const enter_event = {
                 'target': textarea,
@@ -501,7 +501,7 @@ describe("A sent groupchat message", function () {
         const muc_jid = 'lounge@montague.lit';
         await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom', [], members);
         const view = _converse.api.chatviews.get(muc_jid);
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         textarea.value = "Welcome @gibson 💩 We have a guide on how to do that here: https://conversejs.org/docs/html/index.html";
         const enter_event = {
             'target': textarea,
@@ -510,7 +510,7 @@ describe("A sent groupchat message", function () {
             'keyCode': 13 // Enter
         }
         view.onKeyDown(enter_event);
-        const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
+        const message = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
         expect(message.innerHTML.replace(/<!---->/g, '')).toEqual(
             `Welcome <span class="mention">gibson</span> <span title=":poop:">💩</span> `+
             `We have a guide on how to do that here: `+

+ 43 - 43
spec/messages.js

@@ -15,7 +15,7 @@ describe("A Chat Message", function () {
         await mock.openChatBoxFor(_converse, contact_jid);
         const view = _converse.api.chatviews.get(contact_jid);
         await _converse.handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This message will be read'));
-        const msg_el = await u.waitUntil(() => view.el.querySelector('converse-chat-message'));
+        const msg_el = await u.waitUntil(() => view.querySelector('converse-chat-message'));
         expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('This message will be read');
         expect(view.model.get('num_unread')).toBe(0);
 
@@ -26,8 +26,8 @@ describe("A Chat Message", function () {
         expect(view.model.get('num_unread')).toBe(1);
         expect(view.model.get('first_unread_id')).toBe(view.model.messages.last().get('id'));
 
-        await u.waitUntil(() => view.el.querySelectorAll('converse-chat-message').length === 2);
-        const last_msg_el = view.el.querySelector('converse-chat-message:last-child');
+        await u.waitUntil(() => view.querySelectorAll('converse-chat-message').length === 2);
+        const last_msg_el = view.querySelector('converse-chat-message:last-child');
         expect(last_msg_el.firstElementChild?.textContent).toBe('New messages');
         done();
     }));
@@ -87,7 +87,7 @@ describe("A Chat Message", function () {
         await mock.openControlBox(_converse);
 
         const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
+        await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length)
         _converse.filter_by_resource = true;
 
         let msg = $msg({
@@ -174,7 +174,7 @@ describe("A Chat Message", function () {
             .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
             .tree();
         _converse.handleMessageStanza(msg);
-        const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
+        const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent);
         expect(csntext.trim()).toEqual('Mercutio is typing');
 
         msg = $msg({
@@ -365,7 +365,7 @@ describe("A Chat Message", function () {
         expect(msg_obj.get('sender')).toEqual('me');
         expect(msg_obj.get('is_delayed')).toEqual(false);
         // Now check that the message appears inside the chatbox in the DOM
-        const msg_el = await u.waitUntil(() => view.el.querySelector('.chat-content .chat-msg .chat-msg__text'));
+        const msg_el = await u.waitUntil(() => view.querySelector('.chat-content .chat-msg .chat-msg__text'));
         expect(msg_el.textContent).toEqual(msgtext);
         done();
     }));
@@ -428,7 +428,7 @@ describe("A Chat Message", function () {
         const contact_name = mock.cur_names[1];
         const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
-        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+        await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
         await mock.openChatBoxFor(_converse, contact_jid);
 
         const one_day_ago = dayjs().subtract(1, 'day');
@@ -646,7 +646,7 @@ describe("A Chat Message", function () {
         const view = _converse.api.chatviews.get(contact_jid);
         spyOn(view.model, 'sendMessage').and.callThrough();
         mock.sendMessage(view, message);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length, 1000)
+        await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length, 1000)
         expect(view.model.sendMessage).toHaveBeenCalled();
         let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
         expect(msg.innerHTML.replace(/<!---->/g, '').trim()).toEqual(
@@ -656,7 +656,7 @@ describe("A Chat Message", function () {
 
         message += "?param1=val1&param2=val2";
         mock.sendMessage(view, message);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 2, 1000);
+        await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 2, 1000);
         expect(view.model.sendMessage).toHaveBeenCalled();
         msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
         expect(msg.innerHTML.replace(/<!---->/g, '').trim()).toEqual(
@@ -667,7 +667,7 @@ describe("A Chat Message", function () {
         // Test now with two images in one message
         message += ' hello world '+base_url+"/logo/conversejs-filled.svg";
         mock.sendMessage(view, message);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 4, 1000);
+        await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 4, 1000);
         expect(view.model.sendMessage).toHaveBeenCalled();
         msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
         expect(msg.textContent.trim()).toEqual('hello world');
@@ -677,11 +677,11 @@ describe("A Chat Message", function () {
         _converse.api.settings.set('image_urls_regex', /^https?:\/\/(?:www.)?(?:imgur\.com\/\w{7})\/?$/i);
         message = 'https://imgur.com/oxymPax';
         mock.sendMessage(view, message);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 5, 1000);
+        await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 5, 1000);
         expect(view.content.querySelectorAll('.chat-content .chat-image').length).toBe(5);
 
         // Check that the Imgur URL gets a .png attached to make it render
-        await u.waitUntil(() => Array.from(view.el.querySelectorAll('.chat-content .chat-image')).pop().src.endsWith('png'), 1000);
+        await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-content .chat-image')).pop().src.endsWith('png'), 1000);
         done();
     }));
 
@@ -698,12 +698,12 @@ describe("A Chat Message", function () {
         const view = _converse.api.chatviews.get(contact_jid);
         spyOn(view.model, 'sendMessage').and.callThrough();
         mock.sendMessage(view, message);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length === 1);
+        await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length === 1);
 
         message = base_url+"/logo/conversejs-filled.svg";
         mock.sendMessage(view, message);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length === 2, 1000);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 1, 1000)
+        await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length === 2, 1000);
+        await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 1, 1000)
         expect(view.content.querySelectorAll('.chat-content .chat-image').length).toBe(1);
 
         done();
@@ -722,7 +722,7 @@ describe("A Chat Message", function () {
         const view = _converse.api.chatviews.get(contact_jid);
         spyOn(view.model, 'sendMessage').and.callThrough();
         mock.sendMessage(view, message);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length, 1000)
+        await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length, 1000)
         expect(view.model.sendMessage).toHaveBeenCalled();
         const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
         await u.waitUntil(() => msg.innerHTML.replace(/<!---->/g, '').trim() ==
@@ -770,10 +770,10 @@ describe("A Chat Message", function () {
         expect(chatbox.messages.models.length, 1);
         const msg_object = chatbox.messages.models[0];
 
-        const msg_author = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__author');
+        const msg_author = view.querySelector('.chat-content .chat-msg:last-child .chat-msg__author');
         expect(msg_author.textContent.trim()).toBe('Romeo Montague');
 
-        const msg_time = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__time');
+        const msg_time = view.querySelector('.chat-content .chat-msg:last-child .chat-msg__time');
         const time = dayjs(msg_object.get('time')).format(_converse.time_format);
         expect(msg_time.textContent).toBe(time);
         done();
@@ -790,7 +790,7 @@ describe("A Chat Message", function () {
         const base_time = new Date();
         const ONE_MINUTE_LATER = 60000;
 
-        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300);
+        await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 300);
         const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         _converse.filter_by_resource = true;
 
@@ -992,7 +992,7 @@ describe("A Chat Message", function () {
             const include_nick = false;
             await mock.waitForRoster(_converse, 'current', 1, include_nick);
             await mock.openControlBox(_converse);
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 300);
             spyOn(_converse.api, "trigger").and.callThrough();
             const message = 'This is a received message';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -1036,7 +1036,7 @@ describe("A Chat Message", function () {
                 async function (done, _converse) {
 
             await mock.waitForRoster(_converse, 'current', 1, false);
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length, 300);
             const message = '\n\n        This is a received message         \n\n';
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             await _converse.handleMessageStanza(
@@ -1100,13 +1100,13 @@ describe("A Chat Message", function () {
                 const chatbox = await _converse.api.chats.get(sender_jid);
                 expect(chatbox.get('fullname') === sender_jid);
 
-                await u.waitUntil(() => view.el.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio');
-                let author_el = view.el.querySelector('.chat-msg__author');
+                await u.waitUntil(() => view.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio');
+                let author_el = view.querySelector('.chat-msg__author');
                 expect( _.includes(author_el.textContent.trim(), 'Mercutio')).toBeTruthy();
                 await u.waitUntil(() => vcard_fetched, 100);
                 expect(_converse.api.vcard.get).toHaveBeenCalled();
                 await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0])
-                author_el = view.el.querySelector('.chat-msg__author');
+                author_el = view.querySelector('.chat-msg__author');
                 expect( _.includes(author_el.textContent.trim(), 'Mercutio')).toBeTruthy();
                 done();
             }));
@@ -1159,7 +1159,7 @@ describe("A Chat Message", function () {
                 expect(msg_obj.get('sender')).toEqual('them');
                 expect(msg_obj.get('is_delayed')).toEqual(false);
 
-                await u.waitUntil(() => view.el.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio');
+                await u.waitUntil(() => view.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio');
                 // Now check that the message appears inside the chatbox in the DOM
                 expect(view.content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
                 expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
@@ -1363,7 +1363,7 @@ describe("A Chat Message", function () {
             }
             await Promise.all(promises);
 
-            const indicator_el = view.el.querySelector('.new-msgs-indicator');
+            const indicator_el = view.querySelector('.new-msgs-indicator');
             expect(u.isVisible(indicator_el)).toBeTruthy();
 
             expect(view.model.get('scrolled')).toBe(true);
@@ -1380,7 +1380,7 @@ describe("A Chat Message", function () {
                 async function (done, _converse) {
 
             await mock.waitForRoster(_converse, 'current');
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length)
             // Send a message from a different resource
             spyOn(converse.env.log, 'error');
             spyOn(_converse.api.chatboxes, 'create').and.callThrough();
@@ -1444,12 +1444,12 @@ describe("A Chat Message", function () {
                 </message>`)
             _converse.connection._dataRecv(mock.createRequest(stanza));
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg audio').length, 1000);
-            let msg = view.el.querySelector('.chat-msg .chat-msg__text');
+            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg audio').length, 1000);
+            let msg = view.querySelector('.chat-msg .chat-msg__text');
             expect(msg.classList.length).toEqual(1);
             expect(u.hasClass('chat-msg__text', msg)).toBe(true);
             expect(msg.textContent).toEqual('Have you heard this funny audio?');
-            let media = view.el.querySelector('.chat-msg .chat-msg__media');
+            let media = view.querySelector('.chat-msg .chat-msg__media');
             expect(media.innerHTML.replace(/<!---->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
                 `<audio controls="" src="https://montague.lit/audio.mp3"></audio>    `+
                 `<a target="_blank" rel="noopener" href="https://montague.lit/audio.mp3">Download audio file "audio.mp3"</a>`);
@@ -1464,9 +1464,9 @@ describe("A Chat Message", function () {
                 </message>`);
             _converse.connection._dataRecv(mock.createRequest(stanza));
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text');
+            msg = view.querySelector('.chat-msg:last-child .chat-msg__text');
             expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('Have you heard this funny audio?'); // Emtpy
-            media = view.el.querySelector('.chat-msg:last-child .chat-msg__media');
+            media = view.querySelector('.chat-msg:last-child .chat-msg__media');
             expect(media.innerHTML.replace(/<!---->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
                 `<audio controls="" src="https://montague.lit/audio.mp3"></audio>    `+
                 `<a target="_blank" rel="noopener" href="https://montague.lit/audio.mp3">`+
@@ -1493,11 +1493,11 @@ describe("A Chat Message", function () {
                     <x xmlns="jabber:x:oob"><url>https://montague.lit/video.mp4</url></x>
                 </message>`);
             _converse.connection._dataRecv(mock.createRequest(stanza));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg video').length, 2000)
-            let msg = view.el.querySelector('.chat-msg .chat-msg__text');
+            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg video').length, 2000)
+            let msg = view.querySelector('.chat-msg .chat-msg__text');
             expect(msg.classList.length).toBe(1);
             expect(msg.textContent).toEqual('Have you seen this funny video?');
-            let media = view.el.querySelector('.chat-msg .chat-msg__media');
+            let media = view.querySelector('.chat-msg .chat-msg__media');
             expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
                 `<!----><video controls="" preload="metadata" style="max-height: 50vh" src="https://montague.lit/video.mp4"></video><!---->`);
 
@@ -1512,9 +1512,9 @@ describe("A Chat Message", function () {
                 </message>`);
             _converse.connection._dataRecv(mock.createRequest(stanza));
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text');
+            msg = view.querySelector('.chat-msg:last-child .chat-msg__text');
             expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('Have you seen this funny video?');
-            media = view.el.querySelector('.chat-msg:last-child .chat-msg__media');
+            media = view.querySelector('.chat-msg:last-child .chat-msg__media');
             expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
                 `<!----><video controls="" preload="metadata" style="max-height: 50vh" src="https://montague.lit/video.mp4"></video><!---->`);
             done();
@@ -1539,11 +1539,11 @@ describe("A Chat Message", function () {
                 </message>`);
             _converse.connection._dataRecv(mock.createRequest(stanza));
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg a').length, 1000);
-            const msg = view.el.querySelector('.chat-msg .chat-msg__text');
+            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg a').length, 1000);
+            const msg = view.querySelector('.chat-msg .chat-msg__text');
             expect(u.hasClass('chat-msg__text', msg)).toBe(true);
             expect(msg.textContent).toEqual('Have you downloaded this funny file?');
-            const media = view.el.querySelector('.chat-msg .chat-msg__media');
+            const media = view.querySelector('.chat-msg .chat-msg__media');
             expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
                 `<!----><a target="_blank" rel="noopener" href="https://montague.lit/funny.pdf"><!---->Download file "funny.pdf"<!----></a><!---->`);
             done();
@@ -1572,11 +1572,11 @@ describe("A Chat Message", function () {
             _converse.connection._dataRecv(mock.createRequest(stanza));
             _converse.connection._dataRecv(mock.createRequest(stanza));
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg a').length, 1000);
-            const msg = view.el.querySelector('.chat-msg .chat-msg__text');
+            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg a').length, 1000);
+            const msg = view.querySelector('.chat-msg .chat-msg__text');
             expect(u.hasClass('chat-msg__text', msg)).toBe(true);
             expect(msg.textContent).toEqual('Have you seen this funny image?');
-            const media = view.el.querySelector('.chat-msg .chat-msg__media');
+            const media = view.querySelector('.chat-msg .chat-msg__media');
             expect(media.innerHTML.replace(/<!---->/g, '').replace(/(\r\n|\n|\r)/gm, "")).toEqual(
                 `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
                 `Download image file "conversejs-filled.svg"</a>`);

+ 18 - 18
spec/minchats.js

@@ -21,12 +21,12 @@ describe("A chat message", function () {
         await mock.openControlBox(_converse);
         spyOn(_converse.api, "trigger").and.callThrough();
 
-        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+        await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
         await mock.openChatBoxFor(_converse, contact_jid);
         const chatview = _converse.api.chatviews.get(contact_jid);
-        expect(u.isVisible(chatview.el)).toBeTruthy();
+        expect(u.isVisible(chatview)).toBeTruthy();
         expect(chatview.model.get('minimized')).toBeFalsy();
-        chatview.el.querySelector('.toggle-chatbox-button').click();
+        chatview.querySelector('.toggle-chatbox-button').click();
         expect(chatview.model.get('minimized')).toBeTruthy();
         var message = 'This message is sent to a minimized chatbox';
         var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -44,7 +44,7 @@ describe("A chat message", function () {
         expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
         const trimmed_chatboxes = _converse.minimized_chats;
         let count = trimmed_chatboxes.el.querySelector('converse-minimized-chat .message-count');
-        expect(u.isVisible(chatview.el)).toBeFalsy();
+        expect(u.isVisible(chatview)).toBeFalsy();
         expect(chatview.model.get('minimized')).toBeTruthy();
 
         expect(u.isVisible(count)).toBeTruthy();
@@ -60,7 +60,7 @@ describe("A chat message", function () {
         );
 
         await u.waitUntil(() => (chatview.model.messages.length > 1));
-        expect(u.isVisible(chatview.el)).toBeFalsy();
+        expect(u.isVisible(chatview)).toBeFalsy();
         expect(chatview.model.get('minimized')).toBeTruthy();
         count = trimmed_chatboxes.el.querySelector('converse-minimized-chat .message-count');
         expect(u.isVisible(count)).toBeTruthy();
@@ -85,12 +85,12 @@ describe("A Groupcaht", function () {
         spyOn(view, 'onMaximized').and.callThrough();
         spyOn(_converse.api, "trigger").and.callThrough();
         view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
-        const button = await u.waitUntil(() => view.el.querySelector('.toggle-chatbox-button'));
+        const button = await u.waitUntil(() => view.querySelector('.toggle-chatbox-button'));
         button.click();
 
         expect(view.onMinimized).toHaveBeenCalled();
         expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
-        expect(u.isVisible(view.el)).toBeFalsy();
+        expect(u.isVisible(view)).toBeFalsy();
         expect(view.model.get('minimized')).toBeTruthy();
         expect(view.onMinimized).toHaveBeenCalled();
         const el = await u.waitUntil(() => _converse.minimized_chats.el.querySelector("a.restore-chat"));
@@ -115,21 +115,21 @@ describe("A Chatbox", function () {
         await mock.openControlBox(_converse);
 
         const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+        await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length);
         await mock.openChatBoxFor(_converse, contact_jid);
         const chatview = _converse.chatboxviews.get(contact_jid);
         spyOn(chatview, 'minimize').and.callThrough();
         spyOn(_converse.api, "trigger").and.callThrough();
         // We need to rebind all events otherwise our spy won't be called
         chatview.delegateEvents();
-        chatview.el.querySelector('.toggle-chatbox-button').click();
+        chatview.querySelector('.toggle-chatbox-button').click();
 
         expect(chatview.minimize).toHaveBeenCalled();
         expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
         expect(_converse.api.trigger.calls.count(), 2);
-        expect(u.isVisible(chatview.el)).toBeFalsy();
+        expect(u.isVisible(chatview)).toBeFalsy();
         expect(chatview.model.get('minimized')).toBeTruthy();
-        chatview.el.querySelector('.toggle-chatbox-button').click();
+        chatview.querySelector('.toggle-chatbox-button').click();
 
         await u.waitUntil(() => _converse.chatboxviews.keys().length);
         _converse.minimized_chats.el.querySelector("a.restore-chat").click();
@@ -150,7 +150,7 @@ describe("A Chatbox", function () {
         await _converse.api.chats.create(sender_jid, {'minimized': true});
         await u.waitUntil(() => _converse.chatboxes.length > 1);
         const chatBoxView = _converse.chatboxviews.get(sender_jid);
-        expect(u.isVisible(chatBoxView.el)).toBeFalsy();
+        expect(u.isVisible(chatBoxview)).toBeFalsy();
         expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(true);
         expect(_converse.minimized_chats.el.firstElementChild.querySelectorAll('converse-minimized-chat').length).toBe(1);
         expect(_converse.chatboxes.filter('minimized').length).toBe(1);
@@ -175,9 +175,9 @@ describe("A Chatbox", function () {
         expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
 
         _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached.
-        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length);
+        await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group li').length);
         // Test that they can be maximized again
-        const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
+        const online_contacts = _converse.rosterview.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
         expect(online_contacts.length).toBe(17);
         let i;
         for (i=0; i<online_contacts.length; i++) {
@@ -265,9 +265,9 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
         const view = _converse.chatboxviews.get(contact_jid);
         spyOn(view.model, 'sendMessage').and.callThrough();
         mock.sendMessage(view, message);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000);
+        await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length, 1000);
         expect(view.model.sendMessage).toHaveBeenCalled();
-        const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
+        const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop();
         await u.waitUntil(() => msg.innerHTML.replace(/\<!----\>/g, '') ===
             '<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&amp;'+
             'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&amp;mlon=-122.399677#map=18/37.786971/-122.399677</a>');
@@ -292,7 +292,7 @@ describe("The Minimized Chats Widget", function () {
         let chatview = _converse.chatboxviews.get(contact_jid);
         expect(chatview.model.get('minimized')).toBeFalsy();
         expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(false);
-        chatview.el.querySelector('.toggle-chatbox-button').click();
+        chatview.querySelector('.toggle-chatbox-button').click();
         expect(chatview.model.get('minimized')).toBeTruthy();
         expect(u.isVisible(_converse.minimized_chats.el)).toBe(true);
         expect(_converse.chatboxes.filter('minimized').length).toBe(1);
@@ -302,7 +302,7 @@ describe("The Minimized Chats Widget", function () {
         await mock.openChatBoxFor(_converse, contact_jid);
         chatview = _converse.chatboxviews.get(contact_jid);
         expect(chatview.model.get('minimized')).toBeFalsy();
-        chatview.el.querySelector('.toggle-chatbox-button').click();
+        chatview.querySelector('.toggle-chatbox-button').click();
         expect(chatview.model.get('minimized')).toBeTruthy();
         expect(u.isVisible(_converse.minimized_chats.el)).toBe(true);
         expect(_converse.chatboxes.filter('minimized').length).toBe(2);

+ 3 - 3
spec/mock.js

@@ -442,9 +442,9 @@ window.addEventListener('converse-loaded', () => {
 
     mock.sendMessage = function (view, message) {
         const promise = new Promise(resolve => view.model.messages.once('rendered', resolve));
-        view.el.querySelector('.chat-textarea').value = message;
+        view.querySelector('.chat-textarea').value = message;
         view.onKeyDown({
-            target: view.el.querySelector('textarea.chat-textarea'),
+            target: view.querySelector('textarea.chat-textarea'),
             preventDefault: () => {},
             keyCode: 13
         });
@@ -641,7 +641,7 @@ window.addEventListener('converse-loaded', () => {
             'view_mode': mock.view_mode
         }, settings || {}));
 
-        _converse.ChatBoxViews.prototype.trimChat = function () {};
+        _converse.minimize.trimChat = function () {};
 
         _converse.api.vcard.get = function (model, force) {
             let jid;

+ 3 - 3
spec/modtools.js

@@ -8,7 +8,7 @@ const u = converse.env.utils;
 
 
 async function openModtools (_converse, view) {
-    const textarea = view.el.querySelector('.chat-textarea');
+    const textarea = view.querySelector('.chat-textarea');
     textarea.value = '/modtools';
     const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
     view.onKeyDown(enter);
@@ -263,7 +263,7 @@ describe("The groupchat moderator tool", function () {
         ));
         await u.waitUntil(() => (view.model.occupants.length === 7), 1000);
 
-        const textarea = view.el.querySelector('.chat-textarea');
+        const textarea = view.querySelector('.chat-textarea');
         textarea.value = '/modtools';
         const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
         view.onKeyDown(enter);
@@ -474,7 +474,7 @@ describe("The groupchat moderator tool", function () {
         const members = [{'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'}];
         await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members);
         const view = _converse.chatboxviews.get(muc_jid);
-        const textarea = view.el.querySelector('.chat-textarea');
+        const textarea = view.querySelector('.chat-textarea');
         textarea.value = '/modtools';
         const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
         view.onKeyDown(enter);

+ 1 - 1
spec/muc-mentions.js

@@ -32,7 +32,7 @@ describe("MUC Mention Notfications", function () {
         await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED);
 
         const lview = _converse.rooms_list_view
-        const room_el = await u.waitUntil(() => lview.el.querySelector(".available-chatroom"));
+        const room_el = await u.waitUntil(() => lview.querySelector(".available-chatroom"));
         expect(Array.from(room_el.classList).includes('unread-msgs')).toBeFalsy();
 
         const base_time = new Date();

文件差异内容过多而无法显示
+ 145 - 145
spec/muc.js


+ 27 - 27
spec/muc_messages.js

@@ -19,7 +19,7 @@ describe("A Groupchat Message", function () {
             const muc_jid = 'lounge@montague.lit';
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
             const view = _converse.api.chatviews.get(muc_jid);
-            const textarea = view.el.querySelector('textarea.chat-textarea');
+            const textarea = view.querySelector('textarea.chat-textarea');
             textarea.value = 'hello world'
             const enter_event = {
                 'target': textarea,
@@ -42,7 +42,7 @@ describe("A Groupchat Message", function () {
                 </message>
             `);
             _converse.connection._dataRecv(mock.createRequest(error));
-            expect(await u.waitUntil(() => view.el.querySelector('.chat-msg__error')?.textContent?.trim())).toBe(err_msg_text);
+            expect(await u.waitUntil(() => view.querySelector('.chat-msg__error')?.textContent?.trim())).toBe(err_msg_text);
             expect(view.model.messages.length).toBe(1);
             const message = view.model.messages.at(0);
             expect(message.get('received')).toBeUndefined();
@@ -74,7 +74,7 @@ describe("A Groupchat Message", function () {
                 </presence>
             `);
             _converse.connection._dataRecv(mock.createRequest(presence));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-info').length === 1);
 
             presence = u.toStanza(`
                 <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo1">
@@ -86,9 +86,9 @@ describe("A Groupchat Message", function () {
                 </presence>
             `);
             _converse.connection._dataRecv(mock.createRequest(presence));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
+            await u.waitUntil(() => view.querySelectorAll('.chat-info').length === 2);
 
-            const messages = view.el.querySelectorAll('.chat-info');
+            const messages = view.querySelectorAll('.chat-info');
             expect(u.hasClass('chat-msg--followup', messages[0])).toBe(false);
             expect(u.hasClass('chat-msg--followup', messages[1])).toBe(false);
             done();
@@ -119,11 +119,11 @@ describe("A Groupchat Message", function () {
             spyOn(view.model, 'createInfoMessages').and.callThrough();
             _converse.connection._dataRecv(mock.createRequest(presence));
             await u.waitUntil(() => view.model.createInfoMessages.calls.count());
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-info').length === 1);
 
             _converse.connection._dataRecv(mock.createRequest(presence));
             await u.waitUntil(() => view.model.createInfoMessages.calls.count() === 2);
-            expect(view.el.querySelectorAll('.chat-info').length).toBe(1);
+            expect(view.querySelectorAll('.chat-info').length).toBe(1);
             done();
         }));
     });
@@ -159,7 +159,7 @@ describe("A Groupchat Message", function () {
         expect(converse.env.log.error).toHaveBeenCalledWith(
             `Ignoring unencapsulated forwarded message from ${muc_jid}/mallory`
         );
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(0);
         expect(view.model.messages.length).toBe(0);
         done();
     }));
@@ -172,7 +172,7 @@ describe("A Groupchat Message", function () {
         const muc_jid = 'lounge@montague.lit';
         await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
         const view = _converse.api.chatviews.get(muc_jid);
-        if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
+        if (!view.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
         const message = 'romeo: Your attention is required';
         const nick = mock.chatroom_names[0],
             msg = $msg({
@@ -185,7 +185,7 @@ describe("A Groupchat Message", function () {
               .tree();
         await view.model.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelector('.chat-msg')).not.toBe(null);
+        expect(view.querySelector('.chat-msg')).not.toBe(null);
         done();
     }));
 
@@ -197,7 +197,7 @@ describe("A Groupchat Message", function () {
         const muc_jid = 'lounge@montague.lit';
         await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
         const view = _converse.api.chatviews.get(muc_jid);
-        if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
+        if (!view.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
         const id = u.getUniqueId();
         let msg = $msg({
                 from: 'lounge@montague.lit/some1',
@@ -206,7 +206,7 @@ describe("A Groupchat Message", function () {
                 type: 'groupchat'
             }).c('body').t('First message').tree();
         await view.model.handleMessageStanza(msg);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
 
         msg = $msg({
                 from: 'lounge@montague.lit/some2',
@@ -215,7 +215,7 @@ describe("A Groupchat Message", function () {
                 type: 'groupchat'
             }).c('body').t('Another message').tree();
         await view.model.handleMessageStanza(msg);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2);
         expect(view.model.messages.length).toBe(2);
         done();
     }));
@@ -371,7 +371,7 @@ describe("A Groupchat Message", function () {
         expect(converse.env.log.error).toHaveBeenCalledWith(
             'Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied'
         );
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(0);
         expect(view.model.messages.length).toBe(0);
         done();
     }));
@@ -391,10 +391,10 @@ describe("A Groupchat Message", function () {
             type: 'groupchat'
         }).c('body').t('I wrote this message!').tree();
         await view.model.handleMessageStanza(msg);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
         expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner');
         expect(view.model.messages.last().occupant.get('role')).toBe('moderator');
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
         expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat chat-msg--with-avatar moderator owner');
         let presence = $pres({
                 to:'romeo@montague.lit/orchard',
@@ -420,7 +420,7 @@ describe("A Groupchat Message", function () {
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
         expect(view.model.messages.last().occupant.get('role')).toBe('participant');
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(2);
         expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat chat-msg--with-avatar participant member');
 
         presence = $pres({
@@ -438,12 +438,12 @@ describe("A Groupchat Message", function () {
         _converse.connection._dataRecv(mock.createRequest(presence));
 
         view.model.sendMessage('hello world');
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 3);
 
         const occupant = await u.waitUntil(() => view.model.messages.filter(m => m.get('type') === 'groupchat')[2].occupant);
         expect(occupant.get('affiliation')).toBe('owner');
         expect(occupant.get('role')).toBe('moderator');
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(3);
         await u.waitUntil(() => sizzle('.chat-msg', view.el).pop().classList.value.trim() === 'message chat-msg groupchat chat-msg--with-avatar moderator owner');
 
         const add_events = view.model.occupants._events.add.length;
@@ -533,7 +533,7 @@ describe("A Groupchat Message", function () {
         const muc_jid = 'lounge@montague.lit';
         await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
         const view = _converse.api.chatviews.get(muc_jid);
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         textarea.value = 'But soft, what light through yonder airlock breaks?';
         view.onKeyDown({
             target: textarea,
@@ -541,7 +541,7 @@ describe("A Groupchat Message", function () {
             keyCode: 13 // Enter
         });
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0);
+        expect(view.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0);
 
         const msg_obj = view.model.messages.at(0);
         const stanza = u.toStanza(`
@@ -556,9 +556,9 @@ describe("A Groupchat Message", function () {
                 <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
             </message>`);
         await view.model.handleMessageStanza(stanza);
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
-        expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
-        expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
+        expect(view.querySelectorAll('.chat-msg__receipt').length).toBe(0);
+        expect(view.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1);
         expect(view.model.messages.length).toBe(1);
 
         const message = view.model.messages.at(0);
@@ -611,7 +611,7 @@ describe("A Groupchat Message", function () {
         const muc_jid = 'lounge@montague.lit';
         await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
         const view = _converse.api.chatviews.get(muc_jid);
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         textarea.value = 'But soft, what light through yonder airlock breaks?';
         view.onKeyDown({
             target: textarea,
@@ -619,7 +619,7 @@ describe("A Groupchat Message", function () {
             keyCode: 13 // Enter
         });
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
 
         const msg_obj = view.model.messages.at(0);
         let stanza = u.toStanza(`
@@ -643,7 +643,7 @@ describe("A Groupchat Message", function () {
                 <origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
             </message>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
-        expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(1);
         done();
     }));
 });

+ 27 - 24
spec/muclist.js

@@ -17,19 +17,19 @@ describe("A list of open groupchats", function () {
         await mock.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC');
 
         const lview = _converse.rooms_list_view
-        await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length);
-        let room_els = lview.el.querySelectorAll(".open-room");
+        await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
+        let room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
         expect(room_els[0].innerText).toBe('room@conference.shakespeare.lit');
 
         await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
-        await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length > 1);
-        room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
+        await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1);
+        room_els = _converse.rooms_list_view.querySelectorAll(".open-room");
         expect(room_els.length).toBe(2);
 
         let view = _converse.chatboxviews.get('room@conference.shakespeare.lit');
         await view.close();
-        room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
+        room_els = _converse.rooms_list_view.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
         expect(room_els[0].innerText).toBe('lounge@montague.lit');
         list = controlbox.el.querySelector('.list-container--openrooms');
@@ -37,7 +37,7 @@ describe("A list of open groupchats", function () {
 
         view = _converse.chatboxviews.get('lounge@montague.lit');
         await view.close();
-        room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
+        room_els = _converse.rooms_list_view.querySelectorAll(".open-room");
         expect(room_els.length).toBe(0);
 
         list = controlbox.el.querySelector('.list-container--openrooms');
@@ -119,8 +119,8 @@ describe("A groupchat shown in the groupchats list", function () {
         const muc_jid = 'coven@chat.shakespeare.lit';
         await _converse.api.rooms.open(muc_jid, {'nick': 'some1'}, true);
         const lview = _converse.rooms_list_view
-        await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length);
-        let room_els = lview.el.querySelectorAll(".available-chatroom");
+        await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
+        let room_els = lview.querySelectorAll(".available-chatroom");
         expect(room_els.length).toBe(1);
 
         let item = room_els[0];
@@ -128,11 +128,11 @@ describe("A groupchat shown in the groupchats list", function () {
         await u.waitUntil(() => u.hasClass('open', item), 1000);
         expect(item.textContent.trim()).toBe('coven@chat.shakespeare.lit');
         await _converse.api.rooms.open('balcony@chat.shakespeare.lit', {'nick': 'some1'}, true);
-        await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length > 1);
-        room_els = lview.el.querySelectorAll(".open-room");
+        await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1);
+        room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(2);
 
-        room_els = lview.el.querySelectorAll(".available-chatroom.open");
+        room_els = lview.querySelectorAll(".available-chatroom.open");
         expect(room_els.length).toBe(1);
         item = room_els[0];
         expect(item.textContent.trim()).toBe('balcony@chat.shakespeare.lit');
@@ -198,10 +198,11 @@ describe("A groupchat shown in the groupchats list", function () {
             .c('status').attrs({code:'110'});
         _converse.connection._dataRecv(mock.createRequest(presence));
 
-        await u.waitUntil(() => _converse.rooms_list_view.el.querySelectorAll(".open-room").length, 500);
-        const room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
+        const rooms_list = document.querySelector('converse-rooms-list');
+        await u.waitUntil(() => rooms_list.querySelectorAll(".open-room").length, 500);
+        const room_els = rooms_list.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
-        const info_el = _converse.rooms_list_view.el.querySelector(".room-info");
+        const info_el = rooms_list.querySelector(".room-info");
         info_el.click();
 
         const modal = _converse.api.modal.get('muc-details-modal');
@@ -259,16 +260,17 @@ describe("A groupchat shown in the groupchats list", function () {
         await mock.openChatRoom(_converse, 'lounge', 'conference.shakespeare.lit', 'JC');
         expect(_converse.chatboxes.length).toBe(2);
         const lview = _converse.rooms_list_view
-        await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length);
-        let room_els = lview.el.querySelectorAll(".open-room");
+        await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
+        let room_els = lview.querySelectorAll(".open-room");
         expect(room_els.length).toBe(1);
-        const close_el = _converse.rooms_list_view.el.querySelector(".close-room");
+        const rooms_list = document.querySelector('converse-rooms-list');
+        const close_el = rooms_list.querySelector(".close-room");
         close_el.click();
         expect(window.confirm).toHaveBeenCalledWith(
             'Are you sure you want to leave the groupchat lounge@conference.shakespeare.lit?');
 
         await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve));
-        room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
+        room_els = rooms_list.querySelectorAll(".open-room");
         expect(room_els.length).toBe(0);
         expect(_converse.chatboxes.length).toBe(1);
         done();
@@ -284,7 +286,8 @@ describe("A groupchat shown in the groupchats list", function () {
         const u = converse.env.utils;
         await mock.openControlBox(_converse);
         const room_jid = 'kitchen@conference.shakespeare.lit';
-        await u.waitUntil(() => _converse.rooms_list_view !== undefined, 500);
+        const rooms_list = document.querySelector('converse-rooms-list');
+        await u.waitUntil(() => rooms_list !== undefined, 500);
         await mock.openAndEnterChatRoom(_converse, room_jid, 'romeo');
         const view = _converse.chatboxviews.get(room_jid);
         view.model.set({'minimized': true});
@@ -299,7 +302,7 @@ describe("A groupchat shown in the groupchats list", function () {
 
         // If the user isn't mentioned, the counter doesn't get incremented, but the text of the groupchat is bold
         const lview = _converse.rooms_list_view
-        let room_el = await u.waitUntil(() => lview.el.querySelector(".available-chatroom"));
+        let room_el = await u.waitUntil(() => lview.querySelector(".available-chatroom"));
         expect(Array.from(room_el.classList).includes('unread-msgs')).toBeTruthy();
 
         // If the user is mentioned, the counter also gets updated
@@ -312,7 +315,7 @@ describe("A groupchat shown in the groupchats list", function () {
             }).c('body').t('romeo: Your attention is required').tree()
         );
 
-        let indicator_el = await u.waitUntil(() => lview.el.querySelector(".msgs-indicator"));
+        let indicator_el = await u.waitUntil(() => lview.querySelector(".msgs-indicator"));
         expect(indicator_el.textContent).toBe('1');
 
         spyOn(view.model, 'handleUnreadMessage').and.callThrough();
@@ -325,13 +328,13 @@ describe("A groupchat shown in the groupchats list", function () {
             }).c('body').t('romeo: and another thing...').tree()
         );
         await u.waitUntil(() => view.model.handleUnreadMessage.calls.count());
-        await u.waitUntil(() => lview.el.querySelector(".msgs-indicator").textContent === '2', 1000);
+        await u.waitUntil(() => lview.querySelector(".msgs-indicator").textContent === '2', 1000);
 
         // When the chat gets maximized again, the unread indicators are removed
         view.model.set({'minimized': false});
-        indicator_el = lview.el.querySelector(".msgs-indicator");
+        indicator_el = lview.querySelector(".msgs-indicator");
         expect(indicator_el === null);
-        room_el = lview.el.querySelector(".available-chatroom");
+        room_el = lview.querySelector(".available-chatroom");
         expect(Array.from(room_el.classList).includes('unread-msgs')).toBeFalsy();
         done();
     }));

+ 4 - 4
spec/notification.js

@@ -39,7 +39,7 @@ describe("Notifications", function () {
                     await mock.waitForRoster(_converse, 'current');
                     await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
                     const view = _converse.api.chatviews.get('lounge@montague.lit');
-                    if (!view.el.querySelectorAll('.chat-area').length) {
+                    if (!view.querySelectorAll('.chat-area').length) {
                         view.renderChatArea();
                     }
 
@@ -163,7 +163,7 @@ describe("Notifications", function () {
                 spyOn(window, 'Audio').and.returnValue(stub);
 
                 const view = _converse.chatboxviews.get('lounge@montague.lit');
-                if (!view.el.querySelectorAll('.chat-area').length) {
+                if (!view.querySelectorAll('.chat-area').length) {
                     view.renderChatArea();
                 }
                 let text = 'This message will play a sound because it mentions romeo';
@@ -327,7 +327,7 @@ describe("Notifications", function () {
 
             // come back to converse-chat page
             _converse.saveWindowState({'type': 'focus'});
-            await u.waitUntil(() => u.isVisible(view.el));
+            await u.waitUntil(() => u.isVisible(view));
             await u.waitUntil(() => favico.badge.calls.count() === 2);
             expect(favico.badge.calls.mostRecent().args.pop()).toBe(0);
 
@@ -338,7 +338,7 @@ describe("Notifications", function () {
             // check that msg_counter is incremented from zero again
             await _converse.handleMessageStanza(msgFactory());
             view = _converse.chatboxviews.get(sender_jid);
-            await u.waitUntil(() => u.isVisible(view.el));
+            await u.waitUntil(() => u.isVisible(view));
             await u.waitUntil(() => favico.badge.calls.count() === 3);
             expect(favico.badge.calls.mostRecent().args.pop()).toBe(1);
             done();

+ 24 - 24
spec/omemo.js

@@ -114,7 +114,7 @@ describe("The OMEMO module", function() {
         const view = _converse.chatboxviews.get(contact_jid);
         view.model.set('omemo_active', true);
 
-        const textarea = view.el.querySelector('.chat-textarea');
+        const textarea = view.querySelector('.chat-textarea');
         textarea.value = 'This message will be encrypted';
         view.onKeyDown({
             target: textarea,
@@ -198,7 +198,7 @@ describe("The OMEMO module", function() {
         _converse.connection._dataRecv(mock.createRequest(stanza));
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         expect(view.model.messages.length).toBe(2);
-        expect(view.el.querySelectorAll('.chat-msg__body')[1].textContent.trim())
+        expect(view.querySelectorAll('.chat-msg__body')[1].textContent.trim())
             .toBe('This is an encrypted message from the contact');
 
         // #1193 Check for a received message without <body> tag
@@ -218,7 +218,7 @@ describe("The OMEMO module", function() {
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         await u.waitUntil(() => view.model.messages.length > 1);
         expect(view.model.messages.length).toBe(3);
-        expect(view.el.querySelectorAll('.chat-msg__body')[2].textContent.trim())
+        expect(view.querySelectorAll('.chat-msg__body')[2].textContent.trim())
             .toBe('Another received encrypted message without fallback');
         done();
     }));
@@ -244,7 +244,7 @@ describe("The OMEMO module", function() {
         const view = _converse.chatboxviews.get('lounge@montague.lit');
         await u.waitUntil(() => initializedOMEMO(_converse));
 
-        const toolbar = view.el.querySelector('.chat-toolbar');
+        const toolbar = view.querySelector('.chat-toolbar');
         const el = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
         el.click();
         expect(view.model.get('omemo_active')).toBe(true);
@@ -297,7 +297,7 @@ describe("The OMEMO module", function() {
         expect(u.hasClass('fa-unlock', icon)).toBe(false);
         expect(u.hasClass('fa-lock', icon)).toBe(true);
 
-        const textarea = view.el.querySelector('.chat-textarea');
+        const textarea = view.querySelector('.chat-textarea');
         textarea.value = 'This message will be encrypted';
         view.onKeyDown({
             target: textarea,
@@ -451,7 +451,7 @@ describe("The OMEMO module", function() {
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         expect(view.model.messages.length).toBe(1);
 
-        expect(view.el.querySelector('.chat-msg__text').textContent.trim())
+        expect(view.querySelector('.chat-msg__text').textContent.trim())
             .toBe('This is an encrypted carbon message from another device of mine');
 
         expect(contact_devicelist.devices.length).toBe(1);
@@ -463,7 +463,7 @@ describe("The OMEMO module", function() {
         expect(my_devicelist.devices.at(2).get('id')).toBe('988349631');
         expect(my_devicelist.devices.get('988349631').get('active')).toBe(true);
 
-        const textarea = view.el.querySelector('.chat-textarea');
+        const textarea = view.querySelector('.chat-textarea');
         textarea.value = 'This is an encrypted message from this device';
         view.onKeyDown({
             target: textarea,
@@ -514,13 +514,13 @@ describe("The OMEMO module", function() {
             }).tree();
         _converse.connection._dataRecv(mock.createRequest(stanza));
 
-        const toolbar = view.el.querySelector('.chat-toolbar');
+        const toolbar = view.querySelector('.chat-toolbar');
         const toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
         toggle.click();
         expect(view.model.get('omemo_active')).toBe(true);
         expect(view.model.get('omemo_supported')).toBe(true);
 
-        const textarea = view.el.querySelector('.chat-textarea');
+        const textarea = view.querySelector('.chat-textarea');
         textarea.value = 'This message will be encrypted';
         view.onKeyDown({
             target: textarea,
@@ -605,7 +605,7 @@ describe("The OMEMO module", function() {
                   "to be subscribed to their presence in order to see their OMEMO information");
 
         expect(view.model.get('omemo_supported')).toBe(false);
-        expect(view.el.querySelector('.chat-textarea').value).toBe('This message will be encrypted');
+        expect(view.querySelector('.chat-textarea').value).toBe('This message will be encrypted');
         done();
     }));
 
@@ -1232,7 +1232,7 @@ describe("The OMEMO module", function() {
         expect(devicelist.devices.at(3).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
         await u.waitUntil(() => _converse.chatboxviews.get(contact_jid).el.querySelector('.chat-toolbar'));
         const view = _converse.chatboxviews.get(contact_jid);
-        const toolbar = view.el.querySelector('.chat-toolbar');
+        const toolbar = view.querySelector('.chat-toolbar');
         expect(view.model.get('omemo_active')).toBe(undefined);
         const toggle = toolbar.querySelector('.toggle-omemo');
         expect(toggle === null).toBe(false);
@@ -1248,7 +1248,7 @@ describe("The OMEMO module", function() {
         expect(u.hasClass('fa-unlock', icon)).toBe(false);
         expect(u.hasClass('fa-lock', icon)).toBe(true);
 
-        const textarea = view.el.querySelector('.chat-textarea');
+        const textarea = view.querySelector('.chat-textarea');
         textarea.value = 'This message will be sent encrypted';
         view.onKeyDown({
             target: textarea,
@@ -1296,7 +1296,7 @@ describe("The OMEMO module", function() {
         const view = _converse.chatboxviews.get('lounge@montague.lit');
         await u.waitUntil(() => initializedOMEMO(_converse));
 
-        const toolbar = view.el.querySelector('.chat-toolbar');
+        const toolbar = view.querySelector('.chat-toolbar');
         let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
         expect(view.model.get('omemo_active')).toBe(undefined);
         expect(view.model.get('omemo_supported')).toBe(true);
@@ -1370,31 +1370,31 @@ describe("The OMEMO module", function() {
         // anonymous or semi-anonymous
         view.model.features.save({'nonanonymous': false, 'semianonymous': true});
         await u.waitUntil(() => !view.model.get('omemo_supported'));
-        await u.waitUntil(() => view.el.querySelector('.toggle-omemo').disabled);
+        await u.waitUntil(() => view.querySelector('.toggle-omemo').disabled);
 
         view.model.features.save({'nonanonymous': true, 'semianonymous': false});
         await u.waitUntil(() => view.model.get('omemo_supported'));
-        await u.waitUntil(() => view.el.querySelector('.toggle-omemo') !== null);
+        await u.waitUntil(() => view.querySelector('.toggle-omemo') !== null);
         expect(u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
         expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(false);
-        expect(!!view.el.querySelector('.toggle-omemo').disabled).toBe(false);
+        expect(!!view.querySelector('.toggle-omemo').disabled).toBe(false);
 
         // Test that the button gets disabled when the room becomes open
         view.model.features.save({'membersonly': false, 'open': true});
         await u.waitUntil(() => !view.model.get('omemo_supported'));
-        await u.waitUntil(() => view.el.querySelector('.toggle-omemo').disabled);
+        await u.waitUntil(() => view.querySelector('.toggle-omemo').disabled);
 
         view.model.features.save({'membersonly': true, 'open': false});
         await u.waitUntil(() => view.model.get('omemo_supported'));
-        await u.waitUntil(() => !view.el.querySelector('.toggle-omemo').disabled);
+        await u.waitUntil(() => !view.querySelector('.toggle-omemo').disabled);
 
-        expect(u.hasClass('fa-unlock', view.el.querySelector('.toggle-omemo converse-icon'))).toBe(true);
-        expect(u.hasClass('fa-lock', view.el.querySelector('.toggle-omemo converse-icon'))).toBe(false);
+        expect(u.hasClass('fa-unlock', view.querySelector('.toggle-omemo converse-icon'))).toBe(true);
+        expect(u.hasClass('fa-lock', view.querySelector('.toggle-omemo converse-icon'))).toBe(false);
 
         expect(view.model.get('omemo_supported')).toBe(true);
         expect(view.model.get('omemo_active')).toBe(false);
 
-        view.el.querySelector('.toggle-omemo').click();
+        view.querySelector('.toggle-omemo').click();
         expect(view.model.get('omemo_active')).toBe(true);
 
         // Someone enters the room who doesn't have OMEMO support, while we
@@ -1429,13 +1429,13 @@ describe("The OMEMO module", function() {
         _converse.connection._dataRecv(mock.createRequest(stanza));
 
         await u.waitUntil(() => !view.model.get('omemo_supported'));
-        await u.waitUntil(() => view.el.querySelector('.chat-error .chat-info__message')?.textContent.trim() ===
+        await u.waitUntil(() => view.querySelector('.chat-error .chat-info__message')?.textContent.trim() ===
             "oldguy doesn't appear to have a client that supports OMEMO. "+
             "Encrypted chat will no longer be possible in this grouchat."
         );
 
         await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').disabled);
-        icon =  view.el.querySelector('.toggle-omemo converse-icon');
+        icon =  view.querySelector('.toggle-omemo converse-icon');
         expect(u.hasClass('fa-unlock', icon)).toBe(true);
         expect(u.hasClass('fa-lock', icon)).toBe(false);
         expect(toolbar.querySelector('.toggle-omemo').title).toBe('This groupchat needs to be members-only and non-anonymous in order to support OMEMO encrypted messages');
@@ -1461,7 +1461,7 @@ describe("The OMEMO module", function() {
         _converse.api.trigger('OMEMOInitialized');
 
         const view = _converse.chatboxviews.get(contact_jid);
-        const show_modal_button = view.el.querySelector('.show-user-details-modal');
+        const show_modal_button = view.querySelector('.show-user-details-modal');
         show_modal_button.click();
         const modal = _converse.api.modal.get('user-details-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 1000);

+ 2 - 2
spec/presence.js

@@ -75,7 +75,7 @@ describe("A sent presence stanza", function () {
         spyOn(_converse.connection, 'send').and.callThrough();
 
         const cbview = _converse.chatboxviews.get('controlbox');
-        const change_status_el = await u.waitUntil(() => cbview.el.querySelector('.change-status'));
+        const change_status_el = await u.waitUntil(() => cbview.querySelector('.change-status'));
         change_status_el.click()
         let modal = _converse.api.modal.get('modal-status-change');
         await u.waitUntil(() => u.isVisible(modal.el), 1000);
@@ -94,7 +94,7 @@ describe("A sent presence stanza", function () {
         await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true");
         await u.waitUntil(() => !u.isVisible(modal.el));
 
-        cbview.el.querySelector('.change-status').click()
+        cbview.querySelector('.change-status').click()
         modal = _converse.api.modal.get('modal-status-change');
         await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "false", 1000);
         modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"

+ 1 - 1
spec/protocol.js

@@ -65,7 +65,7 @@ describe("The Protocol", function () {
                 IQ_id = sendIQ.bind(this)(iq, callback, errback);
             });
 
-            cbview.el.querySelector('.add-contact').click()
+            cbview.querySelector('.add-contact').click()
             const modal = _converse.api.modal.get('add-contact-modal');
             await u.waitUntil(() => u.isVisible(modal.el), 1000);
             spyOn(modal, "addContactFromForm").and.callThrough();

+ 2 - 2
spec/rai.js

@@ -97,7 +97,7 @@ describe("XEP-0437 Room Activity Indicators", function () {
         expect(view.model.get('has_activity')).toBe(false);
 
         const lview = _converse.rooms_list_view
-        const room_el = await u.waitUntil(() => lview.el.querySelector(".available-chatroom"));
+        const room_el = await u.waitUntil(() => lview.querySelector(".available-chatroom"));
         expect(Array.from(room_el.classList).includes('unread-msgs')).toBeFalsy();
 
         const activity_stanza = u.toStanza(`
@@ -161,7 +161,7 @@ describe("XEP-0437 Room Activity Indicators", function () {
         expect(view.model.get('has_activity')).toBe(false);
 
         const lview = _converse.rooms_list_view
-        const room_el = await u.waitUntil(() => lview.el.querySelector(".available-chatroom"));
+        const room_el = await u.waitUntil(() => lview.querySelector(".available-chatroom"));
         expect(Array.from(room_el.classList).includes('unread-msgs')).toBeFalsy();
 
         const activity_stanza = u.toStanza(`

+ 3 - 3
spec/receipts.js

@@ -108,7 +108,7 @@ describe("A delivery receipt", function () {
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         await mock.openChatBoxFor(_converse, contact_jid);
         const view = _converse.chatboxviews.get(contact_jid);
-        const textarea = view.el.querySelector('textarea.chat-textarea');
+        const textarea = view.querySelector('textarea.chat-textarea');
         textarea.value = 'But soft, what light through yonder airlock breaks?';
         view.onKeyDown({
             target: textarea,
@@ -126,7 +126,7 @@ describe("A delivery receipt", function () {
                 'id': u.getUniqueId(),
             }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
         _converse.connection._dataRecv(mock.createRequest(msg));
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__receipt').length === 1);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 1);
 
         // Also handle receipts with type 'chat'. See #1353
         spyOn(_converse, 'handleMessageStanza').and.callThrough();
@@ -147,7 +147,7 @@ describe("A delivery receipt", function () {
                 'id': u.getUniqueId(),
             }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
         _converse.connection._dataRecv(mock.createRequest(msg));
-        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__receipt').length === 2);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 2);
         expect(_converse.handleMessageStanza.calls.count()).toBe(1);
         done();
     }));

+ 38 - 38
spec/register.js

@@ -16,7 +16,7 @@ describe("The Registration Panel", function () {
 
         await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
         const cbview = _converse.api.controlbox.get();
-        expect(cbview.el.querySelectorAll('a.register-account').length).toBe(0);
+        expect(cbview.querySelectorAll('a.register-account').length).toBe(0);
         done();
     }));
 
@@ -36,10 +36,10 @@ describe("The Registration Panel", function () {
         }
         await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'), 300);
         const cbview = _converse.chatboxviews.get('controlbox');
-        const panels = cbview.el.querySelector('.controlbox-panes');
+        const panels = cbview.querySelector('.controlbox-panes');
         const login = panels.firstElementChild;
         const registration = panels.childNodes[1];
-        const register_link = cbview.el.querySelector('a.register-account');
+        const register_link = cbview.querySelector('a.register-account');
         expect(register_link.textContent).toBe("Create an account");
         register_link.click();
 
@@ -62,17 +62,17 @@ describe("The Registration Panel", function () {
         toggle.click();
 
         const cbview = _converse.api.controlbox.get();
-        await u.waitUntil(() => u.isVisible(cbview.el));
+        await u.waitUntil(() => u.isVisible(cbview));
         const registerview = cbview.registerpanel;
         spyOn(registerview, 'onProviderChosen').and.callThrough();
         spyOn(registerview, 'fetchRegistrationForm').and.callThrough();
         registerview.delegateEvents();  // We need to rebind all events otherwise our spy won't be called
 
         // Open the register panel
-        cbview.el.querySelector('.toggle-register-login').click();
+        cbview.querySelector('.toggle-register-login').click();
 
         // Check the form layout
-        const form = cbview.el.querySelector('#converse-register');
+        const form = cbview.querySelector('#converse-register');
         expect(form.querySelectorAll('input').length).toEqual(2);
         expect(form.querySelectorAll('input')[0].getAttribute('name')).toEqual('domain');
         expect(sizzle('input:last', form).pop().getAttribute('type')).toEqual('submit');
@@ -100,7 +100,7 @@ describe("The Registration Panel", function () {
 
         await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
         const cbview = _converse.api.controlbox.get();
-        cbview.el.querySelector('.toggle-register-login').click();
+        cbview.querySelector('.toggle-register-login').click();
 
         const registerview = _converse.chatboxviews.get('controlbox').registerpanel;
         spyOn(registerview, 'fetchRegistrationForm').and.callThrough();
@@ -112,8 +112,8 @@ describe("The Registration Panel", function () {
 
         expect(registerview._registering).toBeFalsy();
         expect(_converse.api.connection.connected()).toBeFalsy();
-        registerview.el.querySelector('input[name=domain]').value  = 'conversejs.org';
-        registerview.el.querySelector('input[type=submit]').click();
+        registerview.querySelector('input[name=domain]').value  = 'conversejs.org';
+        registerview.querySelector('input[type=submit]').click();
         expect(registerview.onProviderChosen).toHaveBeenCalled();
         expect(registerview._registering).toBeTruthy();
         await u.waitUntil(() => registerview.fetchRegistrationForm.calls.count());
@@ -140,9 +140,9 @@ describe("The Registration Panel", function () {
         _converse.connection._dataRecv(mock.createRequest(stanza));
         expect(registerview.onRegistrationFields).toHaveBeenCalled();
         expect(registerview.renderRegistrationForm).toHaveBeenCalled();
-        expect(registerview.el.querySelectorAll('input').length).toBe(5);
-        expect(registerview.el.querySelectorAll('input[type=submit]').length).toBe(1);
-        expect(registerview.el.querySelectorAll('input[type=button]').length).toBe(1);
+        expect(registerview.querySelectorAll('input').length).toBe(5);
+        expect(registerview.querySelectorAll('input[type=submit]').length).toBe(1);
+        expect(registerview.querySelectorAll('input[type=button]').length).toBe(1);
         done();
     }));
 
@@ -163,7 +163,7 @@ describe("The Registration Panel", function () {
             toggle.click();
         }
         const cbview = _converse.api.controlbox.get();
-        cbview.el.querySelector('.toggle-register-login').click();
+        cbview.querySelector('.toggle-register-login').click();
 
         const registerview = cbview.registerpanel;
         spyOn(registerview, 'onProviderChosen').and.callThrough();
@@ -172,8 +172,8 @@ describe("The Registration Panel", function () {
         spyOn(registerview, 'renderRegistrationForm').and.callThrough();
         registerview.delegateEvents();  // We need to rebind all events otherwise our spy won't be called
 
-        registerview.el.querySelector('input[name=domain]').value = 'conversejs.org';
-        registerview.el.querySelector('input[type=submit]').click();
+        registerview.querySelector('input[name=domain]').value = 'conversejs.org';
+        registerview.querySelector('input[type=submit]').click();
 
         let stanza = new Strophe.Builder("stream:features", {
                     'xmlns:stream': "http://etherx.jabber.org/streams",
@@ -194,12 +194,12 @@ describe("The Registration Panel", function () {
         _converse.connection._dataRecv(mock.createRequest(stanza));
         expect(registerview.form_type).toBe('legacy');
 
-        registerview.el.querySelector('input[name=username]').value = 'testusername';
-        registerview.el.querySelector('input[name=password]').value = 'testpassword';
-        registerview.el.querySelector('input[name=email]').value = 'test@email.local';
+        registerview.querySelector('input[name=username]').value = 'testusername';
+        registerview.querySelector('input[name=password]').value = 'testpassword';
+        registerview.querySelector('input[name=email]').value = 'test@email.local';
 
         spyOn(_converse.connection, 'send');
-        registerview.el.querySelector('input[type=submit]').click();
+        registerview.querySelector('input[type=submit]').click();
 
         expect(_converse.connection.send).toHaveBeenCalled();
         stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
@@ -227,7 +227,7 @@ describe("The Registration Panel", function () {
             toggle.click();
         }
         const cbview = _converse.api.controlbox.get();
-        cbview.el.querySelector('.toggle-register-login').click();
+        cbview.querySelector('.toggle-register-login').click();
         const registerview = _converse.chatboxviews.get('controlbox').registerpanel;
         spyOn(registerview, 'onProviderChosen').and.callThrough();
         spyOn(registerview, 'getRegistrationFields').and.callThrough();
@@ -235,8 +235,8 @@ describe("The Registration Panel", function () {
         spyOn(registerview, 'renderRegistrationForm').and.callThrough();
         registerview.delegateEvents();  // We need to rebind all events otherwise our spy won't be called
 
-        registerview.el.querySelector('input[name=domain]').value = 'conversejs.org';
-        registerview.el.querySelector('input[type=submit]').click();
+        registerview.querySelector('input[name=domain]').value = 'conversejs.org';
+        registerview.querySelector('input[type=submit]').click();
 
         let stanza = new Strophe.Builder("stream:features", {
                     'xmlns:stream': "http://etherx.jabber.org/streams",
@@ -259,13 +259,13 @@ describe("The Registration Panel", function () {
         _converse.connection._dataRecv(mock.createRequest(stanza));
         expect(registerview.form_type).toBe('xform');
 
-        registerview.el.querySelector('input[name=username]').value = 'testusername';
-        registerview.el.querySelector('input[name=password]').value = 'testpassword';
-        registerview.el.querySelector('input[name=email]').value = 'test@email.local';
+        registerview.querySelector('input[name=username]').value = 'testusername';
+        registerview.querySelector('input[name=password]').value = 'testpassword';
+        registerview.querySelector('input[name=email]').value = 'test@email.local';
 
         spyOn(_converse.connection, 'send');
 
-        registerview.el.querySelector('input[type=submit]').click();
+        registerview.querySelector('input[type=submit]').click();
 
         expect(_converse.connection.send).toHaveBeenCalled();
         stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
@@ -308,7 +308,7 @@ describe("The Registration Panel", function () {
             toggle.click();
         }
         const cbview = _converse.chatboxviews.get('controlbox');
-        cbview.el.querySelector('.toggle-register-login').click();
+        cbview.querySelector('.toggle-register-login').click();
         const registerview = _converse.chatboxviews.get('controlbox').registerpanel;
         spyOn(registerview, 'onProviderChosen').and.callThrough();
         spyOn(registerview, 'getRegistrationFields').and.callThrough();
@@ -316,8 +316,8 @@ describe("The Registration Panel", function () {
         spyOn(registerview, 'renderRegistrationForm').and.callThrough();
         registerview.delegateEvents();  // We need to rebind all events otherwise our spy won't be called
 
-        registerview.el.querySelector('input[name=domain]').value = 'conversejs.org';
-        registerview.el.querySelector('input[type=submit]').click();
+        registerview.querySelector('input[name=domain]').value = 'conversejs.org';
+        registerview.querySelector('input[type=submit]').click();
 
         let stanza = new Strophe.Builder("stream:features", {
                     'xmlns:stream': "http://etherx.jabber.org/streams",
@@ -353,7 +353,7 @@ describe("The Registration Panel", function () {
             </iq>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
         expect(registerview.form_type).toBe('xform');
-        expect(registerview.el.querySelectorAll('#converse-register input[required]').length).toBe(3);
+        expect(registerview.querySelectorAll('#converse-register input[required]').length).toBe(3);
         // Hide the controlbox so that we can see whether the test
         // passed or failed
         u.addClass('hidden', _converse.chatboxviews.get('controlbox').el);
@@ -379,12 +379,12 @@ describe("The Registration Panel", function () {
             toggle.click();
         }
         const cbview = _converse.chatboxviews.get('controlbox');
-        cbview.el.querySelector('.toggle-register-login').click();
+        cbview.querySelector('.toggle-register-login').click();
         const view = _converse.chatboxviews.get('controlbox').registerpanel;
         view.delegateEvents();  // We need to rebind all events otherwise our spy won't be called
 
-        view.el.querySelector('input[name=domain]').value = 'conversejs.org';
-        view.el.querySelector('input[type=submit]').click();
+        view.querySelector('input[name=domain]').value = 'conversejs.org';
+        view.querySelector('input[type=submit]').click();
 
         let stanza = new Strophe.Builder("stream:features", {
                     'xmlns:stream': "http://etherx.jabber.org/streams",
@@ -421,13 +421,13 @@ describe("The Registration Panel", function () {
         _converse.connection._dataRecv(mock.createRequest(stanza));
 
         spyOn(view, 'submitRegistrationForm').and.callThrough();
-        const username_input = view.el.querySelector('[name="username"]');
+        const username_input = view.querySelector('[name="username"]');
         username_input.value = 'romeo';
-        const password_input = view.el.querySelector('[name="password"]');
+        const password_input = view.querySelector('[name="password"]');
         password_input.value = 'secret';
-        const ocr_input = view.el.querySelector('[name="ocr"]');
+        const ocr_input = view.querySelector('[name="ocr"]');
         ocr_input.value = '8m9D88';
-        view.el.querySelector('[type="submit"]').click();
+        view.querySelector('[type="submit"]').click();
         expect(view.submitRegistrationForm).toHaveBeenCalled();
 
         const response_IQ = u.toStanza(`
@@ -439,7 +439,7 @@ describe("The Registration Panel", function () {
                 </error>
             </iq>`);
         _converse.connection._dataRecv(mock.createRequest(response_IQ));
-        expect(view.el.querySelector('.error')?.textContent.trim()).toBe('Too many CAPTCHA requests');
+        expect(view.querySelector('.error')?.textContent.trim()).toBe('Too many CAPTCHA requests');
         delete _converse.connection;
         done();
     }));

+ 69 - 69
spec/retractions.js

@@ -6,7 +6,7 @@ const u = converse.env.utils;
 
 async function sendAndThenRetractMessage (_converse, view) {
     view.model.sendMessage('hello world');
-    await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 1);
+    await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 1);
     const msg_obj = view.model.messages.last();
     const reflection_stanza = u.toStanza(`
         <message xmlns="jabber:client"
@@ -20,9 +20,9 @@ async function sendAndThenRetractMessage (_converse, view) {
             <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
         </message>`);
     await view.model.handleMessageStanza(reflection_stanza);
-    await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
+    await u.waitUntil(() => view.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
 
-    const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
+    const retract_button = await u.waitUntil(() => view.querySelector('.chat-msg__content .chat-msg__action-retract'));
     retract_button.click();
     await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
     const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
@@ -53,7 +53,7 @@ describe("Message Retractions", function () {
             `);
             const view = _converse.api.chatviews.get(muc_jid);
             await view.model.handleMessageStanza(received_stanza);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
             expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
             expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
 
@@ -69,7 +69,7 @@ describe("Message Retractions", function () {
             _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
             await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
             expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true);
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
             expect(view.model.messages.length).toBe(2);
             expect(view.model.messages.at(1).get('retracted')).toBeTruthy();
             expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy();
@@ -119,7 +119,7 @@ describe("Message Retractions", function () {
             `);
             _converse.connection._dataRecv(mock.createRequest(received_stanza));
             await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1, 1000);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1, 1000);
             expect(view.model.messages.length).toBe(1);
 
             const message = view.model.messages.at(0)
@@ -179,8 +179,8 @@ describe("Message Retractions", function () {
             _converse.connection._dataRecv(mock.createRequest(received_stanza));
             await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
 
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
             expect(view.model.messages.length).toBe(1);
 
             const message = view.model.messages.at(0)
@@ -227,7 +227,7 @@ describe("Message Retractions", function () {
             expect(message.get('dangling_retraction')).toBe(true);
             expect(message.get('is_ephemeral')).toBe(false);
             expect(message.get('retracted')).toBeTruthy();
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(0);
 
             const stanza = u.toStanza(`
                 <message xmlns="jabber:client"
@@ -279,7 +279,7 @@ describe("Message Retractions", function () {
 
             _converse.connection._dataRecv(mock.createRequest(stanza));
             await u.waitUntil(() => view.model.messages.length === 1);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
 
             stanza = u.toStanza(`
                 <message xmlns="jabber:client"
@@ -295,7 +295,7 @@ describe("Message Retractions", function () {
 
             _converse.connection._dataRecv(mock.createRequest(stanza));
             await u.waitUntil(() => view.model.messages.length === 2);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2);
 
             const retraction_stanza =  u.toStanza(`
                 <message id="${u.getUniqueId()}"
@@ -309,16 +309,16 @@ describe("Message Retractions", function () {
                 </message>
             `);
             _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
 
             expect(view.model.messages.length).toBe(2);
 
             const message = view.model.messages.at(1);
             expect(message.get('retracted')).toBeTruthy();
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-            const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message');
             expect(msg_el.textContent.trim()).toBe('Mercutio has removed this message');
-            expect(u.hasClass('chat-msg--followup', view.el.querySelector('.chat-msg--retracted'))).toBe(true);
+            expect(u.hasClass('chat-msg--followup', view.querySelector('.chat-msg--retracted'))).toBe(true);
             done();
         }));
     });
@@ -335,7 +335,7 @@ describe("Message Retractions", function () {
             const view = await mock.openChatBoxFor(_converse, contact_jid);
 
             view.model.sendMessage('hello world');
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
 
             const message = view.model.messages.at(0);
             expect(view.model.messages.length).toBe(1);
@@ -343,14 +343,14 @@ describe("Message Retractions", function () {
             expect(message.get('editable')).toBeTruthy();
 
 
-            const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
+            const retract_button = await u.waitUntil(() => view.querySelector('.chat-msg__content .chat-msg__action-retract'));
             retract_button.click();
             await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
             const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
             submit_button.click();
 
             const sent_stanzas = _converse.connection.sent_stanzas;
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
 
             const msg_obj = view.model.messages.at(0);
             const retraction_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
@@ -365,8 +365,8 @@ describe("Message Retractions", function () {
             expect(view.model.messages.length).toBe(1);
             expect(message.get('retracted')).toBeTruthy();
             expect(message.get('editable')).toBeFalsy();
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-            const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            const el = view.querySelector('.chat-msg--retracted .chat-msg__message');
             expect(el.textContent.trim()).toBe('Romeo Montague has removed this message');
             done();
         }));
@@ -393,7 +393,7 @@ describe("Message Retractions", function () {
             `);
             const view = _converse.api.chatviews.get(muc_jid);
             await view.model.handleMessageStanza(received_stanza);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
             expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
             expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
 
@@ -407,12 +407,12 @@ describe("Message Retractions", function () {
             _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
 
             // We opportunistically save the message as retracted, even before receiving the retraction message
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
             expect(view.model.messages.length).toBe(1);
             expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
             expect(view.model.messages.at(0).get('editable')).toBe(false);
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-            const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message');
             expect(msg_el.textContent.trim()).toBe('eve has removed this message');
             expect(msg_el.querySelector('.chat-msg--retracted q')).toBe(null);
             done();
@@ -443,7 +443,7 @@ describe("Message Retractions", function () {
             expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
 
             const reason = "This content is inappropriate for this forum!"
-            const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
+            const retract_button = await u.waitUntil(() => view.querySelector('.chat-msg__content .chat-msg__action-retract'));
             retract_button.click();
 
             await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
@@ -472,15 +472,15 @@ describe("Message Retractions", function () {
             _converse.connection._dataRecv(mock.createRequest(result_iq));
 
             // We opportunistically save the message as retracted, even before receiving the retraction message
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
             expect(view.model.messages.length).toBe(1);
             expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
             expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
             expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
             expect(view.model.messages.at(0).get('editable')).toBe(false);
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
 
-            const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
+            const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message');
             expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message');
 
             const qel = msg_el.querySelector('q');
@@ -523,8 +523,8 @@ describe("Message Retractions", function () {
                 </message>
             `);
             await view.model.handleMessageStanza(received_stanza);
-            await u.waitUntil(() => view.el.querySelector('.chat-msg__content'));
-            expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null);
+            await u.waitUntil(() => view.querySelector('.chat-msg__content'));
+            expect(view.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null);
             const result = await view.model.canModerateMessages();
             expect(result).toBe(false);
             done();
@@ -553,7 +553,7 @@ describe("Message Retractions", function () {
             await u.waitUntil(() => view.model.messages.length === 1);
             expect(view.model.messages.length).toBe(1);
 
-            const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
+            const retract_button = await u.waitUntil(() => view.querySelector('.chat-msg__content .chat-msg__action-retract'));
             retract_button.click();
             await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
 
@@ -579,13 +579,13 @@ describe("Message Retractions", function () {
                 </message>`);
             await view.model.handleMessageStanza(retraction);
 
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
             expect(view.model.messages.length).toBe(1);
             expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-            const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
             expect(msg_el.textContent).toBe('romeo has removed this message');
-            const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
+            const qel = view.querySelector('.chat-msg--retracted .chat-msg__message q');
             expect(qel.textContent).toBe('This content is inappropriate for this forum!');
 
             const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
@@ -615,7 +615,7 @@ describe("Message Retractions", function () {
             expect(occupant.get('role')).toBe('moderator');
             occupant.save('role', 'member');
             const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1, 1000);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1, 1000);
             console.log('XXX: First message retracted by author');
 
             const msg_obj = view.model.messages.last();
@@ -654,8 +654,8 @@ describe("Message Retractions", function () {
             expect(view.model.messages.last().get('retracted')).toBeTruthy();
             expect(view.model.messages.last().get('is_ephemeral')).toBe(false);
             expect(view.model.messages.last().get('editable')).toBe(false);
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-            const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
             expect(el.textContent).toBe('romeo has removed this message');
             done();
         }));
@@ -672,13 +672,13 @@ describe("Message Retractions", function () {
             const occupant = view.model.getOwnOccupant();
             expect(occupant.get('role')).toBe('moderator');
             occupant.save('role', 'member');
-            await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("romeo is no longer a moderator"));
+            await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent.includes("romeo is no longer a moderator"));
             const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1, 1000);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1, 1000);
 
             expect(view.model.messages.length).toBe(1);
             await u.waitUntil(() => view.model.messages.last().get('retracted'), 1000);
-            const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
+            const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
             expect(el.textContent.trim()).toBe('romeo has removed this message');
 
             const message = view.model.messages.last();
@@ -697,14 +697,14 @@ describe("Message Retractions", function () {
                 </message>`);
 
             _converse.connection._dataRecv(mock.createRequest(error));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__error').length === 1, 1000);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0, 1000);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg__error').length === 1, 1000);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 0, 1000);
             expect(view.model.messages.length).toBe(1);
             expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
             expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
             expect(view.model.messages.at(0).get('editable')).toBe(false);
 
-            const errmsg = view.el.querySelector('.chat-msg__error');
+            const errmsg = view.querySelector('.chat-msg__error');
             expect(errmsg.textContent.trim()).toBe("You're not allowed to retract your message.");
             done();
         }));
@@ -723,23 +723,23 @@ describe("Message Retractions", function () {
             const occupant = view.model.getOwnOccupant();
             expect(occupant.get('role')).toBe('moderator');
             occupant.save('role', 'member');
-            await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("romeo is no longer a moderator"))
+            await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent.includes("romeo is no longer a moderator"))
             await sendAndThenRetractMessage(_converse, view);
             expect(view.model.messages.length).toBe(1);
             expect(view.model.messages.last().get('retracted')).toBeTruthy();
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
-            const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
+            const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
             expect(el.textContent.trim()).toBe('romeo has removed this message');
 
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
 
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 0);
             expect(view.model.messages.length).toBe(1);
             expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
             expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
             expect(view.model.messages.at(0).get('editable')).toBeTruthy();
 
-            const error_messages = view.el.querySelectorAll('.chat-msg__error');
+            const error_messages = view.querySelectorAll('.chat-msg__error');
             expect(error_messages.length).toBe(1);
             expect(error_messages[0].textContent.trim()).toBe('A timeout happened while while trying to retract your message.');
             done();
@@ -759,7 +759,7 @@ describe("Message Retractions", function () {
             expect(occupant.get('role')).toBe('moderator');
 
             view.model.sendMessage('Visit this site to get free bitcoin');
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
             const stanza_id = 'retraction-id-1';
             const msg_obj = view.model.messages.at(0);
             const reflection_stanza = u.toStanza(`
@@ -774,7 +774,7 @@ describe("Message Retractions", function () {
                     <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
                 </message>`);
             await view.model.handleMessageStanza(reflection_stanza);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
             expect(view.model.messages.length).toBe(1);
             expect(view.model.messages.at(0).get('editable')).toBe(true);
 
@@ -811,7 +811,7 @@ describe("Message Retractions", function () {
             expect(occupant.get('role')).toBe('moderator');
 
             view.model.sendMessage('Visit this site to get free bitcoin');
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);
             const stanza_id = 'retraction-id-1';
             const msg_obj = view.model.messages.at(0);
             const reflection_stanza = u.toStanza(`
@@ -826,7 +826,7 @@ describe("Message Retractions", function () {
                     <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
                 </message>`);
             await view.model.handleMessageStanza(reflection_stanza);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
             expect(view.model.messages.length).toBe(1);
             expect(view.model.messages.at(0).get('editable')).toBe(true);
 
@@ -853,15 +853,15 @@ describe("Message Retractions", function () {
             _converse.connection._dataRecv(mock.createRequest(result_iq));
 
             // We opportunistically save the message as retracted, even before receiving the retraction message
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
             expect(view.model.messages.length).toBe(1);
             expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
             expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined);
             expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
             expect(view.model.messages.at(0).get('editable')).toBe(false);
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
 
-            const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
+            const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message');
             expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message');
             expect(msg_el.querySelector('q')).toBe(null);
 
@@ -967,9 +967,9 @@ describe("Message Retractions", function () {
             expect(await view.model.handleRetraction.calls.first().returnValue).toBe(false);
             expect(await view.model.handleRetraction.calls.all()[1].returnValue).toBe(false);
             expect(await view.model.handleRetraction.calls.all()[2].returnValue).toBe(true);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-            const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2);
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
             expect(el.textContent.trim()).toBe('Mercutio has removed this message');
             expect(u.hasClass('chat-msg--followup', el.parentElement)).toBe(false);
             done();
@@ -1043,10 +1043,10 @@ describe("Message Retractions", function () {
             message = view.model.messages.at(0);
             expect(message.get('retracted')).toBeTruthy();
             expect(message.get('is_tombstone')).toBe(true);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-            const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
             expect(el.textContent.trim()).toBe('eve has removed this message');
             done();
         }));
@@ -1128,13 +1128,13 @@ describe("Message Retractions", function () {
             expect(message.get('is_tombstone')).toBe(true);
             expect(message.get('moderation_reason')).toBe("This message contains inappropriate content");
 
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length, 500);
-            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length, 500);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
 
-            expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-            const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
+            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
+            const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
             expect(el.textContent.trim()).toBe('A moderator has removed this message');
-            const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
+            const qel = view.querySelector('.chat-msg--retracted .chat-msg__message q');
             expect(qel.textContent.trim()).toBe('This message contains inappropriate content');
             done();
         }));

+ 1 - 1
spec/room_registration.js

@@ -18,7 +18,7 @@ describe("Chatrooms", function () {
             const muc_jid = 'coven@chat.shakespeare.lit';
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo')
             const view = _converse.chatboxviews.get(muc_jid);
-            const textarea = view.el.querySelector('.chat-textarea')
+            const textarea = view.querySelector('.chat-textarea')
             textarea.value = '/register';
             view.onKeyDown({
                 target: textarea,

+ 27 - 27
spec/roster.js

@@ -153,13 +153,13 @@ describe("The Contacts Roster", function () {
                 ['rosterGroupsFetched'], {},
                 async function (done, _converse) {
 
-            const filter = _converse.rosterview.el.querySelector('.roster-filter');
+            const filter = _converse.rosterview.querySelector('.roster-filter');
             expect(filter === null).toBe(false);
             await mock.waitForRoster(_converse, 'current');
             await mock.openControlBox(_converse);
 
             const view = _converse.chatboxviews.get('controlbox');
-            const flyout = view.el.querySelector('.box-flyout');
+            const flyout = view.querySelector('.box-flyout');
             const panel = flyout.querySelector('.controlbox-pane');
             function hasScrollBar (el) {
                 return el.isConnected && flyout.offsetHeight < panel.scrollHeight;
@@ -176,7 +176,7 @@ describe("The Contacts Roster", function () {
 
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current');
-            let filter = _converse.rosterview.el.querySelector('.roster-filter');
+            let filter = _converse.rosterview.querySelector('.roster-filter');
             const roster = _converse.rosterview.roster_el;
             _converse.rosterview.filter_view.delegateEvents();
 
@@ -194,7 +194,7 @@ describe("The Contacts Roster", function () {
             const visible_group = sizzle('.roster-group', roster).filter(u.isVisible).pop();
             expect(visible_group.querySelector('a.group-toggle').textContent.trim()).toBe('friends & acquaintences');
 
-            filter = _converse.rosterview.el.querySelector('.roster-filter');
+            filter = _converse.rosterview.querySelector('.roster-filter');
             filter.value = "j";
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 2), 700);
@@ -207,14 +207,14 @@ describe("The Contacts Roster", function () {
             expect(visible_groups[0].textContent.trim()).toBe('friends & acquaintences');
             expect(visible_groups[1].textContent.trim()).toBe('Ungrouped');
 
-            filter = _converse.rosterview.el.querySelector('.roster-filter');
+            filter = _converse.rosterview.querySelector('.roster-filter');
             filter.value = "xxx";
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 0), 600);
             visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle'));
             expect(visible_groups.length).toBe(0);
 
-            filter = _converse.rosterview.el.querySelector('.roster-filter');
+            filter = _converse.rosterview.querySelector('.roster-filter');
             filter.value = "";
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600);
@@ -230,7 +230,7 @@ describe("The Contacts Roster", function () {
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current');
 
-            const filter = _converse.rosterview.el.querySelector('.roster-filter');
+            const filter = _converse.rosterview.querySelector('.roster-filter');
             const roster = _converse.rosterview.roster_el;
             _converse.rosterview.filter_view.delegateEvents();
 
@@ -278,13 +278,13 @@ describe("The Contacts Roster", function () {
             _converse.rosterview.filter_view.delegateEvents();
             var roster = _converse.rosterview.roster_el;
 
-            var button = _converse.rosterview.el.querySelector('span[data-type="groups"]');
+            var button = _converse.rosterview.querySelector('span[data-type="groups"]');
             button.click();
 
             await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600);
             expect(sizzle('.roster-group', roster).filter(u.isVisible).length).toBe(5);
 
-            var filter = _converse.rosterview.el.querySelector('.roster-filter');
+            var filter = _converse.rosterview.querySelector('.roster-filter');
             filter.value = "colleagues";
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
 
@@ -294,14 +294,14 @@ describe("The Contacts Roster", function () {
             // Check that all contacts under the group are shown
             expect(sizzle('div.roster-group:not(.collapsed) li', roster).filter(l => !u.isVisible(l)).length).toBe(0);
 
-            filter = _converse.rosterview.el.querySelector('.roster-filter');
+            filter = _converse.rosterview.querySelector('.roster-filter');
             filter.value = "xxx";
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
 
             await u.waitUntil(() => (roster.querySelectorAll('div.roster-group.collapsed').length === 5), 700);
             expect(roster.querySelectorAll('div.roster-group:not(.collapsed) a').length).toBe(0);
 
-            filter = _converse.rosterview.el.querySelector('.roster-filter');
+            filter = _converse.rosterview.querySelector('.roster-filter');
             filter.value = ""; // Check that groups are shown again, when the filter string is cleared.
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
             await u.waitUntil(() => (roster.querySelectorAll('div.roster-group.collapsed').length === 0), 700);
@@ -318,15 +318,15 @@ describe("The Contacts Roster", function () {
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current');
 
-            const filter = _converse.rosterview.el.querySelector('.roster-filter');
+            const filter = _converse.rosterview.querySelector('.roster-filter');
             filter.value = "xxx";
             u.triggerEvent(filter, "keydown", "KeyboardEvent");
             expect(_.includes(filter.classList, "x")).toBeFalsy();
-            expect(u.hasClass('hidden', _converse.rosterview.el.querySelector('.roster-filter-form .clear-input'))).toBeTruthy();
+            expect(u.hasClass('hidden', _converse.rosterview.querySelector('.roster-filter-form .clear-input'))).toBeTruthy();
 
             const isHidden = _.partial(u.hasClass, 'hidden');
-            await u.waitUntil(() => !isHidden(_converse.rosterview.el.querySelector('.roster-filter-form .clear-input')), 900);
-            _converse.rosterview.el.querySelector('.clear-input').click();
+            await u.waitUntil(() => !isHidden(_converse.rosterview.querySelector('.roster-filter-form .clear-input')), 900);
+            _converse.rosterview.querySelector('.clear-input').click();
             expect(document.querySelector('.roster-filter').value).toBe("");
             done();
         }));
@@ -344,11 +344,11 @@ describe("The Contacts Roster", function () {
             jid = mock.cur_names[4].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             _converse.roster.get(jid).presence.set('show', 'dnd');
             mock.openControlBox(_converse);
-            const button = _converse.rosterview.el.querySelector('span[data-type="state"]');
+            const button = _converse.rosterview.querySelector('span[data-type="state"]');
             button.click();
             const roster = _converse.rosterview.roster_el;
             await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 15, 900);
-            const filter = _converse.rosterview.el.querySelector('.state-type');
+            const filter = _converse.rosterview.querySelector('.state-type');
             expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5);
             filter.value = "online";
             u.triggerEvent(filter, 'change');
@@ -565,7 +565,7 @@ describe("The Contacts Roster", function () {
                 }
             });
             const view = _converse.rosterview.get('Colleagues');
-            const toggle = view.el.querySelector('a.group-toggle');
+            const toggle = view.querySelector('a.group-toggle');
             expect(view.model.get('state')).toBe('opened');
             toggle.click();
             await u.waitUntil(() => view.model.get('state') === 'closed');
@@ -736,7 +736,7 @@ describe("The Contacts Roster", function () {
             await u.waitUntil(() => sizzle('li', _converse.rosterview.get('Pending contacts').el).filter(u.isVisible).length, 900);
             // Check that they are sorted alphabetically
             const view = _converse.rosterview.get('Pending contacts');
-            const spans = view.el.querySelectorAll('.pending-xmpp-contact span');
+            const spans = view.querySelectorAll('.pending-xmpp-contact span');
             const t = _.reduce(spans, (result, value) => result + _.trim(value.textContent), '');
             expect(t).toEqual(mock.pend_names.slice(0,i+1).sort().join(''));
             done();
@@ -757,7 +757,7 @@ describe("The Contacts Roster", function () {
 
             await _addContacts(_converse);
             await u.waitUntil(() => sizzle('li', _converse.rosterview.el).filter(u.isVisible).length, 500);
-            await checkHeaderToggling.apply(_converse, [_converse.rosterview.el.querySelector('.roster-group')]);
+            await checkHeaderToggling.apply(_converse, [_converse.rosterview.querySelector('.roster-group')]);
             done();
         }));
 
@@ -769,7 +769,7 @@ describe("The Contacts Roster", function () {
             _converse.roster_groups = false;
             await _addContacts(_converse);
             await u.waitUntil(() => sizzle('li', _converse.rosterview.el).filter(u.isVisible).length, 500);
-            _converse.rosterview.el.querySelector('.roster-group a.group-toggle').click();
+            _converse.rosterview.querySelector('.roster-group a.group-toggle').click();
             const name = "Romeo Montague";
             const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             _converse.roster.create({
@@ -814,7 +814,7 @@ describe("The Contacts Roster", function () {
                 async function (done, _converse) {
 
             await _addContacts(_converse);
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('li').length);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('li').length);
             const name = mock.cur_names[0];
             const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const contact = _converse.roster.get(jid);
@@ -857,12 +857,12 @@ describe("The Contacts Roster", function () {
             spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
                 if (typeof callback === "function") { return callback(); }
             });
-            expect(u.isVisible(_converse.rosterview.el.querySelector('.roster-group'))).toBe(true);
+            expect(u.isVisible(_converse.rosterview.querySelector('.roster-group'))).toBe(true);
             sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop().click();
             expect(window.confirm).toHaveBeenCalled();
             expect(_converse.connection.sendIQ).toHaveBeenCalled();
             expect(contact.removeFromRoster).toHaveBeenCalled();
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length === 0);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group').length === 0);
             done();
         }));
 
@@ -872,7 +872,7 @@ describe("The Contacts Roster", function () {
                 async function (done, _converse) {
 
             await _addContacts(_converse);
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700);
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group li').length, 700);
             const roster = _converse.rosterview.el;
             const groups = roster.querySelectorAll('.roster-group');
             const groupnames = Array.from(groups).map(g => g.getAttribute('data-group'));
@@ -1025,7 +1025,7 @@ describe("The Contacts Roster", function () {
                 _converse.roster.get(jid).presence.set('show', 'unavailable');
             }
 
-            await u.waitUntil(() => u.isVisible(_converse.rosterview.el.querySelector('li:first-child')), 900);
+            await u.waitUntil(() => u.isVisible(_converse.rosterview.querySelector('li:first-child')), 900);
             const roster = _converse.rosterview.el;
             const groups = roster.querySelectorAll('.roster-group');
             const groupnames = Array.from(groups).map(g => g.getAttribute('data-group'));
@@ -1165,7 +1165,7 @@ describe("The Contacts Roster", function () {
             const jid =  name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const contact = _converse.roster.get(jid);
             spyOn(contact, 'authorize').and.callFake(() => contact);
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length)
+            await u.waitUntil(() => _converse.rosterview.querySelectorAll('.roster-group li').length)
             // TODO: Testing can be more thorough here, the user is
             // actually not accepted/authorized because of
             // mock_connection.

+ 23 - 23
spec/spoilers.js

@@ -39,10 +39,10 @@ describe("A spoiler message", function () {
         const view = _converse.chatboxviews.get(sender_jid);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
-        expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Mercutio');
-        const message_content = view.el.querySelector('.chat-msg__text');
+        expect(view.querySelector('.chat-msg__author').textContent.trim()).toBe('Mercutio');
+        const message_content = view.querySelector('.chat-msg__text');
         await u.waitUntil(() => message_content.textContent === spoiler);
-        const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
+        const spoiler_hint_el = view.querySelector('.spoiler-hint');
         expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
         done();
     }));
@@ -75,13 +75,13 @@ describe("A spoiler message", function () {
         await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve));
         const view = _converse.chatboxviews.get(sender_jid);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        await u.waitUntil(() => u.isVisible(view.el));
+        await u.waitUntil(() => u.isVisible(view));
         await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
-        await u.waitUntil(() => u.isVisible(view.el.querySelector('.chat-msg__author')));
-        expect(view.el.querySelector('.chat-msg__author').textContent.includes('Mercutio')).toBeTruthy();
-        const message_content = view.el.querySelector('.chat-msg__text');
+        await u.waitUntil(() => u.isVisible(view.querySelector('.chat-msg__author')));
+        expect(view.querySelector('.chat-msg__author').textContent.includes('Mercutio')).toBeTruthy();
+        const message_content = view.querySelector('.chat-msg__text');
         await u.waitUntil(() => message_content.textContent === spoiler);
-        const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
+        const spoiler_hint_el = view.querySelector('.spoiler-hint');
         expect(spoiler_hint_el.textContent).toBe('');
         done();
     }));
@@ -112,11 +112,11 @@ describe("A spoiler message", function () {
         const view = _converse.api.chatviews.get(contact_jid);
         spyOn(_converse.connection, 'send');
 
-        await u.waitUntil(() => view.el.querySelector('.toggle-compose-spoiler'));
-        let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+        await u.waitUntil(() => view.querySelector('.toggle-compose-spoiler'));
+        let spoiler_toggle = view.querySelector('.toggle-compose-spoiler');
         spoiler_toggle.click();
 
-        const textarea = view.el.querySelector('.chat-textarea');
+        const textarea = view.querySelector('.chat-textarea');
         textarea.value = 'This is the spoiler';
         view.onKeyDown({
             target: textarea,
@@ -147,15 +147,15 @@ describe("A spoiler message", function () {
         expect(body_el.textContent).toBe(spoiler);
 
         /* Test the HTML spoiler message */
-        expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
+        expect(view.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
 
-        const message_content = view.el.querySelector('.chat-msg__text');
+        const message_content = view.querySelector('.chat-msg__text');
         await u.waitUntil(() => message_content.textContent === spoiler);
 
-        const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
+        const spoiler_msg_el = view.querySelector('.chat-msg__text.spoiler');
         expect(Array.from(spoiler_msg_el.classList).includes('hidden')).toBeTruthy();
 
-        spoiler_toggle = view.el.querySelector('.spoiler-toggle');
+        spoiler_toggle = view.querySelector('.spoiler-toggle');
         expect(spoiler_toggle.textContent.trim()).toBe('Show more');
         spoiler_toggle.click();
         await u.waitUntil(() => !Array.from(spoiler_msg_el.classList).includes('hidden'));
@@ -190,15 +190,15 @@ describe("A spoiler message", function () {
         await mock.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]);
         const view = _converse.api.chatviews.get(contact_jid);
 
-        await u.waitUntil(() => view.el.querySelector('.toggle-compose-spoiler'));
-        let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+        await u.waitUntil(() => view.querySelector('.toggle-compose-spoiler'));
+        let spoiler_toggle = view.querySelector('.toggle-compose-spoiler');
         spoiler_toggle.click();
 
         spyOn(_converse.connection, 'send');
 
-        const textarea = view.el.querySelector('.chat-textarea');
+        const textarea = view.querySelector('.chat-textarea');
         textarea.value = 'This is the spoiler';
-        const hint_input = view.el.querySelector('.spoiler-hint');
+        const hint_input = view.querySelector('.spoiler-hint');
         hint_input.value = 'This is the hint';
 
         view.onKeyDown({
@@ -229,15 +229,15 @@ describe("A spoiler message", function () {
         const body_el = stanza.querySelector('body');
         expect(body_el.textContent).toBe(spoiler);
 
-        expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
+        expect(view.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
 
-        const message_content = view.el.querySelector('.chat-msg__text');
+        const message_content = view.querySelector('.chat-msg__text');
         await u.waitUntil(() => message_content.textContent === spoiler);
 
-        const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
+        const spoiler_msg_el = view.querySelector('.chat-msg__text.spoiler');
         expect(Array.from(spoiler_msg_el.classList).includes('hidden')).toBeTruthy();
 
-        spoiler_toggle = view.el.querySelector('.spoiler-toggle');
+        spoiler_toggle = view.querySelector('.spoiler-toggle');
         expect(spoiler_toggle.textContent.trim()).toBe('Show more');
         spoiler_toggle.click();
         await u.waitUntil(() => !Array.from(spoiler_msg_el.classList).includes('hidden'));

+ 29 - 29
spec/styling.js

@@ -29,7 +29,7 @@ describe("An incoming chat Message", function () {
         expect(view.model.messages.models[0].get('is_unstyled')).toBe(true);
 
         setTimeout(() => {
-            const msg_el = view.el.querySelector('converse-chat-message-body');
+            const msg_el = view.querySelector('converse-chat-message-body');
             expect(msg_el.innerText).toBe(msg_text);
             done();
         }, 500);
@@ -60,7 +60,7 @@ describe("An incoming chat Message", function () {
         expect(view.model.messages.models[0].get('is_unstyled')).toBe(false);
 
         setTimeout(() => {
-            const msg_el = view.el.querySelector('converse-chat-message-body');
+            const msg_el = view.querySelector('converse-chat-message-body');
             expect(msg_el.innerText).toBe(msg_text);
             done();
         }, 500);
@@ -80,7 +80,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = view.el.querySelector('converse-chat-message-body');
+        msg_el = view.querySelector('converse-chat-message-body');
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             'This <span class="styling-directive">*</span>'+
@@ -94,7 +94,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
                 await _converse.handleMessageStanza(msg);
                 await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             'Here\'s a <span class="styling-directive">~</span><del>strikethrough section</del><span class="styling-directive">~</span>');
@@ -104,7 +104,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<span class="styling-directive">~</span>'+
@@ -117,7 +117,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<span class="styling-directive">*</span>'+
@@ -129,7 +129,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<span class="styling-directive">~</span><del> Hello! <span title=":poop:">💩</span> </del><span class="styling-directive">~</span>');
@@ -139,7 +139,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             'This *is not a styling hint \n'+
@@ -149,7 +149,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '(There are three blocks in this body marked by parens,)\n'+
@@ -162,7 +162,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '_<span class="styling-directive">_</span><i> hello world </i><span class="styling-directive">_</span>');
@@ -172,7 +172,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             'Go to ~https://conversejs.org~now <span class="styling-directive">_</span><i>please</i><span class="styling-directive">_</span>');
@@ -181,7 +181,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             'Go to <span class="styling-directive">_</span>'+
@@ -205,7 +205,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             'Here\'s a code block: \n'+
@@ -217,7 +217,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<div class="styling-directive">```</div>'+
@@ -230,7 +230,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '```ignored\n (println "Hello, world!")\n ```\n\n'+
@@ -253,7 +253,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<blockquote>This is quoted text\nThis is also quoted</blockquote>\nThis is not quoted');
@@ -262,7 +262,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<blockquote>This is <span class="styling-directive">*</span><b>quoted</b><span class="styling-directive">*</span> text\n'+
@@ -273,7 +273,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
                 await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') === "<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
 
@@ -281,7 +281,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
                 await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') === "<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
 
@@ -289,7 +289,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<blockquote>'+
@@ -303,7 +303,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<blockquote>```\n (println "Hello, world!")</blockquote>\n\n'+
@@ -313,7 +313,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<blockquote>Also, icons.js is loaded from /dist, instead of dist.</blockquote>\n'+
@@ -323,7 +323,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<blockquote>Where is it located?</blockquote>\n'+
@@ -334,7 +334,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<blockquote>What do you think of it?</blockquote>\n <span title=":poop:">💩</span>');
@@ -343,7 +343,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             '<blockquote>What do you think of it?</blockquote>\n<span class="styling-directive">~</span><del>hello</del><span class="styling-directive">~</span>');
@@ -352,7 +352,7 @@ describe("An incoming chat Message", function () {
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') === 'hello world &gt; this is not a quote');
 
@@ -380,7 +380,7 @@ describe("An incoming chat Message", function () {
         await _converse.handleMessageStanza(msg);
 
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
-        msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             `<blockquote>What do you think of it <span class="mention">romeo</span>?</blockquote>\n Did you see this <span class="mention">romeo</span>?`);
@@ -407,10 +407,10 @@ describe("A outgoing groupchat Message", function () {
             }).c('body').t(msg_text).up()
                 .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'23', 'end':'29', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).nodeTree;
         await view.model.handleMessageStanza(msg);
-        const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
+        const message = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
         expect(message.classList.length).toEqual(1);
 
-        const msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
+        const msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
             'This <span class="styling-directive">*</span><b>message mentions <span class="mention mention--self badge badge-info">romeo</span></b><span class="styling-directive">*</span>');

+ 6 - 5
spec/user-details-modal.js

@@ -15,8 +15,9 @@ describe("The User Details Modal", function () {
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         await mock.openChatBoxFor(_converse, contact_jid);
         await u.waitUntil(() => _converse.chatboxes.length > 1);
+
         const view = _converse.chatboxviews.get(contact_jid);
-        let show_modal_button = view.el.querySelector('.show-user-details-modal');
+        let show_modal_button = view.querySelector('.show-user-details-modal');
         show_modal_button.click();
         const modal = _converse.api.modal.get('user-details-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 1000);
@@ -27,7 +28,7 @@ describe("The User Details Modal", function () {
         remove_contact_button.click();
         await u.waitUntil(() => modal.el.getAttribute('aria-hidden'), 1000);
         await u.waitUntil(() => !u.isVisible(modal.el));
-        show_modal_button = view.el.querySelector('.show-user-details-modal');
+        show_modal_button = view.querySelector('.show-user-details-modal');
         show_modal_button.click();
         remove_contact_button = modal.el.querySelector('button.remove-contact');
         expect(remove_contact_button === null).toBeTruthy();
@@ -43,7 +44,7 @@ describe("The User Details Modal", function () {
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         await mock.openChatBoxFor(_converse, contact_jid)
         const view = _converse.chatboxviews.get(contact_jid);
-        let show_modal_button = view.el.querySelector('.show-user-details-modal');
+        let show_modal_button = view.querySelector('.show-user-details-modal');
         show_modal_button.click();
         let modal = _converse.api.modal.get('user-details-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 2000);
@@ -61,12 +62,12 @@ describe("The User Details Modal", function () {
         expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim())
             .toBe("Sorry, there was an error while trying to remove Mercutio as a contact.");
         document.querySelector('.alert-danger  button.close').click();
-        show_modal_button = view.el.querySelector('.show-user-details-modal');
+        show_modal_button = view.querySelector('.show-user-details-modal');
         show_modal_button.click();
         modal = _converse.api.modal.get('user-details-modal');
         await u.waitUntil(() => u.isVisible(modal.el), 2000)
 
-        show_modal_button = view.el.querySelector('.show-user-details-modal');
+        show_modal_button = view.querySelector('.show-user-details-modal');
         show_modal_button.click();
         await u.waitUntil(() => u.isVisible(modal.el), 2000)
 

+ 3 - 3
spec/xss.js

@@ -294,8 +294,8 @@ describe("XSS", function () {
 
             _converse.connection._dataRecv(mock.createRequest(presence));
             const view = _converse.chatboxviews.get('lounge@montague.lit');
-            await u.waitUntil(() => view.el.querySelectorAll('li .occupant-nick').length, 500);
-            const occupants = view.el.querySelector('.occupant-list').querySelectorAll('li .occupant-nick');
+            await u.waitUntil(() => view.querySelectorAll('li .occupant-nick').length, 500);
+            const occupants = view.querySelector('.occupant-list').querySelectorAll('li .occupant-nick');
             expect(occupants.length).toBe(2);
             expect(occupants[0].textContent.trim()).toBe("&lt;img src=&quot;x&quot; onerror=&quot;alert(123)&quot;/&gt;");
             done();
@@ -314,7 +314,7 @@ describe("XSS", function () {
                 'text': subject,
                 'author': 'ralphm'
             }});
-            const text = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')?.textContent.trim());
+            const text = await u.waitUntil(() => view.querySelector('.chat-head__desc')?.textContent.trim());
             expect(text).toBe(subject);
             done();
         }));

+ 5 - 5
src/components/converse.js

@@ -1,4 +1,5 @@
-import { api, converse } from "@converse/headless/core";
+import tpl_converse from "../templates/converse.js";
+import { CustomElement } from './element.js';
 
 
 /**
@@ -8,11 +9,10 @@ import { api, converse } from "@converse/headless/core";
  * It can be inserted into the DOM before or after Converse has loaded or been
  * initialized.
  */
-class ConverseRoot extends HTMLElement {
+class ConverseRoot extends CustomElement {
 
-    async connectedCallback () {
-        await api.waitUntil('initialized');
-        converse.insertInto(this);
+    render () { // eslint-disable-line class-methods-use-this
+        return tpl_converse();
     }
 }
 

+ 4 - 2
src/converse.js

@@ -17,16 +17,17 @@ import "./plugins/chatview/index.js";       // Renders standalone chat boxes for
 import "./plugins/controlbox/index.js";     // The control box
 import "./plugins/dragresize/index.js";     // Allows chat boxes to be resized by dragging them
 import "./plugins/fullscreen.js";
+import "./plugins/headlines-view/index.js";
 import "./plugins/mam-views.js";
 import "./plugins/minimize/index.js";             // Allows chat boxes to be minimized
 import "./plugins/muc-views/index.js";      // Views related to MUC
-import "./plugins/headlines-view/index.js";
 import "./plugins/notifications/index.js";
 import "./plugins/omemo.js";
 import "./plugins/profile/index.js";
 import "./plugins/push.js";                 // XEP-0357 Push Notifications
 import "./plugins/register/index.js";       // XEP-0077 In-band registration
 import "./plugins/roomslist/index.js";      // Show currently open chat rooms
+import "./plugins/rootview/index.js";
 import "./plugins/rosterview/index.js";
 import "./plugins/singleton.js";
 /* END: Removable components */
@@ -43,17 +44,18 @@ const WHITELISTED_PLUGINS = [
     'converse-controlbox',
     'converse-dragresize',
     'converse-fullscreen',
+    'converse-headlines-view',
     'converse-mam-views',
     'converse-minimize',
     'converse-modal',
     'converse-muc-views',
-    'converse-headlines-view',
     'converse-notification',
     'converse-omemo',
     'converse-profile',
     'converse-push',
     'converse-register',
     'converse-roomslist',
+    'converse-rootview',
     'converse-rosterview',
     'converse-singleton'
 ];

+ 1 - 1
src/headless/log.js

@@ -1,4 +1,4 @@
-import { isElement } from 'lodash-es';
+import isElement from 'lodash-es/isElement';
 
 const LEVELS = {
     'debug': 0,

+ 2 - 2
src/modals/base.js

@@ -2,7 +2,7 @@ import bootstrap from "bootstrap.native";
 import log from "@converse/headless/log";
 import tpl_alert_component from "templates/alert.js";
 import { View } from '@converse/skeletor/src/view.js';
-import { _converse, api, converse } from "@converse/headless/core";
+import { api, converse } from "@converse/headless/core";
 import { render } from 'lit-html';
 
 const { sizzle } = converse.env;
@@ -46,7 +46,7 @@ const BaseModal = View.extend({
     },
 
     insertIntoDOM () {
-        const container_el = _converse.chatboxviews.el.querySelector("#converse-modals");
+        const container_el = document.querySelector("#converse-modals");
         container_el.insertAdjacentElement('beforeEnd', this.el);
     },
 

+ 1 - 1
src/plugins/bookmark-views/mixins.js

@@ -28,7 +28,7 @@ export const bookmarkableChatRoomView = {
                 'model': this.model,
                 'chatroomview': this
             });
-            const container_el = this.el.querySelector('.chatroom-body');
+            const container_el = this.querySelector('.chatroom-body');
             container_el.insertAdjacentElement('beforeend', this.bookmark_form.el);
         }
         u.showElement(this.bookmark_form.el);

+ 45 - 0
src/plugins/chatboxviews/container.js

@@ -0,0 +1,45 @@
+
+class ChatBoxViews {
+
+    constructor () {
+        this.views = {};
+    }
+
+    add (key, val) {
+        this.views[key] = val;
+    }
+
+    get (key) {
+        return this.views[key];
+    }
+
+    getAll () {
+        return Object.values(this.views);
+    }
+
+    keys () {
+        return Object.keys(this.views);
+    }
+
+    remove (key) {
+        delete this.views[key];
+    }
+
+    map (f) {
+        return Object.values(this.views).map(f);
+    }
+
+    forEach (f) {
+        return Object.values(this.views).forEach(f);
+    }
+
+    filter (f) {
+        return Object.values(this.views).filter(f);
+    }
+
+    closeAllChatBoxes () {
+        return Promise.all(Object.values(this.views).map(v => v.close({ 'name': 'closeAllChatBoxes' })));
+    }
+}
+
+export default ChatBoxViews;

+ 8 - 14
src/plugins/chatboxviews/index.js

@@ -3,23 +3,13 @@
  * @copyright 2020, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
+import './view.js';
 import '@converse/headless/plugins/chatboxes';
 import 'components/converse.js';
+import ChatBoxViews from './container.js';
 import ViewWithAvatar from 'shared/avatar.js';
-import ChatBoxViews from './view.js';
 import { _converse, api, converse } from '@converse/headless/core';
 
-function onChatBoxViewsInitialized () {
-    _converse.chatboxviews = new _converse.ChatBoxViews({
-        'model': _converse.chatboxes
-    });
-    /**
-     * Triggered once the _converse.ChatBoxViews view-colleciton has been initialized
-     * @event _converse#chatBoxViewsInitialized
-     * @example _converse.api.listen.on('chatBoxViewsInitialized', () => { ... });
-     */
-    api.trigger('chatBoxViewsInitialized');
-}
 
 function calculateViewportHeightUnit () {
     const vh = window.innerHeight * 0.01;
@@ -47,13 +37,17 @@ converse.plugins.add('converse-chatboxviews', {
         });
 
         _converse.ViewWithAvatar = ViewWithAvatar;
-        _converse.ChatBoxViews = ChatBoxViews;
+        _converse.chatboxviews = new ChatBoxViews();
 
         /************************ BEGIN Event Handlers ************************/
-        api.listen.on('chatBoxesInitialized', onChatBoxViewsInitialized);
+        api.listen.on('chatBoxesInitialized', () => {
+            _converse.chatboxes.on('destroy', m => _converse.chatboxviews.remove(m.get('jid')));
+        });
+
         api.listen.on('cleanup', () => delete _converse.chatboxviews);
         api.listen.on('clearSession', () => _converse.chatboxviews.closeAllChatBoxes());
         api.listen.on('chatBoxViewsInitialized', calculateViewportHeightUnit);
+
         window.addEventListener('resize', calculateViewportHeightUnit);
         /************************ END Event Handlers ************************/
 

+ 25 - 0
src/plugins/chatboxviews/templates/chats.js

@@ -0,0 +1,25 @@
+import { html } from 'lit-html';
+import { _converse } from '@converse/headless/core';
+
+export default () => {
+    const { chatboxes, CONTROLBOX_TYPE, CHATROOMS_TYPE } = _converse;
+    return html`
+        <converse-minimized-chats></converse-minimized-chats>
+        ${chatboxes.map(m => {
+            if (m.get('type') === CONTROLBOX_TYPE) {
+                return html`
+                    <converse-controlbox-toggle class="${!m.get('closed') ? 'hidden' : ''}"></converse-controlbox-toggle>
+                    <converse-controlbox id="controlbox" class="chatbox ${m.get('closed') ? 'hidden' : ''}"></converse-controlbox>
+                `;
+            } else if (m.get('type') === CHATROOMS_TYPE) {
+                return html`
+                    <converse-muc jid="${m.get('jid')}" class="chatbox ${(m.get('hidden') || m.get('minimized')) ? 'hidden' : ''}"></converse-muc>
+                `;
+            } else {
+                return html`
+                    <converse-chat jid="${m.get('jid')}" class="chatbox ${(m.get('hidden') || m.get('minimized')) ? 'hidden' : ''}"></converse-chat>
+                `;
+            }
+        })}
+    `;
+};

+ 22 - 58
src/plugins/chatboxviews/view.js

@@ -1,75 +1,39 @@
 import tpl_background_logo from '../../templates/background_logo.js';
-import tpl_converse from '../../templates/converse.js';
-import { Overview } from '@converse/skeletor/src/overview';
+import tpl_chats from './templates/chats.js';
+import { ElementView } from '@converse/skeletor/src/element.js';
+import { api, _converse } from '@converse/headless/core';
 import { render } from 'lit-html';
-import { result } from 'lodash-es';
-import { _converse, api, converse } from '@converse/headless/core';
 
-const u = converse.env.utils;
 
-
-const ChatBoxViews = Overview.extend({
-    _ensureElement () {
-        /* Override method from backbone.js
-         * If the #conversejs element doesn't exist, create it.
-         */
-        if (this.el) {
-            this.setElement(result(this, 'el'), false);
-        } else {
-            let el = _converse.root.querySelector('#conversejs');
-            if (el === null) {
-                el = document.createElement('div');
-                el.setAttribute('id', 'conversejs');
-                u.addClass(`theme-${api.settings.get('theme')}`, el);
-                const body = _converse.root.querySelector('body');
-                if (body) {
-                    body.appendChild(el);
-                } else {
-                    // Perhaps inside a web component?
-                    _converse.root.appendChild(el);
-                }
-            }
-            this.setElement(el, false);
-        }
-    },
+class ConverseChats extends ElementView {
 
     initialize () {
-        this.listenTo(this.model, 'destroy', this.removeChat);
+        this.model = _converse.chatboxes;
+        this.listenTo(this.model, 'destroy', this.render);
+        this.listenTo(this.model, 'add', this.render);
+        this.listenTo(this.model, 'change:hidden', this.render);
+        this.listenTo(this.model, 'change:closed', this.render);
+        this.listenTo(this.model, 'change:jid', this.render);
+
         const bg = document.getElementById('conversejs-bg');
         if (bg && !bg.innerHTML.trim()) {
             render(tpl_background_logo(), bg);
         }
         const body = document.querySelector('body');
         body.classList.add(`converse-${api.settings.get('view_mode')}`);
-        this.el.classList.add(`converse-${api.settings.get('view_mode')}`);
-        if (api.settings.get('singleton')) {
-            this.el.classList.add(`converse-singleton`);
-        }
         this.render();
-    },
 
-    render () {
-        this._ensureElement();
-        render(tpl_converse(), this.el);
-        this.row_el = this.el.querySelector('.row');
-    },
-
-    /**
-     * Add a new DOM element (likely a chat box) into the
-     * the row managed by this overview.
-     * @param { HTMLElement } el
-     */
-    insertRowColumn (el) {
-        this.row_el.insertAdjacentElement('afterBegin', el);
-    },
-
-    removeChat (item) {
-        this.remove(item.get('id'));
-    },
+        /**
+         * Triggered once the _converse.ChatBoxViews view-colleciton has been initialized
+         * @event _converse#chatBoxViewsInitialized
+         * @example _converse.api.listen.on('chatBoxViewsInitialized', () => { ... });
+         */
+        api.trigger('chatBoxViewsInitialized');
+    }
 
-    closeAllChatBoxes () {
-        return Promise.all(this.map(v => v.close({ 'name': 'closeAllChatBoxes' })));
+    render () {
+        render(tpl_chats(), this);
     }
-});
+}
 
-export default ChatBoxViews;
+api.elements.define('converse-chats', ConverseChats);

+ 0 - 21
src/plugins/chatview/index.js

@@ -14,25 +14,6 @@ import chatview_api from './api.js';
 
 const { Strophe } = converse.env;
 
-function onWindowStateChanged (data) {
-    if (_converse.chatboxviews) {
-        _converse.chatboxviews.forEach(view => {
-            if (view.model.get('id') !== 'controlbox') {
-                view.onWindowStateChanged(data.state);
-            }
-        });
-    }
-}
-
-function onChatBoxViewsInitialized () {
-    const views = _converse.chatboxviews;
-    _converse.chatboxes.on('add', async item => {
-        if (!views.get(item.get('id')) && item.get('type') === _converse.PRIVATE_CHAT_TYPE) {
-            await item.initialized;
-            views.add(item.get('id'), new _converse.ChatBoxView({ model: item }));
-        }
-    });
-}
 
 converse.plugins.add('converse-chatview', {
     /* Plugin dependencies are other plugins which might be
@@ -77,8 +58,6 @@ converse.plugins.add('converse-chatview', {
 
         _converse.ChatBoxView = ChatBoxView;
 
-        api.listen.on('chatBoxViewsInitialized', onChatBoxViewsInitialized);
-        api.listen.on('windowStateChanged', onWindowStateChanged);
         api.listen.on('connected', () => api.disco.own.features.add(Strophe.NS.SPOILER));
     }
 });

+ 71 - 455
src/plugins/chatview/view.js

@@ -1,12 +1,10 @@
+import BaseChatView from 'shared/chatview.js';
 import UserDetailsModal from 'modals/user-details.js';
 import log from '@converse/headless/log';
 import tpl_chatbox from 'templates/chatbox.js';
 import tpl_chatbox_head from 'templates/chatbox_head.js';
-import tpl_chatbox_message_form from 'templates/chatbox_message_form.js';
 import tpl_spinner from 'templates/spinner.js';
-import tpl_toolbar from 'templates/toolbar.js';
-import { View } from '@converse/skeletor/src/view.js';
-import { __ } from '../../i18n';
+import { __ } from 'i18n';
 import { _converse, api, converse } from '@converse/headless/core';
 import { debounce } from 'lodash-es';
 import { html, render } from 'lit-html';
@@ -20,12 +18,12 @@ const { dayjs } = converse.env;
  * @namespace _converse.ChatBoxView
  * @memberOf _converse
  */
-const ChatBoxView = View.extend({
-    length: 200,
-    className: 'chatbox hidden',
-    is_chatroom: false, // Leaky abstraction from MUC
+export default class ChatView extends BaseChatView {
+    length = 200
+    className = 'chatbox hidden'
+    is_chatroom = false // Leaky abstraction from MUC
 
-    events: {
+    events = {
         'click .chatbox-navback': 'showControlBox',
         'click .new-msgs-indicator': 'viewUnreadMessages',
         'click .send-button': 'onFormSubmitted',
@@ -34,15 +32,20 @@ const ChatBoxView = View.extend({
         'keydown .chat-textarea': 'onKeyDown',
         'keyup .chat-textarea': 'onKeyUp',
         'paste .chat-textarea': 'onPaste'
-    },
+    }
 
     async initialize () {
+        const jid = this.getAttribute('jid');
+        _converse.chatboxviews.add(jid, this);
+
+        this.model = _converse.chatboxes.get(jid);
         this.initDebounced();
 
+        api.listen.on('windowStateChanged', this.onWindowStateChanged);
+
         this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
-        this.listenTo(this.model, 'change:hidden', m => (m.get('hidden') ? this.hide() : this.show()));
+        this.listenTo(this.model, 'change:hidden', m => (!m.get('hidden') && this.show()));
         this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
-        this.listenTo(this.model, 'destroy', this.remove);
         this.listenTo(this.model, 'show', this.show);
         this.listenTo(this.model, 'vcard:change', this.renderHeading);
 
@@ -68,7 +71,6 @@ const ChatBoxView = View.extend({
         this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
 
         await this.model.messages.fetched;
-        this.insertIntoDOM();
         this.model.maybeShow();
         this.scrollDown();
         /**
@@ -78,7 +80,7 @@ const ChatBoxView = View.extend({
          * @example _converse.api.listen.on('chatBoxViewInitialized', view => { ... });
          */
         api.trigger('chatBoxViewInitialized', this);
-    },
+    }
 
     initDebounced () {
         this.markScrolled = debounce(this._markScrolled, 100);
@@ -93,20 +95,20 @@ const ChatBoxView = View.extend({
             this.renderChatHistory = () => this.renderChatContent(false);
             this.renderNotifications = () => this.renderChatContent(true);
         }
-    },
+    }
 
     render () {
         const result = tpl_chatbox(Object.assign(this.model.toJSON(), { 'markScrolled': ev => this.markScrolled(ev) }));
-        render(result, this.el);
-        this.content = this.el.querySelector('.chat-content');
-        this.notifications = this.el.querySelector('.chat-content__notifications');
-        this.msgs_container = this.el.querySelector('.chat-content__messages');
-        this.help_container = this.el.querySelector('.chat-content__help');
+        render(result, this);
+        this.content = this.querySelector('.chat-content');
+        this.notifications = this.querySelector('.chat-content__notifications');
+        this.msgs_container = this.querySelector('.chat-content__messages');
+        this.help_container = this.querySelector('.chat-content__help');
         this.renderChatContent();
         this.renderMessageForm();
         this.renderHeading();
         return this;
-    },
+    }
 
     onMessageAdded (message) {
         this.renderChatHistory();
@@ -122,7 +124,7 @@ const ChatBoxView = View.extend({
                 this.showNewMessagesIndicator();
             }
         }
-    },
+    }
 
     getNotifications () {
         if (this.model.notifications.get('chat_state') === _converse.COMPOSING) {
@@ -134,16 +136,16 @@ const ChatBoxView = View.extend({
         } else {
             return '';
         }
-    },
+    }
 
-    getHelpMessages () {
+    getHelpMessages () { // eslint-disable-line class-methods-use-this
         return [
             `<strong>/clear</strong>: ${__('Remove messages')}`,
             `<strong>/close</strong>: ${__('Close this chat')}`,
             `<strong>/me</strong>: ${__('Write in the third person')}`,
             `<strong>/help</strong>: ${__('Show this menu')}`
         ];
-    },
+    }
 
     renderHelpMessages () {
         render(
@@ -159,80 +161,22 @@ const ChatBoxView = View.extend({
 
             this.help_container
         );
-    },
-
-    renderChatContent (msgs_by_ref = false) {
-        if (!this.tpl_chat_content) {
-            this.tpl_chat_content = o => {
-                return html`
-                    <converse-chat-content .chatview=${this} .messages=${o.messages} notifications=${o.notifications}>
-                    </converse-chat-content>
-                `;
-            };
-        }
-        const msg_models = this.model.messages.models;
-        const messages = msgs_by_ref ? msg_models : Array.from(msg_models);
-        render(this.tpl_chat_content({ messages, 'notifications': this.getNotifications() }), this.msgs_container);
-    },
-
-    renderToolbar () {
-        if (!api.settings.get('show_toolbar')) {
-            return this;
-        }
-        const options = Object.assign(
-            {
-                'model': this.model,
-                'chatview': this
-            },
-            this.model.toJSON(),
-            this.getToolbarOptions()
-        );
-        render(tpl_toolbar(options), this.el.querySelector('.chat-toolbar'));
-        /**
-         * Triggered once the _converse.ChatBoxView's toolbar has been rendered
-         * @event _converse#renderToolbar
-         * @type { _converse.ChatBoxView }
-         * @example _converse.api.listen.on('renderToolbar', view => { ... });
-         */
-        api.trigger('renderToolbar', this);
-        return this;
-    },
-
-    renderMessageForm () {
-        const form_container = this.el.querySelector('.message-form-container');
-        render(
-            tpl_chatbox_message_form(
-                Object.assign(this.model.toJSON(), {
-                    'hint_value': this.el.querySelector('.spoiler-hint')?.value,
-                    'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'),
-                    'label_spoiler_hint': __('Optional hint'),
-                    'message_value': this.el.querySelector('.chat-textarea')?.value,
-                    'show_send_button': api.settings.get('show_send_button'),
-                    'show_toolbar': api.settings.get('show_toolbar'),
-                    'unread_msgs': __('You have unread messages')
-                })
-            ),
-            form_container
-        );
-        this.el.addEventListener('focusin', ev => this.emitFocused(ev));
-        this.el.addEventListener('focusout', ev => this.emitBlurred(ev));
-        this.renderToolbar();
-    },
+    }
 
     showControlBox () {
         // Used in mobile view, to navigate back to the controlbox
         _converse.chatboxviews.get('controlbox')?.show();
         this.hide();
-    },
+    }
 
     showUserDetailsModal (ev) {
         ev.preventDefault();
         api.modal.show(UserDetailsModal, { model: this.model }, ev);
-    },
+    }
 
-    onDragOver (evt) {
+    onDragOver (evt) { // eslint-disable-line class-methods-use-this
         evt.preventDefault();
-    },
+    }
 
     onDrop (evt) {
         if (evt.dataTransfer.files.length == 0) {
@@ -242,33 +186,12 @@ const ChatBoxView = View.extend({
         }
         evt.preventDefault();
         this.model.sendFiles(evt.dataTransfer.files);
-    },
+    }
 
     async renderHeading () {
         const tpl = await this.generateHeadingTemplate();
-        render(tpl, this.el.querySelector('.chat-head-chatbox'));
-    },
-
-    async getHeadingStandaloneButton (promise_or_data) {
-        const data = await promise_or_data;
-        return html`
-            <a
-                href="#"
-                class="chatbox-btn ${data.a_class} fa ${data.icon_class}"
-                @click=${data.handler}
-                title="${data.i18n_title}"
-            ></a>
-        `;
-    },
-
-    async getHeadingDropdownItem (promise_or_data) {
-        const data = await promise_or_data;
-        return html`
-            <a href="#" class="dropdown-item ${data.a_class}" @click=${data.handler} title="${data.i18n_title}"
-                ><i class="fa ${data.icon_class}"></i>${data.i18n_text}</a
-            >
-        `;
-    },
+        render(tpl, this.querySelector('.chat-head-chatbox'));
+    }
 
     async generateHeadingTemplate () {
         const vcard = this.model?.vcard;
@@ -295,7 +218,7 @@ const ChatBoxView = View.extend({
                 'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b))
             })
         );
-    },
+    }
 
     /**
      * Returns a list of objects which represent buttons for the chat's header.
@@ -344,12 +267,12 @@ const ChatBoxView = View.extend({
          *  });
          */
         return _converse.api.hook('getHeadingButtons', this, buttons);
-    },
+    }
 
-    getToolbarOptions () {
+    getToolbarOptions () { // eslint-disable-line class-methods-use-this
         //  FIXME: can this be removed?
         return {};
-    },
+    }
 
     /**
      * Scrolls the chat down, *if* appropriate.
@@ -365,32 +288,7 @@ const ChatBoxView = View.extend({
         if ((new_own_msg || !this.model.get('scrolled')) && !this.model.isHidden()) {
             this.debouncedScrollDown();
         }
-    },
-
-    /**
-     * Scrolls the chat down.
-     *
-     * This method will always scroll the chat down, regardless of
-     * whether the user scrolled up manually or not.
-     * @param { Event } [ev] - An optional event that is the cause for needing to scroll down.
-     */
-    scrollDown (ev) {
-        ev?.preventDefault?.();
-        ev?.stopPropagation?.();
-        if (this.model.get('scrolled')) {
-            u.safeSave(this.model, {
-                'scrolled': false,
-                'scrollTop': null
-            });
-        }
-        if (this.msgs_container.scrollTo) {
-            const behavior = this.msgs_container.scrollTop ? 'smooth' : 'auto';
-            this.msgs_container.scrollTo({ 'top': this.msgs_container.scrollHeight, behavior });
-        } else {
-            this.msgs_container.scrollTop = this.msgs_container.scrollHeight;
-        }
-        this.onScrolledDown();
-    },
+    }
 
     /**
      * Scroll to the previously saved scrollTop position, or scroll
@@ -403,22 +301,10 @@ const ChatBoxView = View.extend({
         } else {
             this.scrollDown();
         }
-    },
-
-    insertIntoDOM () {
-        _converse.chatboxviews.insertRowColumn(this.el);
-        /**
-         * Triggered once the _converse.ChatBoxView has been inserted into the DOM
-         * @event _converse#chatBoxInsertedIntoDOM
-         * @type { _converse.ChatBoxView | _converse.HeadlinesBoxView }
-         * @example _converse.api.listen.on('chatBoxInsertedIntoDOM', view => { ... });
-         */
-        api.trigger('chatBoxInsertedIntoDOM', this);
-        return this;
-    },
+    }
 
     addSpinner (append = false) {
-        if (this.el.querySelector('.spinner') === null) {
+        if (this.querySelector('.spinner') === null) {
             const el = u.getElementFromTemplateResult(tpl_spinner());
             if (append) {
                 this.content.insertAdjacentElement('beforeend', el);
@@ -427,11 +313,11 @@ const ChatBoxView = View.extend({
                 this.content.insertAdjacentElement('afterbegin', el);
             }
         }
-    },
+    }
 
     clearSpinner () {
         this.content.querySelectorAll('.spinner').forEach(u.removeElement);
-    },
+    }
 
     onStatusMessageChanged (item) {
         this.renderHeading();
@@ -447,7 +333,7 @@ const ChatBoxView = View.extend({
             'contact': item.attributes,
             'message': item.get('status')
         });
-    },
+    }
 
     /**
      * Given a message element, determine wether it should be
@@ -463,7 +349,7 @@ const ChatBoxView = View.extend({
      * @method _converse.ChatBoxView#markFollowups
      * @param { HTMLElement } el - The message element
      */
-    markFollowups (el) {
+    markFollowups (el) { // eslint-disable-line class-methods-use-this
         const from = el.getAttribute('data-from');
         const previous_el = el.previousElementSibling;
         const date = dayjs(el.getAttribute('data-isodate'));
@@ -495,7 +381,7 @@ const ChatBoxView = View.extend({
         } else {
             u.removeClass('chat-msg--followup', next_el);
         }
-    },
+    }
 
     parseMessageForCommands (text) {
         const match = text.replace(/^\s*/, '').match(/^\/(.*)\s*$/);
@@ -511,82 +397,7 @@ const ChatBoxView = View.extend({
                 return true;
             }
         }
-    },
-
-    async onFormSubmitted (ev) {
-        ev.preventDefault();
-        const textarea = this.el.querySelector('.chat-textarea');
-        const message_text = textarea.value.trim();
-        if (
-            (api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit')) ||
-            !message_text.replace(/\s/g, '').length
-        ) {
-            return;
-        }
-        if (!_converse.connection.authenticated) {
-            const err_msg = __('Sorry, the connection has been lost, and your message could not be sent');
-            api.alert('error', __('Error'), err_msg);
-            api.connection.reconnect();
-            return;
-        }
-        let spoiler_hint,
-            hint_el = {};
-        if (this.model.get('composing_spoiler')) {
-            hint_el = this.el.querySelector('form.sendXMPPMessage input.spoiler-hint');
-            spoiler_hint = hint_el.value;
-        }
-        u.addClass('disabled', textarea);
-        textarea.setAttribute('disabled', 'disabled');
-        this.el.querySelector('converse-emoji-dropdown')?.hideMenu();
-
-        const is_command = this.parseMessageForCommands(message_text);
-        const message = is_command ? null : await this.model.sendMessage(message_text, spoiler_hint);
-        if (is_command || message) {
-            hint_el.value = '';
-            textarea.value = '';
-            u.removeClass('correcting', textarea);
-            textarea.style.height = 'auto';
-            this.updateCharCounter(textarea.value);
-        }
-        if (message) {
-            /**
-             * Triggered whenever a message is sent by the user
-             * @event _converse#messageSend
-             * @type { _converse.Message }
-             * @example _converse.api.listen.on('messageSend', message => { ... });
-             */
-            api.trigger('messageSend', message);
-        }
-        if (api.settings.get('view_mode') === 'overlayed') {
-            // XXX: Chrome flexbug workaround. The .chat-content area
-            // doesn't resize when the textarea is resized to its original size.
-            this.msgs_container.parentElement.style.display = 'none';
-        }
-        textarea.removeAttribute('disabled');
-        u.removeClass('disabled', textarea);
-
-        if (api.settings.get('view_mode') === 'overlayed') {
-            // XXX: Chrome flexbug workaround.
-            this.msgs_container.parentElement.style.display = '';
-        }
-        // Suppress events, otherwise superfluous CSN gets set
-        // immediately after the message, causing rate-limiting issues.
-        this.model.setChatState(_converse.ACTIVE, { 'silent': true });
-        textarea.focus();
-    },
-
-    updateCharCounter (chars) {
-        if (api.settings.get('message_limit')) {
-            const message_limit = this.el.querySelector('.message-limit');
-            const counter = api.settings.get('message_limit') - chars.length;
-            message_limit.textContent = counter;
-            if (counter < 1) {
-                u.addClass('error', message_limit);
-            } else {
-                u.removeClass('error', message_limit);
-            }
-        }
-    },
+    }
 
     onPaste (ev) {
         if (ev.clipboardData.files.length !== 0) {
@@ -599,28 +410,7 @@ const ChatBoxView = View.extend({
             return;
         }
         this.updateCharCounter(ev.clipboardData.getData('text/plain'));
-    },
-
-    autocompleteInPicker (input, value) {
-        const emoji_dropdown = this.el.querySelector('converse-emoji-dropdown');
-        const emoji_picker = this.el.querySelector('converse-emoji-picker');
-        if (emoji_picker && emoji_dropdown) {
-            emoji_picker.model.set({
-                'ac_position': input.selectionStart,
-                'autocompleting': value,
-                'query': value
-            });
-            emoji_dropdown.showMenu();
-            return true;
-        }
-    },
-
-    onEmojiReceivedFromPicker (emoji) {
-        const model = this.el.querySelector('converse-emoji-picker').model;
-        const autocompleting = model.get('autocompleting');
-        const ac_position = model.get('ac_position');
-        this.insertIntoTextArea(emoji, autocompleting, false, ac_position);
-    },
+    }
 
     /**
      * Event handler for when a depressed key goes up
@@ -629,7 +419,7 @@ const ChatBoxView = View.extend({
      */
     onKeyUp (ev) {
         this.updateCharCounter(ev.target.value);
-    },
+    }
 
     /**
      * Event handler for when a key is pressed down in a chat box textarea.
@@ -657,14 +447,14 @@ const ChatBoxView = View.extend({
             } else if (ev.keyCode === converse.keycodes.ENTER) {
                 return this.onEnterPressed(ev);
             } else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
-                const textarea = this.el.querySelector('.chat-textarea');
+                const textarea = this.querySelector('.chat-textarea');
                 if (!textarea.value || u.hasClass('correcting', textarea)) {
                     return this.editEarlierMessage();
                 }
             } else if (
                 ev.keyCode === converse.keycodes.DOWN_ARROW &&
                 ev.target.selectionEnd === ev.target.value.length &&
-                u.hasClass('correcting', this.el.querySelector('.chat-textarea'))
+                u.hasClass('correcting', this.querySelector('.chat-textarea'))
             ) {
                 return this.editLaterMessage();
             }
@@ -685,15 +475,11 @@ const ChatBoxView = View.extend({
             // (which would imply an internal command and not a message).
             this.model.setChatState(_converse.COMPOSING);
         }
-    },
+    }
 
     getOwnMessages () {
         return this.model.messages.filter({ 'sender': 'me' });
-    },
-
-    onEnterPressed (ev) {
-        return this.onFormSubmitted(ev);
-    },
+    }
 
     onEscapePressed (ev) {
         ev.preventDefault();
@@ -703,7 +489,7 @@ const ChatBoxView = View.extend({
             message.save('correcting', false);
         }
         this.insertIntoTextArea('', true, false);
-    },
+    }
 
     async onMessageRetractButtonClicked (message) {
         if (message.get('sender') !== 'me') {
@@ -723,11 +509,11 @@ const ChatBoxView = View.extend({
         if (result) {
             this.model.retractOwnMessage(message);
         }
-    },
+    }
 
     onMessageEditButtonClicked (message) {
         const currently_correcting = this.model.messages.findWhere('correcting');
-        const unsent_text = this.el.querySelector('.chat-textarea')?.value;
+        const unsent_text = this.querySelector('.chat-textarea')?.value;
         if (unsent_text && (!currently_correcting || currently_correcting.get('message') !== unsent_text)) {
             if (!confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'))) {
                 return;
@@ -742,7 +528,7 @@ const ChatBoxView = View.extend({
             message.save('correcting', false);
             this.insertIntoTextArea('', true, false);
         }
-    },
+    }
 
     editLaterMessage () {
         let message;
@@ -764,7 +550,7 @@ const ChatBoxView = View.extend({
         } else {
             this.insertIntoTextArea('', true, false);
         }
-    },
+    }
 
     editEarlierMessage () {
         let message;
@@ -789,15 +575,15 @@ const ChatBoxView = View.extend({
             this.insertIntoTextArea(u.prefixMentions(message), true, true);
             message.save('correcting', true);
         }
-    },
+    }
 
-    inputChanged (ev) {
+    inputChanged (ev) { // eslint-disable-line class-methods-use-this
         const height = ev.target.scrollHeight + 'px';
         if (ev.target.style.height != height) {
             ev.target.style.height = 'auto';
             ev.target.style.height = height;
         }
-    },
+    }
 
     async clearMessages (ev) {
         if (ev && ev.preventDefault) {
@@ -808,52 +594,14 @@ const ChatBoxView = View.extend({
             await this.model.clearMessages();
         }
         return this;
-    },
-
-    /**
-     * Insert a particular string value into the textarea of this chat box.
-     * @private
-     * @method _converse.ChatBoxView#insertIntoTextArea
-     * @param {string} value - The value to be inserted.
-     * @param {(boolean|string)} [replace] - Whether an existing value
-     *  should be replaced. If set to `true`, the entire textarea will
-     *  be replaced with the new value. If set to a string, then only
-     *  that string will be replaced *if* a position is also specified.
-     * @param {integer} [position] - The end index of the string to be
-     * replaced with the new value.
-     */
-    insertIntoTextArea (value, replace = false, correcting = false, position) {
-        const textarea = this.el.querySelector('.chat-textarea');
-        if (correcting) {
-            u.addClass('correcting', textarea);
-        } else {
-            u.removeClass('correcting', textarea);
-        }
-        if (replace) {
-            if (position && typeof replace == 'string') {
-                textarea.value = textarea.value.replace(new RegExp(replace, 'g'), (match, offset) =>
-                    offset == position - replace.length ? value + ' ' : match
-                );
-            } else {
-                textarea.value = value;
-            }
-        } else {
-            let existing = textarea.value;
-            if (existing && existing[existing.length - 1] !== ' ') {
-                existing = existing + ' ';
-            }
-            textarea.value = existing + value + ' ';
-        }
-        this.updateCharCounter(textarea.value);
-        u.placeCaretAtEnd(textarea);
-    },
+    }
 
     onPresenceChanged (item) {
         const show = item.get('show');
         const fullname = this.model.getDisplayName();
 
         let text;
-        if (u.isVisible(this.el)) {
+        if (u.isVisible(this)) {
             if (show === 'offline') {
                 text = __('%1$s has gone offline', fullname);
             } else if (show === 'away') {
@@ -865,7 +613,7 @@ const ChatBoxView = View.extend({
             }
             text && this.model.createMessage({ 'message': text, 'type': 'info' });
         }
-    },
+    }
 
     async close (ev) {
         if (ev && ev.preventDefault) {
@@ -881,7 +629,6 @@ const ChatBoxView = View.extend({
             this.model.sendChatState();
         }
         await this.model.close(ev);
-        this.remove();
         /**
          * Triggered once a chatbox has been closed.
          * @event _converse#chatBoxClosed
@@ -890,154 +637,23 @@ const ChatBoxView = View.extend({
          */
         api.trigger('chatBoxClosed', this);
         return this;
-    },
-
-    emitBlurred (ev) {
-        if (this.el.contains(document.activeElement) || this.el.contains(ev.relatedTarget)) {
-            // Something else in this chatbox is still focused
-            return;
-        }
-        /**
-         * Triggered when the focus has been removed from a particular chat.
-         * @event _converse#chatBoxBlurred
-         * @type { _converse.ChatBoxView | _converse.ChatRoomView }
-         * @example _converse.api.listen.on('chatBoxBlurred', (view, event) => { ... });
-         */
-        api.trigger('chatBoxBlurred', this, ev);
-    },
-
-    emitFocused (ev) {
-        if (this.el.contains(ev.relatedTarget)) {
-            // Something else in this chatbox was already focused
-            return;
-        }
-        /**
-         * Triggered when the focus has been moved to a particular chat.
-         * @event _converse#chatBoxFocused
-         * @type { _converse.ChatBoxView | _converse.ChatRoomView }
-         * @example _converse.api.listen.on('chatBoxFocused', (view, event) => { ... });
-         */
-        api.trigger('chatBoxFocused', this, ev);
-    },
-
-    focus () {
-        const textarea_el = this.el.getElementsByClassName('chat-textarea')[0];
-        if (textarea_el && document.activeElement !== textarea_el) {
-            textarea_el.focus();
-        }
-        return this;
-    },
-
-    maybeFocus () {
-        api.settings.get('auto_focus') && this.focus();
-    },
-
-    hide () {
-        this.el.classList.add('hidden');
-        return this;
-    },
+    }
 
     afterShown () {
         this.model.clearUnreadMsgCounter();
         this.model.setChatState(_converse.ACTIVE);
         this.scrollDown();
         this.maybeFocus();
-    },
-
-    show () {
-        if (this.model.get('hidden')) {
-            log.debug(`Not showing chat ${this.model.get('jid')} because it's set as hidden`);
-            return;
-        }
-        if (u.isVisible(this.el)) {
-            this.maybeFocus();
-            return;
-        }
-        if (api.settings.get('animate')) {
-            u.fadeIn(this.el, () => this.afterShown());
-        } else {
-            u.showElement(this.el);
-            this.afterShown();
-        }
-    },
+    }
 
     showNewMessagesIndicator () {
-        u.showElement(this.el.querySelector('.new-msgs-indicator'));
-    },
-
-    hideNewMessagesIndicator () {
-        const new_msgs_indicator = this.el.querySelector('.new-msgs-indicator');
-        if (new_msgs_indicator !== null) {
-            new_msgs_indicator.classList.add('hidden');
-        }
-    },
-
-    /**
-     * Called when the chat content is scrolled up or down.
-     * We want to record when the user has scrolled away from
-     * the bottom, so that we don't automatically scroll away
-     * from what the user is reading when new messages are received.
-     *
-     * Don't call this method directly, instead, call `markScrolled`,
-     * which debounces this method by 100ms.
-     * @private
-     */
-    _markScrolled: function (ev) {
-        let scrolled = true;
-        let scrollTop = null;
-        const is_at_bottom =
-            this.msgs_container.scrollTop + this.msgs_container.clientHeight >= this.msgs_container.scrollHeight - 62; // sigh...
-
-        if (is_at_bottom) {
-            scrolled = false;
-            this.onScrolledDown();
-        } else if (this.msgs_container.scrollTop === 0) {
-            /**
-             * Triggered once the chat's message area has been scrolled to the top
-             * @event _converse#chatBoxScrolledUp
-             * @property { _converse.ChatBoxView | _converse.ChatRoomView } view
-             * @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
-             */
-            api.trigger('chatBoxScrolledUp', this);
-        } else {
-            scrollTop = ev.target.scrollTop;
-        }
-        u.safeSave(this.model, { scrolled, scrollTop });
-    },
+        u.showElement(this.querySelector('.new-msgs-indicator'));
+    }
 
     viewUnreadMessages () {
         this.model.save({ 'scrolled': false, 'scrollTop': null });
         this.scrollDown();
-    },
-
-    onScrolledDown () {
-        this.hideNewMessagesIndicator();
-        if (!this.model.isHidden()) {
-            this.model.clearUnreadMsgCounter();
-            // Clear location hash if set to one of the messages in our history
-            const hash = window.location.hash;
-            hash && this.model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
-        }
-        /**
-         * Triggered once the chat's message area has been scrolled down to the bottom.
-         * @event _converse#chatBoxScrolledDown
-         * @type {object}
-         * @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
-         * @example _converse.api.listen.on('chatBoxScrolledDown', obj => { ... });
-         */
-        api.trigger('chatBoxScrolledDown', { 'chatbox': this.model }); // TODO: clean up
-    },
-
-    onWindowStateChanged (state) {
-        if (state === 'visible') {
-            if (!this.model.isHidden() && this.model.get('num_unread', 0)) {
-                this.model.clearUnreadMsgCounter();
-            }
-        } else if (state === 'hidden') {
-            this.model.setChatState(_converse.INACTIVE, { 'silent': true });
-            this.model.sendChatState();
-        }
     }
-});
+}
 
-export default ChatBoxView;
+api.elements.define('converse-chat', ChatView);

+ 2 - 20
src/plugins/controlbox/index.js

@@ -8,7 +8,7 @@ import "../chatview/index.js";
 import ControlBoxMixin from './model.js';
 import ControlBoxPane from './pane.js';
 import ControlBoxToggle from './toggle.js';
-import ControlBoxViewMixin from './view.js';
+import ControlBoxView from './view.js';
 import log from '@converse/headless/log';
 import { LoginPanelModel, LoginPanel } from './loginpanel.js';
 import { _converse, api, converse } from '@converse/headless/core';
@@ -17,20 +17,6 @@ import controlbox_api from './api.js';
 
 const u = converse.env.utils;
 
-function onChatBoxViewsInitialized () {
-    _converse.chatboxes.on('add', item => {
-        if (item.get('type') === _converse.CONTROLBOX_TYPE) {
-            const views = _converse.chatboxviews;
-            const view = views.get(item.get('id'));
-            if (view) {
-                view.model = item;
-                view.initialize();
-            } else {
-                views.add(item.get('id'), new _converse.ControlBoxView({ model: item }));
-            }
-        }
-    });
-}
 
 function disconnect () {
     /* Upon disconnection, set connected to `false`, so that if
@@ -96,9 +82,6 @@ converse.plugins.add('converse-controlbox', {
     },
 
     initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
         api.settings.extend({
             allow_logout: true,
             allow_user_trust_override: true,
@@ -111,15 +94,14 @@ converse.plugins.add('converse-controlbox', {
         api.promises.add('controlBoxInitialized');
         Object.assign(api, controlbox_api);
 
+        _converse.ControlBoxView = ControlBoxView;
         _converse.ControlBox = _converse.ChatBox.extend(ControlBoxMixin);
-        _converse.ControlBoxView = _converse.ChatBoxView.extend(ControlBoxViewMixin);
         _converse.LoginPanelModel = LoginPanelModel;
         _converse.LoginPanel = LoginPanel;
         _converse.ControlBoxPane = ControlBoxPane;
         _converse.ControlBoxToggle = ControlBoxToggle;
 
         /******************** Event Handlers ********************/
-        api.listen.on('chatBoxViewsInitialized', onChatBoxViewsInitialized);
         api.listen.on('chatBoxesFetched', onChatBoxesFetched);
         api.listen.on('cleanup', () => delete _converse.controlboxtoggle);
         api.listen.on('clearSession', clearSession);

+ 6 - 2
src/plugins/controlbox/templates/controlbox.js

@@ -5,5 +5,9 @@ export default (o) => html`
         <div class="chat-head controlbox-head">
             ${o.sticky_controlbox ? '' : html`<a class="chatbox-btn close-chatbox-button fa fa-times"></a>` }
         </div>
-        <div class="controlbox-panes"></div>
-    </div>`;
+        <div class="controlbox-panes">
+            <converse-headlines-panel></converse-headlines-panel>
+            <converse-rooms-list></converse-rooms-list>
+        </div>
+    </div>
+`;

+ 3 - 3
src/plugins/controlbox/templates/toggle.js

@@ -1,8 +1,8 @@
-import { html } from "lit-html";
-import { api } from "@converse/headless/core";
 import { __ } from 'i18n';
+import { api } from "@converse/headless/core";
+import { html } from "lit-html";
 
 export default  () => {
     const i18n_toggle = api.connection.connected() ? __('Chat Contacts') : __('Toggle chat');
-    return html`<span class="toggle-feedback">${i18n_toggle}</span>`;
+    return html`<a id="toggle-controlbox" class="toggle-controlbox"><span class="toggle-feedback">${i18n_toggle}</span></a>`;
 }

+ 21 - 28
src/plugins/controlbox/toggle.js

@@ -1,6 +1,5 @@
-import log from "@converse/headless/log";
 import tpl_controlbox_toggle from "./templates/toggle.js";
-import { View } from "@converse/skeletor/src/view";
+import { ElementView } from '@converse/skeletor/src/element.js';
 import { _converse, api, converse } from "@converse/headless/core";
 import { addControlBox } from './utils.js';
 import { render } from 'lit-html';
@@ -8,47 +7,39 @@ import { render } from 'lit-html';
 const u = converse.env.utils;
 
 
-const ControlBoxToggle = View.extend({
-    tagName: 'a',
-    className: 'toggle-controlbox hidden',
-    id: 'toggle-controlbox',
-    events: {
+class ControlBoxToggle extends ElementView {
+    events = {
         'click': 'onClick'
-    },
-    attributes: {
-        'href': "#"
-    },
+    }
 
-    initialize () {
-        _converse.chatboxviews.insertRowColumn(this.render().el);
-        api.waitUntil('initialized')
-            .then(this.render.bind(this))
-            .catch(e => log.fatal(e));
-    },
+    async initialize () {
+        await api.waitUntil('initialized');
+        this.render();
+    }
 
     render () {
         // We let the render method of ControlBoxView decide whether
         // the ControlBox or the Toggle must be shown. This prevents
         // artifacts (i.e. on page load the toggle is shown only to then
         // seconds later be hidden in favor of the controlbox).
-        render(tpl_controlbox_toggle(), this.el);
+        render(tpl_controlbox_toggle(), this);
         return this;
-    },
+    }
 
     hide (callback) {
-        if (u.isVisible(this.el)) {
-            u.hideElement(this.el);
+        if (u.isVisible(this)) {
+            u.hideElement(this);
             callback();
         }
-    },
+    }
 
     show (callback) {
-        if (!u.isVisible(this.el)) {
-            u.fadeIn(this.el, callback);
+        if (!u.isVisible(this)) {
+            u.fadeIn(this, callback);
         }
-    },
+    }
 
-    showControlBox () {
+    showControlBox () { // eslint-disable-line class-methods-use-this
         let controlbox = _converse.chatboxes.get('controlbox');
         if (!controlbox) {
             controlbox = addControlBox();
@@ -58,7 +49,7 @@ const ControlBoxToggle = View.extend({
         } else {
             controlbox.trigger('show');
         }
-    },
+    }
 
     onClick (e) {
         e.preventDefault();
@@ -73,6 +64,8 @@ const ControlBoxToggle = View.extend({
             this.showControlBox();
         }
     }
-});
+}
+
+api.elements.define('converse-controlbox-toggle', ControlBoxToggle);
 
 export default ControlBoxToggle;

+ 30 - 51
src/plugins/controlbox/view.js

@@ -1,38 +1,27 @@
 import tpl_controlbox from './templates/controlbox.js';
-import { render } from 'lit-html';
+import { ElementView } from '@converse/skeletor/src/element.js';
 import { _converse, api, converse } from '@converse/headless/core';
+import { render } from 'lit-html';
 
 const u = converse.env.utils;
 
 /**
- * Mixin which turns a ChatBoxView into a ControlBoxView.
- *
  * The ControlBox is the section of the chat that contains the open groupchats,
  * bookmarks and roster.
  *
  * In `overlayed` `view_mode` it's a box like the chat boxes, in `fullscreen`
  * `view_mode` it's a left-aligned sidebar.
- * @mixin
  */
-const ControlBoxViewMixin = {
-    tagName: 'div',
-    className: 'chatbox',
-    id: 'controlbox',
-    events: {
+class ControlBoxView extends ElementView {
+    events = {
         'click a.close-chatbox-button': 'close'
-    },
+    }
 
     initialize () {
-        if (_converse.controlboxtoggle === undefined) {
-            _converse.controlboxtoggle = new _converse.ControlBoxToggle();
-        }
-        _converse.controlboxtoggle.el.insertAdjacentElement('afterend', this.el);
-
+        this.model = _converse.chatboxes.get(this.getAttribute('id'));
         this.listenTo(this.model, 'change:connected', this.onConnected);
-        this.listenTo(this.model, 'destroy', this.hide);
-        this.listenTo(this.model, 'hide', this.hide);
+        // this.listenTo(this.model, 'hide', this.hide);
         this.listenTo(this.model, 'show', this.show);
-        this.listenTo(this.model, 'change:closed', this.ensureClosedState);
         this.render();
         /**
          * Triggered when the _converse.ControlBoxView has been initialized and therefore
@@ -43,9 +32,11 @@ const ControlBoxViewMixin = {
          * @example _converse.api.listen.on('controlBoxInitialized', view => { ... });
          */
         api.trigger('controlBoxInitialized', this);
-    },
+    }
 
     render () {
+        _converse.chatboxviews.add('controlbox', this);
+
         if (this.model.get('connected')) {
             if (this.model.get('closed') === undefined) {
                 this.model.set('closed', !api.settings.get('show_controlbox_by_default'));
@@ -56,13 +47,7 @@ const ControlBoxViewMixin = {
             'sticky_controlbox': api.settings.get('sticky_controlbox'),
             ...this.model.toJSON()
         });
-        render(tpl_result, this.el);
-
-        if (!this.model.get('closed')) {
-            this.show();
-        } else {
-            this.hide();
-        }
+        render(tpl_result, this);
 
         const connection = _converse?.connection || {};
         if (!connection.connected || !connection.authenticated || connection.disconnecting) {
@@ -71,29 +56,29 @@ const ControlBoxViewMixin = {
             this.renderControlBoxPane();
         }
         return this;
-    },
+    }
 
     onConnected () {
         if (this.model.get('connected')) {
             this.render();
         }
-    },
+    }
 
     renderLoginPanel () {
-        this.el.classList.add('logged-out');
+        this.classList.add('logged-out');
         if (this.loginpanel) {
             this.loginpanel.render();
         } else {
             this.loginpanel = new _converse.LoginPanel({
                 'model': new _converse.LoginPanelModel()
             });
-            const panes = this.el.querySelector('.controlbox-panes');
+            const panes = this.querySelector('.controlbox-panes');
             panes.innerHTML = '';
             panes.appendChild(this.loginpanel.render().el);
         }
         this.loginpanel.initPopovers();
         return this;
-    },
+    }
 
     /**
      * Renders the "Contacts" panel of the controlbox.
@@ -109,12 +94,12 @@ const ControlBoxViewMixin = {
         if (this.controlbox_pane && u.isVisible(this.controlbox_pane.el)) {
             return;
         }
-        this.el.classList.remove('logged-out');
+        this.classList.remove('logged-out');
         this.controlbox_pane = new _converse.ControlBoxPane();
-        this.el
+        this
             .querySelector('.controlbox-panes')
             .insertAdjacentElement('afterBegin', this.controlbox_pane.el);
-    },
+    }
 
     async close (ev) {
         if (ev && ev.preventDefault) {
@@ -143,48 +128,42 @@ const ControlBoxViewMixin = {
         }
         api.trigger('controlBoxClosed', this);
         return this;
-    },
-
-    ensureClosedState () {
-        if (this.model.get('closed')) {
-            this.hide();
-        } else {
-            this.show();
-        }
-    },
+    }
 
     hide (callback) {
         if (api.settings.get('sticky_controlbox')) {
             return;
         }
-        u.addClass('hidden', this.el);
+        u.addClass('hidden', this);
         api.trigger('chatBoxClosed', this);
         if (!api.connection.connected()) {
             _converse.controlboxtoggle.render();
         }
         _converse.controlboxtoggle.show(callback);
         return this;
-    },
+    }
 
     onControlBoxToggleHidden () {
         this.model.set('closed', false);
-        this.el.classList.remove('hidden');
+        this.classList.remove('hidden');
         /**
          * Triggered once the controlbox has been opened
          * @event _converse#controlBoxOpened
          * @type {_converse.ControlBox}
          */
         api.trigger('controlBoxOpened', this);
-    },
+    }
 
     show () {
         _converse.controlboxtoggle.hide(() => this.onControlBoxToggleHidden());
         return this;
-    },
+    }
 
-    showHelpMessages () {
+    showHelpMessages () { // eslint-disable-line class-methods-use-this
         return;
     }
-};
+}
+
+api.elements.define('converse-controlbox', ControlBoxView);
 
-export default ControlBoxViewMixin;
+export default ControlBoxView;

+ 2 - 0
src/plugins/dragresize/index.js

@@ -126,6 +126,8 @@ converse.plugins.add('converse-dragresize', {
         });
 
         Object.assign(_converse.ChatBoxView.prototype, DragResizableMixin);
+        Object.assign(_converse.ChatRoomView.prototype, DragResizableMixin);
+        Object.assign(_converse.ControlBoxView.prototype, DragResizableMixin);
 
         /************************ BEGIN Event Handlers ************************/
         function registerGlobalEventHandlers () {

+ 8 - 8
src/plugins/dragresize/mixin.js

@@ -11,7 +11,7 @@ const DragResizableMixin = {
 
         // Determine and store the default box size.
         // We need this information for the drag-resizing feature.
-        const flyout = this.el.querySelector('.box-flyout');
+        const flyout = this.querySelector('.box-flyout');
         const style = window.getComputedStyle(flyout);
 
         if (this.model.get('height') === undefined) {
@@ -66,7 +66,7 @@ const DragResizableMixin = {
         // If a custom width is applied (due to drag-resizing),
         // then we need to set the width of the .chatbox element as well.
         if (this.model.get('width')) {
-            this.el.style.width = this.model.get('width');
+            this.style.width = this.model.get('width');
         }
     },
 
@@ -83,7 +83,7 @@ const DragResizableMixin = {
         } else {
             height = '';
         }
-        const flyout_el = this.el.querySelector('.box-flyout');
+        const flyout_el = this.querySelector('.box-flyout');
         if (flyout_el !== null) {
             flyout_el.style.height = height;
         }
@@ -95,8 +95,8 @@ const DragResizableMixin = {
         } else {
             width = '';
         }
-        this.el.style.width = width;
-        const flyout_el = this.el.querySelector('.box-flyout');
+        this.style.width = width;
+        const flyout_el = this.querySelector('.box-flyout');
         if (flyout_el !== null) {
             flyout_el.style.width = width;
         }
@@ -124,7 +124,7 @@ const DragResizableMixin = {
         }
         ev.preventDefault();
         // Record element attributes for mouseMove().
-        const flyout = this.el.querySelector('.box-flyout'),
+        const flyout = this.querySelector('.box-flyout'),
             style = window.getComputedStyle(flyout);
         this.height = parseInt(style.height.replace(/px$/, ''), 10);
         _converse.resizing = {
@@ -147,8 +147,8 @@ const DragResizableMixin = {
             return true;
         }
         ev.preventDefault();
-        const flyout = this.el.querySelector('.box-flyout'),
-            style = window.getComputedStyle(flyout);
+        const flyout = this.querySelector('.box-flyout');
+        const style = window.getComputedStyle(flyout);
         this.width = parseInt(style.width.replace(/px$/, ''), 10);
         _converse.resizing = {
             'chatbox': this,

+ 2 - 2
src/plugins/dragresize/utils.js

@@ -23,8 +23,8 @@ export function applyDragResistance (value, default_value) {
     return value;
 }
 
-export function renderDragResizeHandles (_converse, view) {
-    const flyout = view.el.querySelector('.box-flyout');
+export function renderDragResizeHandles (_converse, el) {
+    const flyout = el.querySelector('.box-flyout');
     const div = document.createElement('div');
     render(tpl_dragresize(), div);
     flyout.insertBefore(div, flyout.firstChild);

+ 2 - 2
src/plugins/headlines-view/index.js

@@ -5,7 +5,7 @@
  */
 import '../chatview/index.js';
 import HeadlinesBoxViewMixin from './view.js';
-import { HeadlinesPanelMixin, HeadlinesPanelView } from './panel.js';
+import { HeadlinesPanelMixin, HeadlinesPanel} from './panel.js';
 import { _converse, api, converse } from '@converse/headless/core';
 
 function onChatBoxViewsInitialized () {
@@ -45,7 +45,7 @@ converse.plugins.add('converse-headlines-view', {
          */
         _converse.ControlBoxView && Object.assign(_converse.ControlBoxView.prototype, HeadlinesPanelMixin);
         _converse.HeadlinesBoxView = _converse.ChatBoxView.extend(HeadlinesBoxViewMixin);
-        _converse.HeadlinesPanel = HeadlinesPanelView;
+        _converse.HeadlinesPanel = HeadlinesPanel;
 
         api.listen.on('chatBoxViewsInitialized', onChatBoxViewsInitialized);
     }

+ 14 - 19
src/plugins/headlines-view/panel.js

@@ -1,5 +1,5 @@
 import tpl_headline_panel from './templates/panel.js';
-import { View } from '@converse/skeletor/src/view.js';
+import { ElementView } from '@converse/skeletor/src/element.js';
 import { __ } from 'i18n';
 import { _converse, api, converse } from '@converse/headless/core';
 
@@ -11,22 +11,20 @@ const u = converse.env.utils;
  * @namespace _converse.HeadlinesPanel
  * @memberOf _converse
  */
-export const HeadlinesPanelView = View.extend({
-    tagName: 'div',
-    className: 'controlbox-section',
-    id: 'headline',
-
-    events: {
+export class HeadlinesPanel extends ElementView {
+    tagName = 'div'
+    className = 'controlbox-section'
+    id = 'headline'
+    events = {
         'click .open-headline': 'openHeadline'
-    },
+    }
 
     initialize () {
         this.listenTo(this.model, 'add', this.renderIfHeadline);
         this.listenTo(this.model, 'remove', this.renderIfHeadline);
         this.listenTo(this.model, 'destroy', this.renderIfHeadline);
         this.render();
-        this.insertIntoDOM();
-    },
+    }
 
     toHTML () {
         return tpl_headline_panel({
@@ -34,24 +32,21 @@ export const HeadlinesPanelView = View.extend({
             'headlineboxes': this.model.filter(m => m.get('type') === _converse.HEADLINES_TYPE),
             'open_title': __('Click to open this server message')
         });
-    },
+    }
 
     renderIfHeadline (model) {
         return model && model.get('type') === _converse.HEADLINES_TYPE && this.render();
-    },
+    }
 
-    openHeadline (ev) {
+    openHeadline (ev) { // eslint-disable-line class-methods-use-this
         ev.preventDefault();
         const jid = ev.target.getAttribute('data-headline-jid');
         const chat = _converse.chatboxes.get(jid);
         chat.maybeShow(true);
-    },
-
-    insertIntoDOM () {
-        const view = _converse.chatboxviews.get('controlbox');
-        view && view.el.querySelector('.controlbox-pane').insertAdjacentElement('beforeEnd', this.el);
     }
-});
+}
+
+api.elements.define('converse-headlines-panel', HeadlinesPanel);
 
 /**
  * Mixin for the {@link _converse.ControlBoxView } which add support for

+ 6 - 0
src/plugins/headlines-view/view.js

@@ -13,8 +13,14 @@ const HeadlinesBoxViewMixin = {
     },
 
     async initialize () {
+        const jid = this.getAttribute('jid');
+        _converse.chatboxviews.add(jid, this);
+
+        this.model = _converse.chatboxes.get(jid);
         this.initDebounced();
 
+        api.listen.on('windowStateChanged', this.onWindowStateChanged);
+
         this.model.disable_mam = true; // Don't do MAM queries for this box
         this.listenTo(this.model, 'change:hidden', m => (m.get('hidden') ? this.hide() : this.show()));
         this.listenTo(this.model, 'destroy', this.remove);

+ 2 - 1
src/plugins/minimize/index.js

@@ -125,7 +125,8 @@ converse.plugins.add('converse-minimize', {
 
 
         /************************ BEGIN Event Handlers ************************/
-        api.listen.on('chatBoxInsertedIntoDOM', view => _converse.minimize.trimChats(view));
+        api.listen.on('chatBoxViewInitialized', view => _converse.minimize.trimChats(view));
+        api.listen.on('chatRoomViewInitialized', view => _converse.minimize.trimChats(view));
         api.listen.on('connected', () => initMinimizedChats());
         api.listen.on('controlBoxOpened', view => _converse.minimize.trimChats(view));
         api.listen.on('chatBoxViewInitialized', v => v.listenTo(v.model, 'change:minimized', v.onMinimizedChanged));

+ 0 - 1
src/plugins/minimize/mixins.js

@@ -66,7 +66,6 @@ export const minimizableChatBoxView = {
             this.model.set({ 'scroll': this.content.scrollTop });
         }
         this.model.setChatState(_converse.INACTIVE);
-        this.hide();
         /**
          * Triggered when a previously maximized chat gets Minimized
          * @event _converse#chatBoxMinimized

+ 7 - 11
src/plugins/minimize/utils.js

@@ -6,28 +6,24 @@ const u = converse.env.utils;
 
 function getChatBoxWidth (view) {
     if (view.model.get('id') === 'controlbox') {
-        const controlbox = view.model;
         // We return the width of the controlbox or its toggle,
         // depending on which is visible.
-        if (u.isVisible(controlbox.el)) {
-            return u.getOuterWidth(controlbox.el, true);
+        if (u.isVisible(view)) {
+            return u.getOuterWidth(view, true);
         } else {
             return u.getOuterWidth(_converse.controlboxtoggle.el, true);
         }
-    } else if (!view.model.get('minimized') && u.isVisible(view.el)) {
-        return u.getOuterWidth(view.el, true);
+    } else if (!view.model.get('minimized') && u.isVisible(view)) {
+        return u.getOuterWidth(view, true);
     }
     return 0;
 }
 
 function getShownChats () {
-    return _converse.chatboxviews.filter((view) =>
+    return _converse.chatboxviews.filter(el =>
         // The controlbox can take a while to close,
-        // so we need to check its state. That's why we checked
-        // the 'closed' state.
-        !view.model.get('minimized') &&
-            !view.model.get('closed') &&
-            u.isVisible(view.el)
+        // so we need to check its state. That's why we checked the 'closed' state.
+        !el.model.get('minimized') && !el.model.get('closed') && u.isVisible(el)
     );
 }
 

+ 11 - 16
src/plugins/muc-views/index.js

@@ -7,7 +7,7 @@
 import '../../components/muc-sidebar';
 import '../chatview/index.js';
 import '../modal.js';
-import ChatRoomViewMixin from './muc.js';
+import MUCView from './muc.js';
 import MUCConfigForm from './config-form.js';
 import MUCPasswordForm from './password-form.js';
 import log from '@converse/headless/log';
@@ -60,18 +60,10 @@ function fetchAndSetMUCDomain (controlboxview) {
     }
 }
 
-function openChatRoomFromURIClicked (ev) {
-    ev.preventDefault();
-    api.rooms.open(ev.target.href);
-}
-
-async function addView (model) {
-    const views = _converse.chatboxviews;
-    if (!views.get(model.get('id')) && model.get('type') === _converse.CHATROOMS_TYPE && model.isValid()) {
-        await model.initialized;
-        return views.add(model.get('id'), new _converse.ChatRoomView({ model }));
-    }
-}
+// function openChatRoomFromURIClicked (ev) {
+//     ev.preventDefault();
+//     api.rooms.open(ev.target.href);
+// }
 
 converse.plugins.add('converse-muc-views', {
     /* Dependencies are other plugins which might be
@@ -129,7 +121,7 @@ converse.plugins.add('converse-muc-views', {
 
         _converse.MUCConfigForm = MUCConfigForm;
         _converse.MUCPasswordForm = MUCPasswordForm;
-        _converse.ChatRoomView = _converse.ChatBoxView.extend(ChatRoomViewMixin);
+        _converse.ChatRoomView = MUCView;
         _converse.RoomsPanel = RoomsPanel;
         _converse.ControlBoxView && Object.assign(_converse.ControlBoxView.prototype, RoomsPanelViewMixin);
 
@@ -137,8 +129,11 @@ converse.plugins.add('converse-muc-views', {
 
         /************************ BEGIN Event Handlers ************************/
         api.listen.on('chatBoxViewsInitialized', () => {
-            _converse.chatboxviews.delegate('click', 'a.open-chatroom', openChatRoomFromURIClicked);
-            _converse.chatboxes.on('add', addView);
+            // FIXME: Find a new way to implement this
+            // _converse.chatboxviews.delegate('click', 'a.open-chatroom', openChatRoomFromURIClicked);
+
+            // TODO: Remove
+            // _converse.chatboxes.on('add', addView);
         });
 
         api.listen.on('clearSession', () => {

+ 120 - 117
src/plugins/muc-views/muc.js

@@ -1,6 +1,7 @@
 import './config-form.js';
 import './password-form.js';
 import 'shared/autocomplete/index.js';
+import BaseChatView from 'shared/chatview.js';
 import MUCInviteModal from 'modals/muc-invite.js';
 import ModeratorToolsModal from 'modals/moderator-tools.js';
 import OccupantModal from 'modals/occupant.js';
@@ -11,9 +12,9 @@ import tpl_chatroom_head from 'templates/chatroom_head.js';
 import tpl_muc_bottom_panel from 'templates/muc_bottom_panel.js';
 import tpl_muc_destroyed from 'templates/muc_destroyed.js';
 import tpl_muc_disconnect from 'templates/muc_disconnect.js';
-import { $pres, Strophe } from 'strophe.js/src/strophe';
 import tpl_muc_nickname_form from 'templates/muc_nickname_form.js';
 import tpl_spinner from 'templates/spinner.js';
+import { $pres, Strophe } from 'strophe.js/src/strophe';
 import { Model } from '@converse/skeletor/src/model.js';
 import { __ } from 'i18n';
 import { _converse, api, converse } from '@converse/headless/core';
@@ -49,12 +50,12 @@ const COMMAND_TO_AFFILIATION = {
  * @namespace _converse.ChatRoomView
  * @memberOf _converse
  */
-const ChatRoomViewMixin = {
-    length: 300,
-    tagName: 'div',
-    className: 'chatbox chatroom hidden',
-    is_chatroom: true,
-    events: {
+export default class MUCView extends BaseChatView {
+    length = 300
+    tagName = 'div'
+    className = 'chatbox chatroom hidden'
+    is_chatroom = true
+    events = {
         'click .chatbox-navback': 'showControlBox',
         'click .hide-occupants': 'hideOccupants',
         'click .new-msgs-indicator': 'viewUnreadMessages',
@@ -71,21 +72,25 @@ const ChatRoomViewMixin = {
         'mousedown .dragresize-occupants-left': 'onStartResizeOccupants',
         'paste .chat-textarea': 'onPaste',
         'submit .muc-nickname-form': 'submitNickname'
-    },
+    }
 
     async initialize () {
+        const jid = this.getAttribute('jid');
+        _converse.chatboxviews.add(jid, this);
+
+        this.model = _converse.chatboxes.get(jid);
         this.initDebounced();
 
+        api.listen.on('windowStateChanged', this.onWindowStateChanged);
+
         this.listenTo(
             this.model,
             'change',
             debounce(() => this.renderHeading(), 250)
         );
         this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
-        this.listenTo(this.model, 'change:hidden', m => (m.get('hidden') ? this.hide() : this.show()));
         this.listenTo(this.model, 'change:hidden_occupants', this.onSidebarToggle);
         this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
-        this.listenTo(this.model, 'destroy', this.hide);
         this.listenTo(this.model, 'show', this.show);
         this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
         this.listenTo(this.model.features, 'change:open', this.renderHeading);
@@ -115,7 +120,6 @@ const ChatRoomViewMixin = {
         this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
 
         this.renderChatContent();
-        this.insertIntoDOM();
         // Register later due to await
         const user_settings = await _converse.api.user.settings.getModel();
         this.listenTo(user_settings, 'change:mucs_with_hidden_subject', this.renderHeading);
@@ -129,11 +133,11 @@ const ChatRoomViewMixin = {
          * @example _converse.api.listen.on('chatRoomViewInitialized', view => { ... });
          */
         api.trigger('chatRoomViewInitialized', this);
-    },
+    }
 
     async render () {
         const sidebar_hidden = !this.shouldShowSidebar();
-        this.el.setAttribute('id', this.model.get('box_id'));
+        this.setAttribute('id', this.model.get('box_id'));
         render(
             tpl_chatroom({
                 sidebar_hidden,
@@ -146,13 +150,13 @@ const ChatRoomViewMixin = {
                 'muc_show_logs_before_join': api.settings.get('muc_show_logs_before_join'),
                 'show_send_button': _converse.show_send_button
             }),
-            this.el
+            this
         );
 
-        this.notifications = this.el.querySelector('.chat-content__notifications');
-        this.content = this.el.querySelector('.chat-content');
-        this.msgs_container = this.el.querySelector('.chat-content__messages');
-        this.help_container = this.el.querySelector('.chat-content__help');
+        this.notifications = this.querySelector('.chat-content__notifications');
+        this.content = this.querySelector('.chat-content');
+        this.msgs_container = this.querySelector('.chat-content__messages');
+        this.help_container = this.querySelector('.chat-content__help');
 
         this.renderBottomPanel();
         if (
@@ -166,7 +170,7 @@ const ChatRoomViewMixin = {
         // Otherwise e.g. this.notifications is not yet defined when accessed elsewhere.
         await this.renderHeading();
         !this.model.get('hidden') && this.show();
-    },
+    }
 
     getNotifications () {
         const actors_per_state = this.model.notifications.toJSON();
@@ -240,7 +244,7 @@ const ChatRoomViewMixin = {
             }
             return result;
         }, '');
-    },
+    }
 
     getHelpMessages () {
         const setting = api.settings.get('muc_disable_slash_commands');
@@ -269,7 +273,7 @@ const ChatRoomViewMixin = {
         ]
             .filter(line => disabled_commands.every(c => !line.startsWith(c + '<', 9)))
             .filter(line => this.getAllowedCommands().some(c => line.startsWith(c + '<', 9)));
-    },
+    }
 
     /**
      * Renders the MUC heading if any relevant attributes have changed.
@@ -279,11 +283,11 @@ const ChatRoomViewMixin = {
      */
     async renderHeading () {
         const tpl = await this.generateHeadingTemplate();
-        render(tpl, this.el.querySelector('.chat-head-chatroom'));
-    },
+        render(tpl, this.querySelector('.chat-head-chatroom'));
+    }
 
     renderBottomPanel () {
-        const container = this.el.querySelector('.bottom-panel');
+        const container = this.querySelector('.bottom-panel');
         const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
         const can_edit = entered && !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor');
         render(tpl_muc_bottom_panel({ can_edit, entered }), container);
@@ -291,18 +295,18 @@ const ChatRoomViewMixin = {
             this.renderMessageForm();
             this.initMentionAutoComplete();
         }
-    },
+    }
 
     onStartResizeOccupants (ev) {
         this.resizing = true;
-        this.el.addEventListener('mousemove', this.onMouseMove);
-        this.el.addEventListener('mouseup', this.onMouseUp);
+        this.addEventListener('mousemove', this.onMouseMove);
+        this.addEventListener('mouseup', this.onMouseUp);
 
-        const sidebar_el = this.el.querySelector('converse-muc-sidebar');
+        const sidebar_el = this.querySelector('converse-muc-sidebar');
         const style = window.getComputedStyle(sidebar_el);
         this.width = parseInt(style.width.replace(/px$/, ''), 10);
         this.prev_pageX = ev.pageX;
-    },
+    }
 
     onMouseMove (ev) {
         if (this.resizing) {
@@ -311,24 +315,24 @@ const ChatRoomViewMixin = {
             this.resizeSidebarView(delta, ev.pageX);
             this.prev_pageX = ev.pageX;
         }
-    },
+    }
 
     onMouseUp (ev) {
         if (this.resizing) {
             ev.preventDefault();
             this.resizing = false;
-            this.el.removeEventListener('mousemove', this.onMouseMove);
-            this.el.removeEventListener('mouseup', this.onMouseUp);
-            const sidebar_el = this.el.querySelector('converse-muc-sidebar');
+            this.removeEventListener('mousemove', this.onMouseMove);
+            this.removeEventListener('mouseup', this.onMouseUp);
+            const sidebar_el = this.querySelector('converse-muc-sidebar');
             const element_position = sidebar_el.getBoundingClientRect();
             const occupants_width = this.calculateSidebarWidth(element_position, 0);
             const attrs = { occupants_width };
             _converse.connection.connected ? this.model.save(attrs) : this.model.set(attrs);
         }
-    },
+    }
 
     resizeSidebarView (delta, current_mouse_position) {
-        const sidebar_el = this.el.querySelector('converse-muc-sidebar');
+        const sidebar_el = this.querySelector('converse-muc-sidebar');
         const element_position = sidebar_el.getBoundingClientRect();
         if (this.is_minimum) {
             this.is_minimum = element_position.left < current_mouse_position;
@@ -338,11 +342,11 @@ const ChatRoomViewMixin = {
             const occupants_width = this.calculateSidebarWidth(element_position, delta);
             sidebar_el.style.flex = '0 0 ' + occupants_width + 'px';
         }
-    },
+    }
 
     calculateSidebarWidth (element_position, delta) {
         let occupants_width = element_position.width + delta;
-        const room_width = this.el.clientWidth;
+        const room_width = this.clientWidth;
         // keeping display in boundaries
         if (occupants_width < room_width * 0.2) {
             // set pixel to 20% width
@@ -361,13 +365,13 @@ const ChatRoomViewMixin = {
             this.is_minimum = false;
         }
         return occupants_width;
-    },
+    }
 
     getAutoCompleteList () {
         return this.model.getAllKnownNicknames().map(nick => ({ 'label': nick, 'value': `@${nick}` }));
-    },
+    }
 
-    getAutoCompleteListItem (text, input) {
+    getAutoCompleteListItem (text, input) { // eslint-disable-line class-methods-use-this
         input = input.trim();
         const element = document.createElement('li');
         element.setAttribute('aria-selected', 'false');
@@ -401,10 +405,10 @@ const ChatRoomViewMixin = {
         });
 
         return element;
-    },
+    }
 
     initMentionAutoComplete () {
-        this.mention_auto_complete = new _converse.AutoComplete(this.el, {
+        this.mention_auto_complete = new _converse.AutoComplete(this, {
             'auto_first': true,
             'auto_evaluate': false,
             'min_chars': api.settings.get('muc_mention_autocomplete_min_chars'),
@@ -419,7 +423,7 @@ const ChatRoomViewMixin = {
             'item': this.getAutoCompleteListItem
         });
         this.mention_auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
-    },
+    }
 
     /**
      * Get the nickname value from the form and then join the groupchat with it.
@@ -431,19 +435,19 @@ const ChatRoomViewMixin = {
         ev.preventDefault();
         const nick = ev.target.nick.value.trim();
         nick && this.model.join(nick);
-    },
+    }
 
     onKeyDown (ev) {
         if (this.mention_auto_complete.onKeyDown(ev)) {
             return;
         }
         return _converse.ChatBoxView.prototype.onKeyDown.call(this, ev);
-    },
+    }
 
     onKeyUp (ev) {
         this.mention_auto_complete.evaluate(ev);
         return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
-    },
+    }
 
     async onMessageRetractButtonClicked (message) {
         const retraction_warning = __(
@@ -480,7 +484,7 @@ const ChatRoomViewMixin = {
             const err_msg = __(`Sorry, you're not allowed to retract this message`);
             api.alert('error', __('Error'), err_msg);
         }
-    },
+    }
 
     /**
      * Retract someone else's message in this groupchat.
@@ -501,7 +505,7 @@ const ChatRoomViewMixin = {
             log(err_msg, Strophe.LogLevel.WARN);
             log(result, Strophe.LogLevel.WARN);
         }
-    },
+    }
 
     showModeratorToolsModal (affiliation) {
         if (!this.verifyRoles(['moderator'])) {
@@ -515,48 +519,48 @@ const ChatRoomViewMixin = {
             modal = api.modal.create(ModeratorToolsModal, { model, _converse, 'chatroomview': this });
         }
         modal.show();
-    },
+    }
 
     showRoomDetailsModal (ev) {
         ev.preventDefault();
         api.modal.show(RoomDetailsModal, { 'model': this.model }, ev);
-    },
+    }
 
-    showOccupantDetailsModal (ev, message) {
+    showOccupantDetailsModal (ev, message) { // eslint-disable-line class-methods-use-this
         ev.preventDefault();
         api.modal.show(OccupantModal, { 'model': message.occupant }, ev);
-    },
+    }
 
     showChatStateNotification (message) {
         if (message.get('sender') === 'me') {
             return;
         }
         return _converse.ChatBoxView.prototype.showChatStateNotification.apply(this, arguments);
-    },
+    }
 
     shouldShowSidebar () {
         return (
             !this.model.get('hidden_occupants') &&
             this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED
         );
-    },
+    }
 
     onSidebarToggle () {
         this.renderToolbar();
-        this.el.querySelector('.occupants')?.setVisibility();
-    },
+        this.querySelector('.occupants')?.setVisibility();
+    }
 
     onOccupantAffiliationChanged (occupant) {
         if (occupant.get('jid') === _converse.bare_jid) {
             this.renderHeading();
         }
-    },
+    }
 
     onOccupantRoleChanged (occupant) {
         if (occupant.get('jid') === _converse.bare_jid) {
             this.renderBottomPanel();
         }
-    },
+    }
 
     /**
      * Returns a list of objects which represent buttons for the groupchat header.
@@ -653,7 +657,7 @@ const ChatRoomViewMixin = {
             });
         }
         return _converse.api.hook('getHeadingButtons', this, buttons);
-    },
+    }
 
     /**
      * Returns the groupchat heading TemplateResult to be rendered.
@@ -674,16 +678,16 @@ const ChatRoomViewMixin = {
                 'title': this.model.getDisplayName()
             })
         );
-    },
+    }
 
     toggleTopic () {
         this.model.toggleSubjectHiddenState();
-    },
+    }
 
     showInviteModal (ev) {
         ev.preventDefault();
         api.modal.show(MUCInviteModal, { 'model': new Model(), 'chatroomview': this }, ev);
-    },
+    }
 
     /**
      * Callback method that gets called after the chat has become visible.
@@ -696,7 +700,7 @@ const ChatRoomViewMixin = {
         // This is instead done in `onConnectionStatusChanged` below.
         this.model.clearUnreadMsgCounter();
         this.scrollDown();
-    },
+    }
 
     onConnectionStatusChanged () {
         const conn_status = this.model.session.get('connection_status');
@@ -715,7 +719,7 @@ const ChatRoomViewMixin = {
         } else if (conn_status === converse.ROOMSTATUS.DESTROYED) {
             this.showDestroyedMessage();
         }
-    },
+    }
 
     getToolbarOptions () {
         return Object.assign(_converse.ChatBoxView.prototype.getToolbarOptions.apply(this, arguments), {
@@ -723,7 +727,7 @@ const ChatRoomViewMixin = {
             'label_hide_occupants': __('Hide the list of participants'),
             'show_occupants_toggle': _converse.visible_toolbar_buttons.toggle_occupants
         });
-    },
+    }
 
     /**
      * Closes this chat box, which implies leaving the groupchat as well.
@@ -731,12 +735,11 @@ const ChatRoomViewMixin = {
      * @method _converse.ChatRoomView#close
      */
     close () {
-        this.hide();
         if (_converse.router.history.getFragment() === 'converse/room?jid=' + this.model.get('jid')) {
             _converse.router.navigate('');
         }
         return _converse.ChatBoxView.prototype.close.apply(this, arguments);
-    },
+    }
 
     /**
      * Hide the right sidebar containing the chat occupants.
@@ -750,7 +753,7 @@ const ChatRoomViewMixin = {
         }
         this.model.save({ 'hidden_occupants': true });
         this.scrollDown();
-    },
+    }
 
     verifyRoles (roles, occupant, show_error = true) {
         if (!Array.isArray(roles)) {
@@ -771,7 +774,7 @@ const ChatRoomViewMixin = {
             this.model.createMessage({ message, 'type': 'error' });
         }
         return false;
-    },
+    }
 
     verifyAffiliations (affiliations, occupant, show_error = true) {
         if (!Array.isArray(affiliations)) {
@@ -792,7 +795,7 @@ const ChatRoomViewMixin = {
             this.model.createMessage({ message, 'type': 'error' });
         }
         return false;
-    },
+    }
 
     validateRoleOrAffiliationChangeArgs (command, args) {
         if (!args) {
@@ -804,7 +807,7 @@ const ChatRoomViewMixin = {
             return false;
         }
         return true;
-    },
+    }
 
     getNickOrJIDFromCommandArgs (args) {
         if (u.isValidJID(args.trim())) {
@@ -832,7 +835,7 @@ const ChatRoomViewMixin = {
             return;
         }
         return nick_or_jid;
-    },
+    }
 
     setAffiliation (command, args, required_affiliations) {
         const affiliation = COMMAND_TO_AFFILIATION[command];
@@ -874,11 +877,11 @@ const ChatRoomViewMixin = {
             .setAffiliation(affiliation, [attrs])
             .then(() => this.model.occupants.fetchMembers())
             .catch(err => this.onCommandError(err));
-    },
+    }
 
-    getReason (args) {
+    getReason (args) { // eslint-disable-line class-methods-use-this
         return args.includes(',') ? args.slice(args.indexOf(',') + 1).trim() : null;
-    },
+    }
 
     setRole (command, args, required_affiliations = [], required_roles = []) {
         /* Check that a command to change a groupchat user's role or
@@ -903,7 +906,7 @@ const ChatRoomViewMixin = {
         const occupant = this.model.getOccupant(nick_or_jid);
         this.model.setRole(occupant, role, reason, undefined, this.onCommandError.bind(this));
         return true;
-    },
+    }
 
     onCommandError (err) {
         log.fatal(err);
@@ -912,7 +915,7 @@ const ChatRoomViewMixin = {
             ' ' +
             __("Check your browser's developer console for details.");
         this.model.createMessage({ message, 'type': 'error' });
-    },
+    }
 
     getAllowedCommands () {
         let allowed_commands = ['clear', 'help', 'me', 'nick', 'register'];
@@ -937,7 +940,7 @@ const ChatRoomViewMixin = {
         } else {
             return allowed_commands;
         }
-    },
+    }
 
     async destroy () {
         const messages = [__('Are you sure you want to destroy this groupchat?')];
@@ -968,7 +971,7 @@ const ChatRoomViewMixin = {
         } catch (e) {
             log.error(e);
         }
-    },
+    }
 
     parseMessageForCommands (text) {
         if (
@@ -1088,7 +1091,7 @@ const ChatRoomViewMixin = {
                 return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
         }
         return true;
-    },
+    }
 
     /**
      * Renders a form given an IQ stanza containing the current
@@ -1107,11 +1110,11 @@ const ChatRoomViewMixin = {
                 'model': this.model,
                 'chatroomview': this
             });
-            const container_el = this.el.querySelector('.chatroom-body');
+            const container_el = this.querySelector('.chatroom-body');
             container_el.insertAdjacentElement('beforeend', this.config_form.el);
         }
         u.showElement(this.config_form.el);
-    },
+    }
 
     /**
      * Renders a form which allows the user to choose theirnickname.
@@ -1122,24 +1125,24 @@ const ChatRoomViewMixin = {
         const tpl_result = tpl_muc_nickname_form(this.model.toJSON());
         if (api.settings.get('muc_show_logs_before_join')) {
             this.hideSpinner();
-            u.showElement(this.el.querySelector('.chat-area'));
-            const container = this.el.querySelector('.muc-bottom-panel');
+            u.showElement(this.querySelector('.chat-area'));
+            const container = this.querySelector('.muc-bottom-panel');
             render(tpl_result, container);
             u.addClass('muc-bottom-panel--nickname', container);
         } else {
-            const form = this.el.querySelector('.muc-nickname-form');
+            const form = this.querySelector('.muc-nickname-form');
             const form_el = u.getElementFromTemplateResult(tpl_result);
             if (form) {
-                sizzle('.spinner', this.el).forEach(u.removeElement);
+                sizzle('.spinner', this).forEach(u.removeElement);
                 form.outerHTML = form_el.outerHTML;
             } else {
                 this.hideChatRoomContents();
-                const container = this.el.querySelector('.chatroom-body');
+                const container = this.querySelector('.chatroom-body');
                 container.insertAdjacentElement('beforeend', form_el);
             }
         }
         u.safeSave(this.model.session, { 'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED });
-    },
+    }
 
     /**
      * Remove the configuration form without submitting and return to the chat view.
@@ -1147,9 +1150,9 @@ const ChatRoomViewMixin = {
      * @method _converse.ChatRoomView#closeForm
      */
     closeForm () {
-        sizzle('.chatroom-form-container', this.el).forEach(e => u.addClass('hidden', e));
+        sizzle('.chatroom-form-container', this).forEach(e => u.addClass('hidden', e));
         this.renderAfterTransition();
-    },
+    }
 
     /**
      * Start the process of configuring a groupchat, either by
@@ -1175,14 +1178,14 @@ const ChatRoomViewMixin = {
         } else {
             this.closeForm();
         }
-    },
+    }
 
     hideChatRoomContents () {
-        const container_el = this.el.querySelector('.chatroom-body');
+        const container_el = this.querySelector('.chatroom-body');
         if (container_el !== null) {
             [].forEach.call(container_el.children, child => child.classList.add('hidden'));
         }
-    },
+    }
 
     renderPasswordForm () {
         this.hideChatRoomContents();
@@ -1196,19 +1199,19 @@ const ChatRoomViewMixin = {
                 }),
                 'chatroomview': this
             });
-            const container_el = this.el.querySelector('.chatroom-body');
+            const container_el = this.querySelector('.chatroom-body');
             container_el.insertAdjacentElement('beforeend', this.password_form.el);
         } else {
             this.password_form.model.set('validation_message', message);
         }
         u.showElement(this.password_form.el);
         this.model.session.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED);
-    },
+    }
 
     showDestroyedMessage () {
-        u.hideElement(this.el.querySelector('.chat-area'));
-        u.hideElement(this.el.querySelector('.occupants'));
-        sizzle('.spinner', this.el).forEach(u.removeElement);
+        u.hideElement(this.querySelector('.chat-area'));
+        u.hideElement(this.querySelector('.occupants'));
+        sizzle('.spinner', this).forEach(u.removeElement);
 
         const reason = this.model.get('destroyed_reason');
         const moved_jid = this.model.get('moved_jid');
@@ -1216,7 +1219,7 @@ const ChatRoomViewMixin = {
             'destroyed_reason': undefined,
             'moved_jid': undefined
         });
-        const container = this.el.querySelector('.disconnect-container');
+        const container = this.querySelector('.disconnect-container');
         render(tpl_muc_destroyed(moved_jid, reason), container);
         const switch_el = container.querySelector('a.switch-chat');
         if (switch_el) {
@@ -1228,16 +1231,16 @@ const ChatRoomViewMixin = {
             });
         }
         u.showElement(container);
-    },
+    }
 
     showDisconnectMessage () {
         const message = this.model.get('disconnection_message');
         if (!message) {
             return;
         }
-        u.hideElement(this.el.querySelector('.chat-area'));
-        u.hideElement(this.el.querySelector('.occupants'));
-        sizzle('.spinner', this.el).forEach(u.removeElement);
+        u.hideElement(this.querySelector('.chat-area'));
+        u.hideElement(this.querySelector('.occupants'));
+        sizzle('.spinner', this).forEach(u.removeElement);
 
         const messages = [message];
         const actor = this.model.get('disconnection_actor');
@@ -1253,17 +1256,17 @@ const ChatRoomViewMixin = {
             'disconnection_reason': undefined,
             'disconnection_actor': undefined
         });
-        const container = this.el.querySelector('.disconnect-container');
+        const container = this.querySelector('.disconnect-container');
         render(tpl_muc_disconnect(messages), container);
         u.showElement(container);
-    },
+    }
 
     onOccupantAdded (occupant) {
         if (occupant.get('jid') === _converse.bare_jid) {
             this.renderHeading();
             this.renderBottomPanel();
         }
-    },
+    }
 
     /**
      * Working backwards, get today's most recent join/leave notification
@@ -1273,7 +1276,7 @@ const ChatRoomViewMixin = {
      * @param {HTMLElement} el
      * @param {string} nick
      */
-    getPreviousJoinOrLeaveNotification (el, nick) {
+    getPreviousJoinOrLeaveNotification (el, nick) { // eslint-disable-line class-methods-use-this
         const today = new Date().toISOString().split('T')[0];
         while (el !== null) {
             if (!el.classList.contains('chat-info')) {
@@ -1291,7 +1294,7 @@ const ChatRoomViewMixin = {
             }
             el = el.previousElementSibling;
         }
-    },
+    }
 
     /**
      * Rerender the groupchat after some kind of transition. For
@@ -1308,18 +1311,18 @@ const ChatRoomViewMixin = {
             this.renderPasswordForm();
         } else if (conn_status == converse.ROOMSTATUS.ENTERED) {
             this.hideChatRoomContents();
-            u.showElement(this.el.querySelector('.chat-area'));
-            this.el.querySelector('.occupants')?.setVisibility();
+            u.showElement(this.querySelector('.chat-area'));
+            this.querySelector('.occupants')?.setVisibility();
             this.scrollDown();
         }
-    },
+    }
 
     showSpinner () {
-        sizzle('.spinner', this.el).forEach(u.removeElement);
+        sizzle('.spinner', this).forEach(u.removeElement);
         this.hideChatRoomContents();
-        const container_el = this.el.querySelector('.chatroom-body');
+        const container_el = this.querySelector('.chatroom-body');
         container_el.insertAdjacentElement('afterbegin', u.getElementFromTemplateResult(tpl_spinner()));
-    },
+    }
 
     /**
      * Check if the spinner is being shown and if so, hide it.
@@ -1329,13 +1332,13 @@ const ChatRoomViewMixin = {
      * @method _converse.ChatRoomView#hideSpinner
      */
     hideSpinner () {
-        const spinner = this.el.querySelector('.spinner');
+        const spinner = this.querySelector('.spinner');
         if (spinner !== null) {
             u.removeElement(spinner);
             this.renderAfterTransition();
         }
         return this;
     }
-};
+}
 
-export default ChatRoomViewMixin;
+api.elements.define('converse-muc', MUCView);

+ 1 - 1
src/plugins/muc-views/rooms-panel.js

@@ -57,7 +57,7 @@ export const RoomsPanelViewMixin = {
             }))()
         });
         this.roomspanel.model.fetch();
-        this.el.querySelector('.controlbox-pane').insertAdjacentElement('beforeEnd', this.roomspanel.render().el);
+        this.querySelector('.controlbox-pane').insertAdjacentElement('beforeEnd', this.roomspanel.render().el);
 
         /**
          * Triggered once the section of the { @link _converse.ControlBoxView }

+ 9 - 9
src/plugins/omemo.js

@@ -67,23 +67,23 @@ function onChatBoxesInitialized () {
 }
 
 
-function onChatInitialized (view) {
-    view.listenTo(view.model.messages, 'add', (message) => {
+function onChatInitialized (el) {
+    el.listenTo(el.model.messages, 'add', (message) => {
         if (message.get('is_encrypted') && !message.get('is_error')) {
-            view.model.save('omemo_supported', true);
+            el.model.save('omemo_supported', true);
         }
     });
-    view.listenTo(view.model, 'change:omemo_supported', () => {
-        if (!view.model.get('omemo_supported') && view.model.get('omemo_active')) {
-            view.model.set('omemo_active', false);
+    el.listenTo(el.model, 'change:omemo_supported', () => {
+        if (!el.model.get('omemo_supported') && el.model.get('omemo_active')) {
+            el.model.set('omemo_active', false);
         } else {
             // Manually trigger an update, setting omemo_active to
             // false above will automatically trigger one.
-            view.el.querySelector('converse-chat-toolbar')?.requestUpdate();
+            el.querySelector('converse-chat-toolbar')?.requestUpdate();
         }
     });
-    view.listenTo(view.model, 'change:omemo_active', () => {
-        view.el.querySelector('converse-chat-toolbar').requestUpdate();
+    el.listenTo(el.model, 'change:omemo_active', () => {
+        el.querySelector('converse-chat-toolbar').requestUpdate();
     });
 }
 

+ 2 - 1
src/plugins/register/controlbox-mixin.js

@@ -1,6 +1,7 @@
 import { _converse, api } from '@converse/headless/core';
 
 const ControlBoxRegistrationMixin = {
+
     showLoginOrRegisterForm () {
         if (!this.registerpanel) {
             return;
@@ -21,7 +22,7 @@ const ControlBoxRegistrationMixin = {
             });
             this.registerpanel.render();
             this.registerpanel.el.classList.add('hidden');
-            const login_panel = this.el.querySelector('#converse-login-panel');
+            const login_panel = this.querySelector('#converse-login-panel');
             if (login_panel) {
                 login_panel.insertAdjacentElement('afterend', this.registerpanel.el);
             }

+ 31 - 0
src/plugins/rootview/index.js

@@ -0,0 +1,31 @@
+import { api, converse } from '@converse/headless/core';
+
+const u = converse.env.utils;
+
+function ensureElement () {
+    if (!api.settings.get('auto_insert')) {
+        return;
+    }
+    const root = api.settings.get('root');
+    if (!root.querySelector('converse-root#conversejs')) {
+        const el = document.createElement('converse-root');
+        el.setAttribute('id', 'conversejs');
+        u.addClass(`theme-${api.settings.get('theme')}`, el);
+        const body = root.querySelector('body');
+        if (body) {
+            body.appendChild(el);
+        } else {
+            root.appendChild(el); // Perhaps inside a web component?
+        }
+    }
+}
+
+converse.plugins.add('converse-rootview', {
+    initialize () {
+        api.settings.extend({
+            'auto_insert': true
+        });
+
+        api.listen.on('chatBoxesInitialized', ensureElement);
+    }
+});

+ 31 - 0
src/plugins/rootview/view.js

@@ -0,0 +1,31 @@
+import { api, converse } from '@converse/headless/converse-core';
+
+const u = converse.env.utils;
+
+converse.plugins.add('converse-rootview', {
+    initialize () {
+        api.settings.extend({
+            'auto_insert': true
+        });
+
+        function ensureElement () {
+            if (!api.settings.get('auto_insert')) {
+                return;
+            }
+            const root = api.settings.get('root');
+            if (!root.querySelector('converse-root#conversejs')) {
+                const el = document.createElement('converse-root');
+                el.setAttribute('id', 'conversejs');
+                u.addClass(`theme-${api.settings.get('theme')}`, el);
+                const body = root.querySelector('body');
+                if (body) {
+                    body.appendChild(el);
+                } else {
+                    root.appendChild(el); // Perhaps inside a web component?
+                }
+            }
+        }
+
+        api.listen.on('chatBoxesInitialized', ensureElement);
+    }
+});

+ 4 - 4
src/plugins/rosterview/rosterview.js

@@ -91,12 +91,12 @@ const RosterView = OrderedListView.extend({
      * contact fetched from browser storage.
      */
     updateFilter: debounce(function () {
-        this.filter_view = this.el.querySelector('converse-roster-filter');
-        const type = this.filter_view.model.get('filter_type');
+        const filter = new _converse.RosterFilter();
+        const type = filter.get('filter_type');
         if (type === 'state') {
-            this.filter(this.filter_view.model.get('chat_state'), type);
+            this.filter(filter.get('chat_state'), type);
         } else {
-            this.filter(this.filter_view.model.get('filter_text'), type);
+            this.filter(filter.get('filter_text'), type);
         }
     }, 100),
 

+ 391 - 0
src/shared/chatview.js

@@ -0,0 +1,391 @@
+import log from '@converse/headless/log';
+import tpl_chatbox_message_form from 'templates/chatbox_message_form.js';
+import tpl_toolbar from 'templates/toolbar.js';
+import { ElementView } from '@converse/skeletor/src/element.js';
+import { __ } from 'i18n';
+import { _converse, api, converse } from '@converse/headless/core';
+import { debounce } from 'lodash-es';
+import { html, render } from 'lit-html';
+
+const u = converse.env.utils;
+
+export default class BaseChatView extends ElementView {
+
+    initDebounced () {
+        this.markScrolled = debounce(this._markScrolled, 100);
+        this.debouncedScrollDown = debounce(this.scrollDown, 100);
+
+        // For tests that use Jasmine.Clock we want to turn of
+        // debouncing, since setTimeout breaks.
+        if (api.settings.get('debounced_content_rendering')) {
+            this.renderChatHistory = debounce(() => this.renderChatContent(false), 100);
+            this.renderNotifications = debounce(() => this.renderChatContent(true), 100);
+        } else {
+            this.renderChatHistory = () => this.renderChatContent(false);
+            this.renderNotifications = () => this.renderChatContent(true);
+        }
+    }
+
+    renderChatContent (msgs_by_ref = false) {
+        if (!this.tpl_chat_content) {
+            this.tpl_chat_content = o => {
+                return html`
+                    <converse-chat-content .chatview=${this} .messages=${o.messages} notifications=${o.notifications}>
+                    </converse-chat-content>
+                `;
+            };
+        }
+        const msg_models = this.model.messages.models;
+        const messages = msgs_by_ref ? msg_models : Array.from(msg_models);
+        render(this.tpl_chat_content({ messages, 'notifications': this.getNotifications() }), this.msgs_container);
+    }
+
+    renderMessageForm () {
+        const form_container = this.querySelector('.message-form-container');
+        render(
+            tpl_chatbox_message_form(
+                Object.assign(this.model.toJSON(), {
+                    'hint_value': this.querySelector('.spoiler-hint')?.value,
+                    'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'),
+                    'label_spoiler_hint': __('Optional hint'),
+                    'message_value': this.querySelector('.chat-textarea')?.value,
+                    'show_send_button': api.settings.get('show_send_button'),
+                    'show_toolbar': api.settings.get('show_toolbar'),
+                    'unread_msgs': __('You have unread messages')
+                })
+            ),
+            form_container
+        );
+        this.addEventListener('focusin', ev => this.emitFocused(ev));
+        this.addEventListener('focusout', ev => this.emitBlurred(ev));
+        this.renderToolbar();
+    }
+
+    renderToolbar () {
+        if (!api.settings.get('show_toolbar')) {
+            return this;
+        }
+        const options = Object.assign(
+            {
+                'model': this.model,
+                'chatview': this
+            },
+            this.model.toJSON(),
+            this.getToolbarOptions()
+        );
+        render(tpl_toolbar(options), this.querySelector('.chat-toolbar'));
+        /**
+         * Triggered once the _converse.ChatBoxView's toolbar has been rendered
+         * @event _converse#renderToolbar
+         * @type { _converse.ChatBoxView }
+         * @example _converse.api.listen.on('renderToolbar', view => { ... });
+         */
+        api.trigger('renderToolbar', this);
+        return this;
+    }
+
+    async getHeadingStandaloneButton (promise_or_data) { // eslint-disable-line class-methods-use-this
+        const data = await promise_or_data;
+        return html`
+            <a
+                href="#"
+                class="chatbox-btn ${data.a_class} fa ${data.icon_class}"
+                @click=${data.handler}
+                title="${data.i18n_title}"
+            ></a>
+        `;
+    }
+
+    hideNewMessagesIndicator () {
+        const new_msgs_indicator = this.querySelector('.new-msgs-indicator');
+        if (new_msgs_indicator !== null) {
+            new_msgs_indicator.classList.add('hidden');
+        }
+    }
+
+    maybeFocus () {
+        api.settings.get('auto_focus') && this.focus();
+    }
+
+    focus () {
+        const textarea_el = this.getElementsByClassName('chat-textarea')[0];
+        if (textarea_el && document.activeElement !== textarea_el) {
+            textarea_el.focus();
+        }
+        return this;
+    }
+
+    show () {
+        if (this.model.get('hidden')) {
+            log.debug(`Not showing chat ${this.model.get('jid')} because it's set as hidden`);
+            return;
+        }
+        if (u.isVisible(this)) {
+            this.maybeFocus();
+            return;
+        }
+        this.afterShown();
+    }
+
+    emitBlurred (ev) {
+        if (this.contains(document.activeElement) || this.contains(ev.relatedTarget)) {
+            // Something else in this chatbox is still focused
+            return;
+        }
+        /**
+         * Triggered when the focus has been removed from a particular chat.
+         * @event _converse#chatBoxBlurred
+         * @type { _converse.ChatBoxView | _converse.ChatRoomView }
+         * @example _converse.api.listen.on('chatBoxBlurred', (view, event) => { ... });
+         */
+        api.trigger('chatBoxBlurred', this, ev);
+    }
+
+    emitFocused (ev) {
+        if (this.contains(ev.relatedTarget)) {
+            // Something else in this chatbox was already focused
+            return;
+        }
+        /**
+         * Triggered when the focus has been moved to a particular chat.
+         * @event _converse#chatBoxFocused
+         * @type { _converse.ChatBoxView | _converse.ChatRoomView }
+         * @example _converse.api.listen.on('chatBoxFocused', (view, event) => { ... });
+         */
+        api.trigger('chatBoxFocused', this, ev);
+    }
+
+    async getHeadingDropdownItem (promise_or_data) { // eslint-disable-line class-methods-use-this
+        const data = await promise_or_data;
+        return html`
+            <a href="#" class="dropdown-item ${data.a_class}" @click=${data.handler} title="${data.i18n_title}"
+                ><i class="fa ${data.icon_class}"></i>${data.i18n_text}</a
+            >
+        `;
+    }
+
+    autocompleteInPicker (input, value) {
+        const emoji_dropdown = this.querySelector('converse-emoji-dropdown');
+        const emoji_picker = this.querySelector('converse-emoji-picker');
+        if (emoji_picker && emoji_dropdown) {
+            emoji_picker.model.set({
+                'ac_position': input.selectionStart,
+                'autocompleting': value,
+                'query': value
+            });
+            emoji_dropdown.showMenu();
+            return true;
+        }
+    }
+
+    onEmojiReceivedFromPicker (emoji) {
+        const model = this.querySelector('converse-emoji-picker').model;
+        const autocompleting = model.get('autocompleting');
+        const ac_position = model.get('ac_position');
+        this.insertIntoTextArea(emoji, autocompleting, false, ac_position);
+    }
+
+    /**
+     * Insert a particular string value into the textarea of this chat box.
+     * @private
+     * @method _converse.ChatBoxView#insertIntoTextArea
+     * @param {string} value - The value to be inserted.
+     * @param {(boolean|string)} [replace] - Whether an existing value
+     *  should be replaced. If set to `true`, the entire textarea will
+     *  be replaced with the new value. If set to a string, then only
+     *  that string will be replaced *if* a position is also specified.
+     * @param {integer} [position] - The end index of the string to be
+     * replaced with the new value.
+     */
+    insertIntoTextArea (value, replace = false, correcting = false, position) {
+        const textarea = this.querySelector('.chat-textarea');
+        if (correcting) {
+            u.addClass('correcting', textarea);
+        } else {
+            u.removeClass('correcting', textarea);
+        }
+        if (replace) {
+            if (position && typeof replace == 'string') {
+                textarea.value = textarea.value.replace(new RegExp(replace, 'g'), (match, offset) =>
+                    offset == position - replace.length ? value + ' ' : match
+                );
+            } else {
+                textarea.value = value;
+            }
+        } else {
+            let existing = textarea.value;
+            if (existing && existing[existing.length - 1] !== ' ') {
+                existing = existing + ' ';
+            }
+            textarea.value = existing + value + ' ';
+        }
+        this.updateCharCounter(textarea.value);
+        u.placeCaretAtEnd(textarea);
+    }
+
+    /**
+     * Called when the chat content is scrolled up or down.
+     * We want to record when the user has scrolled away from
+     * the bottom, so that we don't automatically scroll away
+     * from what the user is reading when new messages are received.
+     *
+     * Don't call this method directly, instead, call `markScrolled`,
+     * which debounces this method by 100ms.
+     * @private
+     */
+    _markScrolled (ev) {
+        let scrolled = true;
+        let scrollTop = null;
+        const is_at_bottom =
+            this.msgs_container.scrollTop + this.msgs_container.clientHeight >= this.msgs_container.scrollHeight - 62; // sigh...
+
+        if (is_at_bottom) {
+            scrolled = false;
+            this.onScrolledDown();
+        } else if (this.msgs_container.scrollTop === 0) {
+            /**
+             * Triggered once the chat's message area has been scrolled to the top
+             * @event _converse#chatBoxScrolledUp
+             * @property { _converse.ChatBoxView | _converse.ChatRoomView } view
+             * @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
+             */
+            api.trigger('chatBoxScrolledUp', this);
+        } else {
+            scrollTop = ev.target.scrollTop;
+        }
+        u.safeSave(this.model, { scrolled, scrollTop });
+    }
+
+    /**
+     * Scrolls the chat down.
+     *
+     * This method will always scroll the chat down, regardless of
+     * whether the user scrolled up manually or not.
+     * @param { Event } [ev] - An optional event that is the cause for needing to scroll down.
+     */
+    scrollDown (ev) {
+        ev?.preventDefault?.();
+        ev?.stopPropagation?.();
+        if (this.model.get('scrolled')) {
+            u.safeSave(this.model, {
+                'scrolled': false,
+                'scrollTop': null
+            });
+        }
+        if (this.msgs_container.scrollTo) {
+            const behavior = this.msgs_container.scrollTop ? 'smooth' : 'auto';
+            this.msgs_container.scrollTo({ 'top': this.msgs_container.scrollHeight, behavior });
+        } else {
+            this.msgs_container.scrollTop = this.msgs_container.scrollHeight;
+        }
+        this.onScrolledDown();
+    }
+
+    onScrolledDown () {
+        this.hideNewMessagesIndicator();
+        if (!this.model.isHidden()) {
+            this.model.clearUnreadMsgCounter();
+            // Clear location hash if set to one of the messages in our history
+            const hash = window.location.hash;
+            hash && this.model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
+        }
+        /**
+         * Triggered once the chat's message area has been scrolled down to the bottom.
+         * @event _converse#chatBoxScrolledDown
+         * @type {object}
+         * @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
+         * @example _converse.api.listen.on('chatBoxScrolledDown', obj => { ... });
+         */
+        api.trigger('chatBoxScrolledDown', { 'chatbox': this.model }); // TODO: clean up
+    }
+
+    onWindowStateChanged (state) {
+        if (state === 'visible') {
+            if (!this.model.isHidden() && this.model.get('num_unread', 0)) {
+                this.model.clearUnreadMsgCounter();
+            }
+        } else if (state === 'hidden') {
+            this.model.setChatState(_converse.INACTIVE, { 'silent': true });
+            this.model.sendChatState();
+        }
+    }
+
+    async onFormSubmitted (ev) {
+        ev.preventDefault();
+        const textarea = this.querySelector('.chat-textarea');
+        const message_text = textarea.value.trim();
+        if (
+            (api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit')) ||
+            !message_text.replace(/\s/g, '').length
+        ) {
+            return;
+        }
+        if (!_converse.connection.authenticated) {
+            const err_msg = __('Sorry, the connection has been lost, and your message could not be sent');
+            api.alert('error', __('Error'), err_msg);
+            api.connection.reconnect();
+            return;
+        }
+        let spoiler_hint,
+            hint_el = {};
+        if (this.model.get('composing_spoiler')) {
+            hint_el = this.querySelector('form.sendXMPPMessage input.spoiler-hint');
+            spoiler_hint = hint_el.value;
+        }
+        u.addClass('disabled', textarea);
+        textarea.setAttribute('disabled', 'disabled');
+        this.querySelector('converse-emoji-dropdown')?.hideMenu();
+
+        const is_command = this.parseMessageForCommands(message_text);
+        const message = is_command ? null : await this.model.sendMessage(message_text, spoiler_hint);
+        if (is_command || message) {
+            hint_el.value = '';
+            textarea.value = '';
+            u.removeClass('correcting', textarea);
+            textarea.style.height = 'auto';
+            this.updateCharCounter(textarea.value);
+        }
+        if (message) {
+            /**
+             * Triggered whenever a message is sent by the user
+             * @event _converse#messageSend
+             * @type { _converse.Message }
+             * @example _converse.api.listen.on('messageSend', message => { ... });
+             */
+            api.trigger('messageSend', message);
+        }
+        if (api.settings.get('view_mode') === 'overlayed') {
+            // XXX: Chrome flexbug workaround. The .chat-content area
+            // doesn't resize when the textarea is resized to its original size.
+            this.msgs_container.parentElement.style.display = 'none';
+        }
+        textarea.removeAttribute('disabled');
+        u.removeClass('disabled', textarea);
+
+        if (api.settings.get('view_mode') === 'overlayed') {
+            // XXX: Chrome flexbug workaround.
+            this.msgs_container.parentElement.style.display = '';
+        }
+        // Suppress events, otherwise superfluous CSN gets set
+        // immediately after the message, causing rate-limiting issues.
+        this.model.setChatState(_converse.ACTIVE, { 'silent': true });
+        textarea.focus();
+    }
+
+    onEnterPressed (ev) {
+        return this.onFormSubmitted(ev);
+    }
+
+    updateCharCounter (chars) {
+        if (api.settings.get('message_limit')) {
+            const message_limit = this.querySelector('.message-limit');
+            const counter = api.settings.get('message_limit') - chars.length;
+            message_limit.textContent = counter;
+            if (counter < 1) {
+                u.addClass('error', message_limit);
+            } else {
+                u.removeClass('error', message_limit);
+            }
+        }
+    }
+}

+ 10 - 4
src/templates/avatar.js

@@ -4,7 +4,13 @@ const getImgHref = (image, image_type) => {
     return image.startsWith('data:') ? image : `data:${image_type};base64,${image}`;
 }
 
-export default  (o) => html`
-    <svg xmlns="http://www.w3.org/2000/svg" class="avatar ${o.classes}" width="${o.width}" height="${o.height}">
-        <image width="${o.width}" height="${o.height}" preserveAspectRatio="xMidYMid meet" href="${getImgHref(o.image, o.image_type)}"/>
-    </svg>`;
+export default  (o) => {
+    if (o.image) {
+        return html`
+                <svg xmlns="http://www.w3.org/2000/svg" class="avatar ${o.classes}" width="${o.width}" height="${o.height}">
+                    <image width="${o.width}" height="${o.height}" preserveAspectRatio="xMidYMid meet" href="${getImgHref(o.image, o.image_type)}"/>
+                </svg>`;
+    } else {
+        return '';
+    }
+}

+ 11 - 7
src/templates/converse.js

@@ -1,9 +1,13 @@
-import { html } from 'lit-html';
 import '../components/font-awesome.js';
+import { api } from '@converse/headless/core';
+import { html } from 'lit-html';
 
-
-export default () => html`
-    <div class="converse-chatboxes row no-gutters"></div>
-    <div id="converse-modals" class="modals"></div>
-    <converse-fontawesome></converse-fontawesome>
-`;
+export default () => {
+    let extra_classes = api.settings.get('singleton') ? 'converse-singleton' : '';
+    extra_classes += `converse-${api.settings.get('view_mode')}`;
+    return html`
+        <converse-chats class="converse-chatboxes row no-gutters ${extra_classes}"></converse-chats>
+        <div id="converse-modals" class="modals"></div>
+        <converse-fontawesome></converse-fontawesome>
+    `;
+};

部分文件因为文件数量过多而无法显示