Browse Source

Re-add `xhr_user_search_url` and autocomplete when adding contacts

JC Brand 7 years ago
parent
commit
1e7c41ebfd

+ 1 - 1
.eslintrc.json

@@ -149,7 +149,7 @@
         "no-negated-condition": "off",
         "no-negated-in-lhs": "error",
         "no-nested-ternary": "off",
-        "no-new": "error",
+        "no-new": "off",
         "no-new-func": "error",
         "no-new-object": "error",
         "no-new-require": "error",

+ 1 - 1
.travis.yml

@@ -7,7 +7,7 @@ addons:
     chrome: stable
 node_js:
  - 6
-install: make node_modules
+install: make stamp-npm
 before_script: make serve_bg
 script: make check
 sudo: false

+ 6 - 16
CHANGES.md

@@ -2,29 +2,19 @@
 
 ## 4.0.0 (Unreleased)
 
-## Removed configuration settings
+## UI changes
 
-Due to rewriting parts of the code, we regrettably had to remove certain
-lesser-used configuration settings because the cost of adding them to the
-new code was too high.
+The UI is now based on Bootstrap4 and Flexbox is used extensively.
 
-If you relied on any of these settings, you can reproduce their
-functionality in your own 3rd party plugins, or you can [contact us](http://opkode.com/contact.html)
-with regards to sponsoring development on reintroducing them.
+## Configuration changes 
 
 * Removed the `xhr_custom_status` and `xhr_custom_status_url` configuration
   settings. If you relied on these settings, you can instead listen for the
   [statusMessageChanged](https://conversejs.org/docs/html/events.html#contactstatusmessagechanged)
   event and make the XMLHttpRequest yourself.
-* Removed the `xhr_user_search` and `xhr_user_search_url` configuration options.
-
-## Updated UI
-
-The UI is now rewritten with Bootstrap4 and Flexbox is used pretty much
-everywhere. Unfortunately this means that in the overlayed view_mode, chat
-boxes can no longer be resized horizontally (or diagonally). Perhaps a solution
-for this can again be found, but time constraints meant that this feature had
-to be removed.
+* Removed  `xhr_user_search` in favor of only accepting `xhr_user_search_url` as configuration option.
+* The data returned from the `xhr_user_search_url` must now include the user's
+  `jid` instead of just an `id`.
 
 ### Bugfixes
 

+ 68 - 67
css/converse.css

@@ -8586,20 +8586,23 @@ body.reset {
   #conversejs:not(.fullscreen) #minimized-chats .chat-head-message-count-hidden {
     display: none; }
 
-#converse-embedded-chat,
-#conversejs {
-  /* Pointer */ }
-  #converse-embedded-chat [hidden],
-  #conversejs [hidden] {
-    display: none; }
-  #converse-embedded-chat .visually-hidden,
-  #conversejs .visually-hidden {
-    position: absolute;
-    clip: rect(0, 0, 0, 0); }
-  #converse-embedded-chat div.awesomplete,
-  #conversejs div.awesomplete {
-    display: inline-block;
-    position: relative; }
+#converse-embedded-chat [hidden],
+#conversejs [hidden] {
+  display: none; }
+#converse-embedded-chat .visually-hidden,
+#conversejs .visually-hidden {
+  position: absolute;
+  clip: rect(0, 0, 0, 0); }
+#converse-embedded-chat .form-group .awesomplete,
+#conversejs .form-group .awesomplete {
+  width: 100%; }
+#converse-embedded-chat div.awesomplete,
+#conversejs div.awesomplete {
+  display: inline-block;
+  position: relative; }
+  #converse-embedded-chat div.awesomplete mark,
+  #conversejs div.awesomplete mark {
+    background: #FFB9A7; }
   #converse-embedded-chat div.awesomplete > input,
   #conversejs div.awesomplete > input {
     display: block; }
@@ -8620,62 +8623,60 @@ body.reset {
     border: 1px solid rgba(0, 0, 0, 0.3);
     box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2);
     text-shadow: none; }
+    #converse-embedded-chat div.awesomplete > ul:before,
+    #conversejs div.awesomplete > ul:before {
+      content: "";
+      position: absolute;
+      top: -.43em;
+      left: 1em;
+      width: 0;
+      height: 0;
+      background: white;
+      border: inherit;
+      border-right: 0;
+      border-bottom: 0;
+      -webkit-transform: rotate(45deg);
+      transform: rotate(45deg); }
+    #converse-embedded-chat div.awesomplete > ul > li,
+    #conversejs div.awesomplete > ul > li {
+      text-overflow: ellipsis;
+      overflow-x: hidden;
+      position: relative;
+      cursor: pointer;
+      padding: 1em; }
+#converse-embedded-chat div.awesomplete > ul[hidden],
+#converse-embedded-chat div.awesomplete > ul:empty,
+#conversejs div.awesomplete > ul[hidden],
+#conversejs div.awesomplete > ul:empty {
+  display: none; }
+@supports (transform: scale(0)) {
+  #converse-embedded-chat div.awesomplete > ul,
+  #conversejs div.awesomplete > ul {
+    transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
+    transform-origin: 1.43em -.43em; }
   #converse-embedded-chat div.awesomplete > ul[hidden],
   #converse-embedded-chat div.awesomplete > ul:empty,
   #conversejs div.awesomplete > ul[hidden],
   #conversejs div.awesomplete > ul:empty {
-    display: none; }
-  @supports (transform: scale(0)) {
-    #converse-embedded-chat div.awesomplete > ul,
-    #conversejs div.awesomplete > ul {
-      transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
-      transform-origin: 1.43em -.43em; }
-    #converse-embedded-chat div.awesomplete > ul[hidden],
-    #converse-embedded-chat div.awesomplete > ul:empty,
-    #conversejs div.awesomplete > ul[hidden],
-    #conversejs div.awesomplete > ul:empty {
-      opacity: 0;
-      transform: scale(0);
-      display: block;
-      transition-timing-function: ease; } }
-  #converse-embedded-chat div.awesomplete > ul:before,
-  #conversejs div.awesomplete > ul:before {
-    content: "";
-    position: absolute;
-    top: -.43em;
-    left: 1em;
-    width: 0;
-    height: 0;
-    background: white;
-    border: inherit;
-    border-right: 0;
-    border-bottom: 0;
-    -webkit-transform: rotate(45deg);
-    transform: rotate(45deg); }
-  #converse-embedded-chat div.awesomplete > ul > li,
-  #conversejs div.awesomplete > ul > li {
-    text-overflow: ellipsis;
-    overflow-x: hidden;
-    position: relative;
-    cursor: pointer; }
-  #converse-embedded-chat div.awesomplete > ul > li:hover,
-  #conversejs div.awesomplete > ul > li:hover {
-    background: #E77051;
-    color: white; }
-  #converse-embedded-chat div.awesomplete > ul > li[aria-selected="true"],
-  #conversejs div.awesomplete > ul > li[aria-selected="true"] {
-    background: #3d6d8f;
-    color: white; }
-  #converse-embedded-chat div.awesomplete mark,
-  #conversejs div.awesomplete mark {
-    background: #FFB9A7; }
-  #converse-embedded-chat div.awesomplete li:hover mark,
-  #conversejs div.awesomplete li:hover mark {
-    background: #A53214;
-    color: white; }
-  #converse-embedded-chat div.awesomplete li[aria-selected="true"] mark,
-  #conversejs div.awesomplete li[aria-selected="true"] mark {
-    background: #3d6b00;
-    color: inherit; }
+    opacity: 0;
+    transform: scale(0);
+    display: block;
+    transition-timing-function: ease; } }
+#converse-embedded-chat div.awesomplete > ul > li:hover,
+#conversejs div.awesomplete > ul > li:hover {
+  background: #E77051;
+  color: white; }
+#converse-embedded-chat div.awesomplete > ul > li[aria-selected="true"],
+#conversejs div.awesomplete > ul > li[aria-selected="true"] {
+  background: #3d6d8f;
+  color: white; }
+#converse-embedded-chat div.awesomplete li:hover mark,
+#conversejs div.awesomplete li:hover mark {
+  background: #A53214;
+  color: white; }
+#converse-embedded-chat div.awesomplete li[aria-selected="true"] mark,
+#conversejs div.awesomplete li[aria-selected="true"] mark {
+  background: #3d6b00;
+  color: inherit; }
 
 /*# sourceMappingURL=converse.css.map */

+ 68 - 67
css/inverse.css

@@ -8698,20 +8698,23 @@ body {
   border: 1.2em solid #E7A151;
   border-top: 0.8em solid #E7A151; }
 
-#converse-embedded-chat,
-#conversejs {
-  /* Pointer */ }
-  #converse-embedded-chat [hidden],
-  #conversejs [hidden] {
-    display: none; }
-  #converse-embedded-chat .visually-hidden,
-  #conversejs .visually-hidden {
-    position: absolute;
-    clip: rect(0, 0, 0, 0); }
-  #converse-embedded-chat div.awesomplete,
-  #conversejs div.awesomplete {
-    display: inline-block;
-    position: relative; }
+#converse-embedded-chat [hidden],
+#conversejs [hidden] {
+  display: none; }
+#converse-embedded-chat .visually-hidden,
+#conversejs .visually-hidden {
+  position: absolute;
+  clip: rect(0, 0, 0, 0); }
+#converse-embedded-chat .form-group .awesomplete,
+#conversejs .form-group .awesomplete {
+  width: 100%; }
+#converse-embedded-chat div.awesomplete,
+#conversejs div.awesomplete {
+  display: inline-block;
+  position: relative; }
+  #converse-embedded-chat div.awesomplete mark,
+  #conversejs div.awesomplete mark {
+    background: #FFB9A7; }
   #converse-embedded-chat div.awesomplete > input,
   #conversejs div.awesomplete > input {
     display: block; }
@@ -8732,62 +8735,60 @@ body {
     border: 1px solid rgba(0, 0, 0, 0.3);
     box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2);
     text-shadow: none; }
+    #converse-embedded-chat div.awesomplete > ul:before,
+    #conversejs div.awesomplete > ul:before {
+      content: "";
+      position: absolute;
+      top: -.43em;
+      left: 1em;
+      width: 0;
+      height: 0;
+      background: white;
+      border: inherit;
+      border-right: 0;
+      border-bottom: 0;
+      -webkit-transform: rotate(45deg);
+      transform: rotate(45deg); }
+    #converse-embedded-chat div.awesomplete > ul > li,
+    #conversejs div.awesomplete > ul > li {
+      text-overflow: ellipsis;
+      overflow-x: hidden;
+      position: relative;
+      cursor: pointer;
+      padding: 1em; }
+#converse-embedded-chat div.awesomplete > ul[hidden],
+#converse-embedded-chat div.awesomplete > ul:empty,
+#conversejs div.awesomplete > ul[hidden],
+#conversejs div.awesomplete > ul:empty {
+  display: none; }
+@supports (transform: scale(0)) {
+  #converse-embedded-chat div.awesomplete > ul,
+  #conversejs div.awesomplete > ul {
+    transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
+    transform-origin: 1.43em -.43em; }
   #converse-embedded-chat div.awesomplete > ul[hidden],
   #converse-embedded-chat div.awesomplete > ul:empty,
   #conversejs div.awesomplete > ul[hidden],
   #conversejs div.awesomplete > ul:empty {
-    display: none; }
-  @supports (transform: scale(0)) {
-    #converse-embedded-chat div.awesomplete > ul,
-    #conversejs div.awesomplete > ul {
-      transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
-      transform-origin: 1.43em -.43em; }
-    #converse-embedded-chat div.awesomplete > ul[hidden],
-    #converse-embedded-chat div.awesomplete > ul:empty,
-    #conversejs div.awesomplete > ul[hidden],
-    #conversejs div.awesomplete > ul:empty {
-      opacity: 0;
-      transform: scale(0);
-      display: block;
-      transition-timing-function: ease; } }
-  #converse-embedded-chat div.awesomplete > ul:before,
-  #conversejs div.awesomplete > ul:before {
-    content: "";
-    position: absolute;
-    top: -.43em;
-    left: 1em;
-    width: 0;
-    height: 0;
-    background: white;
-    border: inherit;
-    border-right: 0;
-    border-bottom: 0;
-    -webkit-transform: rotate(45deg);
-    transform: rotate(45deg); }
-  #converse-embedded-chat div.awesomplete > ul > li,
-  #conversejs div.awesomplete > ul > li {
-    text-overflow: ellipsis;
-    overflow-x: hidden;
-    position: relative;
-    cursor: pointer; }
-  #converse-embedded-chat div.awesomplete > ul > li:hover,
-  #conversejs div.awesomplete > ul > li:hover {
-    background: #E77051;
-    color: white; }
-  #converse-embedded-chat div.awesomplete > ul > li[aria-selected="true"],
-  #conversejs div.awesomplete > ul > li[aria-selected="true"] {
-    background: #3d6d8f;
-    color: white; }
-  #converse-embedded-chat div.awesomplete mark,
-  #conversejs div.awesomplete mark {
-    background: #FFB9A7; }
-  #converse-embedded-chat div.awesomplete li:hover mark,
-  #conversejs div.awesomplete li:hover mark {
-    background: #A53214;
-    color: white; }
-  #converse-embedded-chat div.awesomplete li[aria-selected="true"] mark,
-  #conversejs div.awesomplete li[aria-selected="true"] mark {
-    background: #3d6b00;
-    color: inherit; }
+    opacity: 0;
+    transform: scale(0);
+    display: block;
+    transition-timing-function: ease; } }
+#converse-embedded-chat div.awesomplete > ul > li:hover,
+#conversejs div.awesomplete > ul > li:hover {
+  background: #E77051;
+  color: white; }
+#converse-embedded-chat div.awesomplete > ul > li[aria-selected="true"],
+#conversejs div.awesomplete > ul > li[aria-selected="true"] {
+  background: #3d6d8f;
+  color: white; }
+#converse-embedded-chat div.awesomplete li:hover mark,
+#conversejs div.awesomplete li:hover mark {
+  background: #A53214;
+  color: white; }
+#converse-embedded-chat div.awesomplete li[aria-selected="true"] mark,
+#conversejs div.awesomplete li[aria-selected="true"] mark {
+  background: #3d6b00;
+  color: inherit; }
 
 /*# sourceMappingURL=inverse.css.map */

+ 35 - 0
docs/source/configuration.rst

@@ -1535,3 +1535,38 @@ Example:
             whitelisted_plugins: ['myplugin']
         });
     });
+
+
+xhr_user_search_url
+-------------------
+
+.. note::
+    XHR stands for XMLHTTPRequest, and is meant here in the AJAX sense (Asynchronous JavaScript and XML).
+
+* Default: ``null``
+
+There are two ways to add users.
+
+* The user inputs a valid JID (Jabber ID, aka XMPP address), and the user is added as a pending contact.
+* The user inputs some text (for example part of a first name or last name),
+  an XHR (Ajax Request) will be made to a remote server, and a list of matches are returned.
+  The user can then choose one of the matches to add as a contact.
+
+By providing an XHR search URL, you're enabling the second mechanism.
+
+*What is expected from the remote server?*
+
+A default JSON encoded list of objects must be returned. Each object
+corresponds to a matched user and needs the keys ``jid`` and ``fullname``.
+
+.. code-block:: javascript
+
+    [{"jid": "marty@mcfly.net", "fullname": "Marty McFly"}, {"jid": "doc@brown.com", "fullname": "Doc Brown"}]
+
+.. note::
+    Make sure your server script sets the header `Content-Type: application/json`.
+
+This is the URL to which an XHR GET request will be made to fetch user data from your remote server.
+The query string will be included in the request with ``q`` as its key.
+
+The data returned must be a JSON encoded list of user JIDs.

+ 51 - 46
sass/_awesomplete.scss

@@ -7,31 +7,62 @@
         clip: rect(0, 0, 0, 0);
     }
 
+    .form-group {
+        .awesomplete {
+            width: 100%;
+        }
+    }
+
     div.awesomplete {
         display: inline-block;
         position: relative;
-    }
+        mark {
+            background: $lightest-red;
+        }
 
-    div.awesomplete > input {
-        display: block;
-    }
+        > input {
+            display: block;
+        }
 
-    div.awesomplete > ul {
-        position: absolute;
-        left: 0;
-        right: 0;
-        z-index: 1;
-        min-width: 100%;
-        box-sizing: border-box;
-        list-style: none;
-        padding: 0;
-        border-radius: .3em;
-        margin: .2em 0 0;
-        background: hsla(0,0%,100%,.9);
-        background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.8));
-        border: 1px solid rgba(0,0,0,.3);
-        box-shadow: .05em .2em .6em rgba(0,0,0,.2);
-        text-shadow: none;
+        > ul {
+            &:before {
+                content: "";
+                position: absolute;
+                top: -.43em;
+                left: 1em;
+                width: 0; height: 0;
+                background: white;
+                border: inherit;
+                border-right: 0;
+                border-bottom: 0;
+                -webkit-transform: rotate(45deg);
+                transform: rotate(45deg);
+            }
+
+            position: absolute;
+            left: 0;
+            right: 0;
+            z-index: 1;
+            min-width: 100%;
+            box-sizing: border-box;
+            list-style: none;
+            padding: 0;
+            border-radius: .3em;
+            margin: .2em 0 0;
+            background: hsla(0,0%,100%,.9);
+            background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.8));
+            border: 1px solid rgba(0,0,0,.3);
+            box-shadow: .05em .2em .6em rgba(0,0,0,.2);
+            text-shadow: none;
+
+            > li {
+                text-overflow: ellipsis;
+                overflow-x: hidden;
+                position: relative;
+                cursor: pointer;
+                padding: 1em;
+            }
+        }
     }
 
     div.awesomplete > ul[hidden],
@@ -53,28 +84,6 @@
             transition-timing-function: ease;
         }
     }
-
-    /* Pointer */
-    div.awesomplete > ul:before {
-        content: "";
-        position: absolute;
-        top: -.43em;
-        left: 1em;
-        width: 0; height: 0;
-        background: white;
-        border: inherit;
-        border-right: 0;
-        border-bottom: 0;
-        -webkit-transform: rotate(45deg);
-        transform: rotate(45deg);
-    }
-
-    div.awesomplete > ul > li {
-        text-overflow: ellipsis;
-        overflow-x: hidden;
-        position: relative;
-        cursor: pointer;
-    }
     
     div.awesomplete > ul > li:hover {
         background: $red;
@@ -86,10 +95,6 @@
         color: white;
     }
     
-    div.awesomplete mark {
-        background: $lightest-red;
-    }
-    
     div.awesomplete li:hover mark {
         background: $darkest-red;
         color: $inverse-link-color;

+ 2 - 10
spec/chatroom.js

@@ -1353,20 +1353,12 @@
                 $(view.el).find('.chat-area').remove();
 
                 test_utils.waitUntil(function () {
-                        return $(view.el).find('input.invited-contact').length;
+                    return $(view.el).find('input.invited-contact').length;
                 }, 300).then(function () {
                     var $input = $(view.el).find('input.invited-contact');
                     expect($input.attr('placeholder')).toBe('Invite');
                     $input.val("Felix");
-                    var evt;
-                    // check if Event() is a constructor function
-                    // usage as per the spec, if true
-                    if (typeof(Event) === 'function') {
-                        evt = new Event('input');
-                    } else { // the deprecated way for PhantomJS
-                        evt = document.createEvent('CustomEvent');
-                        evt.initCustomEvent('input', false, false, null);
-                    }
+                    var evt = new Event('input');
                     $input[0].dispatchEvent(evt);
 
                     var sent_stanza;

+ 74 - 23
spec/controlbox.js

@@ -33,6 +33,37 @@
 
         describe("The \"Contacts\" section", function () {
 
+            it("can be used to add contact and it checks for case-sensivity", 
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched'], {},
+                    function (done, _converse) {
+
+                spyOn(_converse, 'emit');
+                spyOn(_converse.rosterview, 'update').and.callThrough();
+                test_utils.openControlBox();
+                // Adding two contacts one with Capital initials and one with small initials of same JID (Case sensitive check)
+                _converse.roster.create({
+                    jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
+                    subscription: 'none',
+                    ask: 'subscribe',
+                    fullname: mock.pend_names[0]
+                });
+                _converse.roster.create({
+                    jid: mock.pend_names[0].replace(/ /g,'.') + '@localhost',
+                    subscription: 'none',
+                    ask: 'subscribe',
+                    fullname: mock.pend_names[0]
+                });
+                test_utils.waitUntil(function () {
+                    return $(_converse.rosterview.el).find('.roster-group li:visible').length;
+                }, 700).then(function () {
+                    // Checking that only one entry is created because both JID is same (Case sensitive check)
+                    expect($(_converse.rosterview.el).find('li:visible').length).toBe(1);
+                    expect(_converse.rosterview.update).toHaveBeenCalled();
+                    done();
+                });
+            }));
+
             it("shows the number of unread mentions received",
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
@@ -157,6 +188,8 @@
                 null, ['rosterGroupsFetched'], {},
                 function (done, _converse) {
 
+            test_utils.createContacts(_converse, 'all').openControlBox();
+
             var panel = _converse.chatboxviews.get('controlbox').contactspanel;
             var cbview = _converse.chatboxviews.get('controlbox');
             cbview.el.querySelector('.add-contact').click()
@@ -165,37 +198,55 @@
                 return u.isVisible(modal.el);
             }, 1000).then(function () {
                 expect(!_.isNull(modal.el.querySelector('form.add-xmpp-contact'))).toBeTruthy();
+                var input_el = modal.el.querySelector('input[name="jid"]');
+                input_el.value = 'someone@';
+                var evt = new Event('input');
+                input_el.dispatchEvent(evt);
+                expect(modal.el.querySelector('.awesomplete li').textContent).toBe('someone@localhost');
                 done();
             });
         }));
 
-        it("can be used to add contact and it checks for case-sensivity", 
+
+        it("integrates with xhr_user_search_url to search for contacts", 
             mock.initConverseWithPromises(
-                null, ['rosterGroupsFetched'], {},
+                null, ['rosterGroupsFetched'],
+                { 'xhr_user_search': true,
+                  'xhr_user_search_url': 'http://example.org/'
+                },
                 function (done, _converse) {
 
-            spyOn(_converse, 'emit');
-            spyOn(_converse.rosterview, 'update').and.callThrough();
-            test_utils.openControlBox();
-            // Adding two contacts one with Capital initials and one with small initials of same JID (Case sensitive check)
-            _converse.roster.create({
-                jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
-                subscription: 'none',
-                ask: 'subscribe',
-                fullname: mock.pend_names[0]
-            });
-            _converse.roster.create({
-                jid: mock.pend_names[0].replace(/ /g,'.') + '@localhost',
-                subscription: 'none',
-                ask: 'subscribe',
-                fullname: mock.pend_names[0]
+            var xhr = {
+                'open': _.noop,
+                'send': function () {
+                    xhr.responseText = JSON.stringify([
+                        {"jid": "marty@mcfly.net", "fullname": "Marty McFly"},
+                        {"jid": "doc@brown.com", "fullname": "Doc Brown"}
+                    ]);
+                    xhr.onload();
+                }
+            };
+            window.XMLHttpRequest = jasmine.createSpy('XMLHttpRequest');
+            XMLHttpRequest.and.callFake(function () {
+                return xhr;
             });
-            test_utils.waitUntil(function () {
-                return $(_converse.rosterview.el).find('.roster-group li:visible').length;
-            }, 700).then(function () {
-                // Checking that only one entry is created because both JID is same (Case sensitive check)
-                expect($(_converse.rosterview.el).find('li:visible').length).toBe(1);
-                expect(_converse.rosterview.update).toHaveBeenCalled();
+
+            var panel = _converse.chatboxviews.get('controlbox').contactspanel;
+            var cbview = _converse.chatboxviews.get('controlbox');
+            cbview.el.querySelector('.add-contact').click()
+            var modal = _converse.rosterview.add_contact_modal;
+            return test_utils.waitUntil(function () {
+                return u.isVisible(modal.el);
+            }, 1000).then(function () {
+                var input_el = modal.el.querySelector('input[name="jid"]');
+                input_el.value = 'marty@';
+                var evt = new Event('input');
+                input_el.dispatchEvent(evt);
+                return test_utils.waitUntil(function () {
+                    return modal.el.querySelector('.awesomplete li');
+                });
+            }).then(function () {
+                expect(modal.el.querySelector('.awesomplete li').textContent).toBe('marty@mcfly.net');
                 done();
             });
         }));

+ 32 - 4
src/converse-rosterview.js

@@ -16,6 +16,7 @@
             "tpl!roster_filter",
             "tpl!roster_item",
             "tpl!search_contact",
+            "awesomplete",
             "converse-chatboxes",
             "converse-modal"
     ], factory);
@@ -28,7 +29,8 @@
             tpl_roster,
             tpl_roster_filter,
             tpl_roster_item,
-            tpl_search_contact
+            tpl_search_contact,
+            Awesomplete
     ) {
     "use strict";
     const { Backbone, Strophe, $iq, b64_sha1, sizzle, _ } = converse.env;
@@ -78,9 +80,10 @@
                   { __ } = _converse;
 
             _converse.api.settings.update({
-                allow_chat_pending_contacts: true,
-                allow_contact_removal: true,
-                show_toolbar: true,
+                'allow_chat_pending_contacts': true,
+                'allow_contact_removal': true,
+                'show_toolbar': true,
+                'xhr_user_search_url': null
             });
             _converse.api.promises.add('rosterViewInitialized');
 
@@ -147,6 +150,31 @@
                     }));
                 },
 
+                afterRender () {
+                    const input_el = this.el.querySelector('input[name="jid"]');
+                    if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
+                        const awesomplete = new Awesomplete(input_el, {'list': [], 'minChars': 2});
+                        const xhr = new window.XMLHttpRequest();
+                        // `open` must be called after `onload` for
+                        // mock/testing purposes.
+                        xhr.onload = function () {
+                            awesomplete.list = JSON.parse(xhr.responseText).map((i) => i.jid);
+                            awesomplete.evaluate();
+                        };
+                        xhr.open("GET", _converse.xhr_user_search_url, true);
+                        input_el.addEventListener('input', _.debounce(() => xhr.send()), 100, {'leading': true});
+                    } else {
+                        const list = _.uniq(_converse.roster.map((item) => Strophe.getDomainFromJid(item.get('jid'))));
+                        new Awesomplete(input_el, {
+                            'list': list,
+                            'data': function (text, input) {
+                                return input.slice(0, input.indexOf("@")) + "@" + text;
+                            },
+                            'filter': Awesomplete.FILTER_STARTSWITH
+                        });
+                    }
+                },
+
                 addContactFromForm (ev) {
                     ev.preventDefault();
                     const data = new FormData(ev.target),

+ 0 - 11
src/templates/add_contact_form.html

@@ -1,11 +0,0 @@
-<form class="pure-form add-xmpp-contact">
-    {[ if (o.error_message) { ]}
-        <span class="pure-form-message error">{{{o.error_message}}}</span>
-    {[ } ]}
-    <input type="text"
-        name="identifier"
-        value="{{{o.value}}}"
-        class="username {[ if (o.error_message) { ]} error {[ } ]}"
-        placeholder="{{{o.label_contact_username}}}"/>
-    <button class="btn btn-primary" type="submit">{{{o.label_add}}}</button>
-</form>

+ 1 - 1
src/templates/add_contact_modal.html

@@ -9,7 +9,7 @@
             <form class="converse-form add-xmpp-contact">
                 <div class="modal-body">
                     <div class="form-group">
-                        <label for="jid">{{{o.label_xmpp_address}}}:</label>
+                        <label class="clearfix" for="jid">{{{o.label_xmpp_address}}}:</label>
                         <input type="text" name="jid" required="required" value="{{{o.jid}}}"
                                class="form-control {[ if (o.error_message) { ]} is-invalid {[ } ]}"
                                placeholder="{{{o.contact_placeholder}}}">

+ 1 - 1
src/templates/chatroom_invite.html

@@ -1,6 +1,6 @@
 <form class="room-invite">
     {[ if (o.error_message) { ]}
-        <span class="pure-form-message error">{{{o.error_message}}}</span>
+        <span class="error">{{{o.error_message}}}</span>
     {[ } ]}
     <input class="form-control invited-contact" placeholder="{{{o.label_invitation}}}" type="text"/>
 </form>