Merge m-c to fx-team, a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 18 Aug 2015 17:59:17 -0700
changeset 290966 e103e721ded39cc22cbf67fea58acd4e9a8f51b0
parent 290965 1c10c8aed80d36a450ca39cdd80ab76b871da53e (diff)
parent 290837 f384789a29dcfd514d25d4a16a97ec5309612d78 (current diff)
child 290967 3658808a325abf5c0dc29ac9d4c20a6d25a57c8a
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to fx-team, a=merge
--- a/browser/components/loop/content/css/contacts.css
+++ b/browser/components/loop/content/css/contacts.css
@@ -12,27 +12,71 @@ html {
 }
 
 .contact-import-spinner.busy {
   display: inline-block;
   vertical-align: middle;
   -moz-margin-start: 10px;
 }
 
-.content-area input.contact-filter {
-  margin-top: 14px;
-  border-radius: 10000px;
+.contact-filter-container {
+  display: flex;
+  height: 2em;
+}
+
+.contact-filter {
+  margin: 0;
+  -moz-padding-start: 34px;
+  width: 100%;
+  height: 28px;
+  border: 0;
+  border-bottom: 1px solid #ddd;
+  background-image: url("../shared/img/icons-14x14.svg#magnifier");
+  background-position: 10px center;
+  background-size: 14px;
+  background-repeat: no-repeat;
+  color: #999;
+  font-size: 1.2rem;
+  flex: 2 1 auto;
+  align-self: stretch;
+}
+
+html[dir="rtl"] .contact-filter {
+  background-position: right 10px center;
+}
+
+.contact-filter:focus + .clear-search,
+.contact-filter:focus {
+  border-bottom: 1px solid #5cccee;
+  color: #4a4a4a;
+}
+
+.clear-search {
+  width: 34px;
+  height: 28px;
+  border: none;
+  border-bottom: 1px solid #ddd;
+  background-color: #fff;
+  background-image: url("../shared/img/icons-14x14.svg#clear");
+  background-position: center;
+  background-size: 14px;
+  background-repeat: no-repeat;
+  cursor: pointer;
+  flex: 0 1 auto;
+  align-self: stretch;
 }
 
 .contact-list {
   overflow-x: hidden;
   overflow-y: auto;
   /* Space for six contacts, not affected by filtering.  This is enough space
      to show the dropdown menu when there is only one contact. */
   height: 306px;
+  /* Contact list title goes away when searching, needed for spacing. */
+  margin-top: 4px;
 }
 
 .contact-list-title {
   padding: 0 1rem;
   color: #666;
   font-weight: 500;
   font-size: .9em;
 }
@@ -160,16 +204,46 @@ html {
   background-image: url("../shared/img/avatars.svg#green-avatar");
   background-color: #56B397;
 }
 
 .contact > .avatar > img {
   width: 100%;
 }
 
+.contact-list-empty,
+.contact-search-list-empty {
+  background-image: url("../shared/img/empty_contacts.svg");
+  background-repeat: no-repeat;
+  background-position: top center;
+  margin-top: 4rem;
+  padding-top: 10rem;
+  padding-bottom: 5rem;
+  text-align: center;
+  color: #4a4a4a;
+}
+
+.contact-search-list-empty {
+  background-image: url("../shared/img/empty_search.svg");
+}
+
+.panel-text-medium,
+.panel-text-large {
+  margin: 3px;
+  color: #4a4a4a;
+}
+
+.panel-text-medium {
+  font-size: 1.2rem;
+}
+
+.panel-text-large {
+  font-size: 2.2rem;
+}
+
 .contact > .details > .username {
   font-size: 1.3rem;
   line-height: 20px;
   color: #000;
 }
 
 .contact.blocked > .details > .username {
   color: #d74345;
@@ -216,17 +290,17 @@ html {
 
 .icon-contact-video-call {
   padding: 15px;
   width: 16px;
   height: 16px;
   border-radius: 50%;
   background-color: #5bc0a4;
   background-image: url("../shared/img/icons-14x14.svg#video-white");
-  background-size: 16px 16px;
+  background-size: 14px 14px;
 }
 
 .icon-contact-video-call:hover {
   background-color: #47b396;
 }
 
 .icon-contact-video-call:active {
   background-color: #3aa689;
@@ -245,16 +319,17 @@ html {
 }
 
 .contact > .dropdown-menu {
   z-index: 2;
   top: 37px;
   right: 22px;
   bottom: auto;
   left: auto;
+  z-index: 2;
 }
 
 html[dir="rtl"] .contact > .dropdown-menu {
   right: auto;
   left: 22px;
 }
 
 .contact > .dropdown-menu-up {
@@ -335,18 +410,29 @@ html[dir="rtl"] .contacts-gravatar-promo
   border-radius: 5px;
 }
 
 .button.primary {
   background: #00A9DC;
   color: #fff;
 }
 
+.button.primary:active,
+.button.primary:hover {
+  background: #5cccee;
+}
+
 .button.secondary {
-  background: #EBEBEB;
+  background: #ebebeb;
+  color: #4D4D4D;
+}
+
+.button.secondary:hover,
+.button.secondary:active {
+  background: #dad6d6;
   color: #4D4D4D;
 }
 
 .contact-controls > .primary {
   flex: 5;
 }
 
 .contact-controls > .secondary {
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -408,16 +408,73 @@ loop.contacts = (function(_, mozL10n) {
         });
       });
     },
 
     componentWillUnmount: function() {
       window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
     },
 
+    /*
+     * Filter a user by name, email or phone number.
+     * Takes in an input to filter by and returns a filter function which
+     * expects a contact.
+     *
+     * @returns {Function}
+     */
+    filterContact: function(filter) {
+      return function(contact) {
+        return getPreferred(contact, "name").toLocaleLowerCase().includes(filter) ||
+          getPreferred(contact, "email").value.toLocaleLowerCase().includes(filter) ||
+          getPreferred(contact, "tel").value.toLocaleLowerCase().includes(filter);
+      };
+    },
+
+    /*
+     * Takes all contacts, it groups and filters them before rendering.
+     */
+    _filterContactsList: function() {
+      let shownContacts = _.groupBy(this.contacts, function(contact) {
+        return contact.blocked ? "blocked" : "available";
+      });
+
+      if (this._shouldShowFilter()) {
+        let filter = this.state.filter.trim().toLocaleLowerCase();
+        let filterFn = this.filterContact(filter);
+        if (filter) {
+          if (shownContacts.available) {
+            shownContacts.available = shownContacts.available.filter(filterFn);
+            // Filter can return an empty array.
+            if (!shownContacts.available.length) {
+              shownContacts.available = null;
+            }
+          }
+          if (shownContacts.blocked) {
+            shownContacts.blocked = shownContacts.blocked.filter(filterFn);
+            // Filter can return an empty array.
+            if (!shownContacts.blocked.length) {
+              shownContacts.blocked = null;
+            }
+          }
+        }
+      }
+
+      return shownContacts;
+    },
+
+    /*
+     * Decide to render contacts filter based on the number of contacts.
+     *
+     * @returns {bool}
+     */
+    _shouldShowFilter: function() {
+      return Object.getOwnPropertyNames(this.contacts).length >=
+        MIN_CONTACTS_FOR_FILTERING;
+    },
+
     _onStatusChanged: function() {
       let profile = this.props.mozLoop.userProfile;
       let currUid = this._userProfile ? this._userProfile.uid : null;
       let newUid = profile ? profile.uid : null;
       if (currUid !== newUid) {
         // On profile change (login, logout), reload all contacts.
         this._userProfile = profile;
         // The following will do a forceUpdate() for us.
@@ -526,123 +583,167 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleUseGravatar: function() {
       // We got permission to use Gravatar icons now, so we need to redraw the
       // list entirely to show them.
       this.refresh();
     },
 
+    /*
+     * Callback triggered when clicking the `X` from the contacts filter.
+     * Clears the search query.
+     */
+    _handleFilterClear: function() {
+      this.setState({
+        filter: ""
+      });
+    },
+
     sortContacts: function(contact1, contact2) {
       let comp = contact1.name[0].localeCompare(contact2.name[0]);
       if (comp !== 0) {
         return comp;
       }
       // If names are equal, compare against unique ids to make sure we have
       // consistent ordering.
       return contact1._guid - contact2._guid;
     },
 
+    _renderFilterClearButton: function() {
+      if (this.state.filter) {
+        return (
+          React.createElement("button", {className: "clear-search", 
+                  onClick: this._handleFilterClear})
+        );
+      }
+
+      return null;
+    },
+
+    _renderContactsFilter: function() {
+      if (this._shouldShowFilter()) {
+        return (
+          React.createElement("div", {className: "contact-filter-container"}, 
+            React.createElement("input", {className: "contact-filter", 
+                   placeholder: mozL10n.get("contacts_search_placesholder"), 
+                   valueLink: this.linkState("filter")}), 
+            this._renderFilterClearButton()
+          )
+        );
+      }
+
+      return null;
+    },
+
     _renderContactsList: function() {
       let cx = React.addons.classSet;
-
+      let shownContacts = this._filterContactsList();
       let viewForItem = item => {
         return (
           React.createElement(ContactDetail, {contact: item, 
                          handleContactAction: this.handleContactAction, 
                          key: item._guid})
         );
       };
 
-      let shownContacts = _.groupBy(this.contacts, function(contact) {
-        return contact.blocked ? "blocked" : "available";
-      });
-
-      let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
-                       MIN_CONTACTS_FOR_FILTERING;
-      if (showFilter) {
-        let filter = this.state.filter.trim().toLocaleLowerCase();
-        if (filter) {
-          let filterFn = contact => {
-            return contact.name[0].toLocaleLowerCase().includes(filter) ||
-                   getPreferred(contact, "email").value.toLocaleLowerCase().includes(filter);
-          };
-          if (shownContacts.available) {
-            shownContacts.available = shownContacts.available.filter(filterFn);
-          }
-          if (shownContacts.blocked) {
-            shownContacts.blocked = shownContacts.blocked.filter(filterFn);
-          }
-        }
+      // If no contacts to show and filter is set, then none match the search.
+      if (!shownContacts.available && !shownContacts.blocked &&
+          this.state.filter) {
+        return (
+          React.createElement("div", {className: "contact-search-list-empty"}, 
+            React.createElement("p", {className: "panel-text-large"}, 
+              mozL10n.get("no_search_results_message_heading")
+            ), 
+            React.createElement("p", {className: "panel-text-medium"}, 
+              mozL10n.get("no_search_results_message_subheading")
+            )
+          )
+        );
       }
 
-      if (shownContacts.available || shownContacts.blocked) {
+      // If no contacts to show and filter is not set, we don't have contacts.
+      if (!shownContacts.available && !shownContacts.blocked &&
+          !this.state.filter) {
         return (
-          React.createElement("div", null, 
-            React.createElement("div", {className: "contact-list-title"}, 
-              mozL10n.get("contact_list_title")
+          React.createElement("div", {className: "contact-list-empty"}, 
+            React.createElement("p", {className: "panel-text-large"}, 
+              mozL10n.get("no_contacts_message_heading")
             ), 
-            React.createElement("ul", {className: "contact-list"}, 
-              shownContacts.available ?
-                shownContacts.available.sort(this.sortContacts).map(viewForItem) :
-                null, 
-              shownContacts.blocked && shownContacts.blocked.length > 0 ?
-                React.createElement("div", {className: "contact-separator"}, mozL10n.get("contacts_blocked_contacts")) :
-                null, 
-              shownContacts.blocked ?
-                shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
-                null
+            React.createElement("p", {className: "panel-text-medium"}, 
+              mozL10n.get("no_contacts_import_or_add")
             )
           )
         );
       }
 
       return (
-        React.createElement("div", {className: "contact-list-empty"}, 
-          React.createElement("p", {className: "panel-text-large"}, 
-            mozL10n.get("no_contacts_message_heading")
+        React.createElement("div", null, 
+          !this.state.filter ? React.createElement("div", {className: "contact-list-title"}, 
+                                  mozL10n.get("contact_list_title")
+                                ) : null, 
+          React.createElement("ul", {className: "contact-list"}, 
+            shownContacts.available ?
+              shownContacts.available.sort(this.sortContacts).map(viewForItem) :
+              null, 
+            shownContacts.blocked && shownContacts.blocked.length > 0 ?
+              React.createElement("div", {className: "contact-separator"}, mozL10n.get("contacts_blocked_contacts")) :
+              null, 
+            shownContacts.blocked ?
+              shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
+              null
+          )
+        )
+      );
+    },
+
+    _renderAddContactButtons: function() {
+      let cx = React.addons.classSet;
+
+      if (this.state.filter) {
+        return null;
+      }
+
+      return (
+        React.createElement(ButtonGroup, {additionalClass: "contact-controls"}, 
+          React.createElement(Button, {additionalClass: "secondary", 
+            caption: this.state.importBusy ? mozL10n.get("importing_contacts_progress_button") :
+                                             mozL10n.get("import_contacts_button3"), 
+              disabled: this.state.importBusy, 
+              onClick: this.handleImportButtonClick}, 
+              React.createElement("div", {className: cx({"contact-import-spinner": true,
+                                 spinner: true,
+              busy: this.state.importBusy})})
           ), 
-          React.createElement("p", {className: "panel-text-medium"}, 
-            mozL10n.get("no_contacts_import_or_add")
-          )
+          React.createElement(Button, {additionalClass: "primary", 
+            caption: mozL10n.get("new_contact_button"), 
+            onClick: this.handleAddContactButtonClick})
+        )
+      );
+    },
+
+    _renderGravatarPromoMessage: function() {
+      if (this.state.filter) {
+        return null;
+      }
+
+      return (
+        React.createElement("div", {className: "content-area"}, 
+          React.createElement(GravatarPromo, {handleUse: this.handleUseGravatar})
         )
       );
     },
 
     render: function() {
-      let cx = React.addons.classSet;
-      let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
-                       MIN_CONTACTS_FOR_FILTERING;
-
       return (
         React.createElement("div", null, 
-          React.createElement("div", {className: "content-area"}, 
-            showFilter ?
-            React.createElement("input", {className: "contact-filter", 
-                   placeholder: mozL10n.get("contacts_search_placesholder"), 
-                   valueLink: this.linkState("filter")})
-            : null, 
-            React.createElement(GravatarPromo, {handleUse: this.handleUseGravatar})
-          ), 
+          this._renderContactsFilter(), 
+          this._renderGravatarPromoMessage(), 
           this._renderContactsList(), 
-          React.createElement(ButtonGroup, {additionalClass: "contact-controls"}, 
-            React.createElement(Button, {additionalClass: "secondary", 
-              caption: this.state.importBusy
-                ? mozL10n.get("importing_contacts_progress_button")
-                : mozL10n.get("import_contacts_button3"), 
-                disabled: this.state.importBusy, 
-                onClick: this.handleImportButtonClick}, 
-                React.createElement("div", {className: cx({"contact-import-spinner": true,
-                                   spinner: true,
-                busy: this.state.importBusy})})
-            ), 
-            React.createElement(Button, {additionalClass: "primary", 
-              caption: mozL10n.get("new_contact_button"), 
-              onClick: this.handleAddContactButtonClick})
-          )
+          this._renderAddContactButtons()
         )
       );
     }
   });
 
   const ContactDetailsForm = React.createClass({displayName: "ContactDetailsForm",
     mixins: [React.addons.LinkedStateMixin],
 
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -408,16 +408,73 @@ loop.contacts = (function(_, mozL10n) {
         });
       });
     },
 
     componentWillUnmount: function() {
       window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
     },
 
+    /*
+     * Filter a user by name, email or phone number.
+     * Takes in an input to filter by and returns a filter function which
+     * expects a contact.
+     *
+     * @returns {Function}
+     */
+    filterContact: function(filter) {
+      return function(contact) {
+        return getPreferred(contact, "name").toLocaleLowerCase().includes(filter) ||
+          getPreferred(contact, "email").value.toLocaleLowerCase().includes(filter) ||
+          getPreferred(contact, "tel").value.toLocaleLowerCase().includes(filter);
+      };
+    },
+
+    /*
+     * Takes all contacts, it groups and filters them before rendering.
+     */
+    _filterContactsList: function() {
+      let shownContacts = _.groupBy(this.contacts, function(contact) {
+        return contact.blocked ? "blocked" : "available";
+      });
+
+      if (this._shouldShowFilter()) {
+        let filter = this.state.filter.trim().toLocaleLowerCase();
+        let filterFn = this.filterContact(filter);
+        if (filter) {
+          if (shownContacts.available) {
+            shownContacts.available = shownContacts.available.filter(filterFn);
+            // Filter can return an empty array.
+            if (!shownContacts.available.length) {
+              shownContacts.available = null;
+            }
+          }
+          if (shownContacts.blocked) {
+            shownContacts.blocked = shownContacts.blocked.filter(filterFn);
+            // Filter can return an empty array.
+            if (!shownContacts.blocked.length) {
+              shownContacts.blocked = null;
+            }
+          }
+        }
+      }
+
+      return shownContacts;
+    },
+
+    /*
+     * Decide to render contacts filter based on the number of contacts.
+     *
+     * @returns {bool}
+     */
+    _shouldShowFilter: function() {
+      return Object.getOwnPropertyNames(this.contacts).length >=
+        MIN_CONTACTS_FOR_FILTERING;
+    },
+
     _onStatusChanged: function() {
       let profile = this.props.mozLoop.userProfile;
       let currUid = this._userProfile ? this._userProfile.uid : null;
       let newUid = profile ? profile.uid : null;
       if (currUid !== newUid) {
         // On profile change (login, logout), reload all contacts.
         this._userProfile = profile;
         // The following will do a forceUpdate() for us.
@@ -526,123 +583,167 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleUseGravatar: function() {
       // We got permission to use Gravatar icons now, so we need to redraw the
       // list entirely to show them.
       this.refresh();
     },
 
+    /*
+     * Callback triggered when clicking the `X` from the contacts filter.
+     * Clears the search query.
+     */
+    _handleFilterClear: function() {
+      this.setState({
+        filter: ""
+      });
+    },
+
     sortContacts: function(contact1, contact2) {
       let comp = contact1.name[0].localeCompare(contact2.name[0]);
       if (comp !== 0) {
         return comp;
       }
       // If names are equal, compare against unique ids to make sure we have
       // consistent ordering.
       return contact1._guid - contact2._guid;
     },
 
+    _renderFilterClearButton: function() {
+      if (this.state.filter) {
+        return (
+          <button className="clear-search"
+                  onClick={this._handleFilterClear} />
+        );
+      }
+
+      return null;
+    },
+
+    _renderContactsFilter: function() {
+      if (this._shouldShowFilter()) {
+        return (
+          <div className="contact-filter-container">
+            <input className="contact-filter"
+                   placeholder={mozL10n.get("contacts_search_placesholder")}
+                   valueLink={this.linkState("filter")} />
+            {this._renderFilterClearButton()}
+          </div>
+        );
+      }
+
+      return null;
+    },
+
     _renderContactsList: function() {
       let cx = React.addons.classSet;
-
+      let shownContacts = this._filterContactsList();
       let viewForItem = item => {
         return (
           <ContactDetail contact={item}
                          handleContactAction={this.handleContactAction}
                          key={item._guid} />
         );
       };
 
-      let shownContacts = _.groupBy(this.contacts, function(contact) {
-        return contact.blocked ? "blocked" : "available";
-      });
-
-      let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
-                       MIN_CONTACTS_FOR_FILTERING;
-      if (showFilter) {
-        let filter = this.state.filter.trim().toLocaleLowerCase();
-        if (filter) {
-          let filterFn = contact => {
-            return contact.name[0].toLocaleLowerCase().includes(filter) ||
-                   getPreferred(contact, "email").value.toLocaleLowerCase().includes(filter);
-          };
-          if (shownContacts.available) {
-            shownContacts.available = shownContacts.available.filter(filterFn);
-          }
-          if (shownContacts.blocked) {
-            shownContacts.blocked = shownContacts.blocked.filter(filterFn);
-          }
-        }
+      // If no contacts to show and filter is set, then none match the search.
+      if (!shownContacts.available && !shownContacts.blocked &&
+          this.state.filter) {
+        return (
+          <div className="contact-search-list-empty">
+            <p className="panel-text-large">
+              {mozL10n.get("no_search_results_message_heading")}
+            </p>
+            <p className="panel-text-medium">
+              {mozL10n.get("no_search_results_message_subheading")}
+            </p>
+          </div>
+        );
       }
 
-      if (shownContacts.available || shownContacts.blocked) {
+      // If no contacts to show and filter is not set, we don't have contacts.
+      if (!shownContacts.available && !shownContacts.blocked &&
+          !this.state.filter) {
         return (
-          <div>
-            <div className="contact-list-title">
-              {mozL10n.get("contact_list_title")}
-            </div>
-            <ul className="contact-list">
-              {shownContacts.available ?
-                shownContacts.available.sort(this.sortContacts).map(viewForItem) :
-                null}
-              {shownContacts.blocked && shownContacts.blocked.length > 0 ?
-                <div className="contact-separator">{mozL10n.get("contacts_blocked_contacts")}</div> :
-                null}
-              {shownContacts.blocked ?
-                shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
-                null}
-            </ul>
+          <div className="contact-list-empty">
+            <p className="panel-text-large">
+              {mozL10n.get("no_contacts_message_heading")}
+            </p>
+            <p className="panel-text-medium">
+              {mozL10n.get("no_contacts_import_or_add")}
+            </p>
           </div>
         );
       }
 
       return (
-        <div className="contact-list-empty">
-          <p className="panel-text-large">
-            {mozL10n.get("no_contacts_message_heading")}
-          </p>
-          <p className="panel-text-medium">
-            {mozL10n.get("no_contacts_import_or_add")}
-          </p>
+        <div>
+          {!this.state.filter ? <div className="contact-list-title">
+                                  {mozL10n.get("contact_list_title")}
+                                </div> : null}
+          <ul className="contact-list">
+            {shownContacts.available ?
+              shownContacts.available.sort(this.sortContacts).map(viewForItem) :
+              null}
+            {shownContacts.blocked && shownContacts.blocked.length > 0 ?
+              <div className="contact-separator">{mozL10n.get("contacts_blocked_contacts")}</div> :
+              null}
+            {shownContacts.blocked ?
+              shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
+              null}
+          </ul>
+        </div>
+      );
+    },
+
+    _renderAddContactButtons: function() {
+      let cx = React.addons.classSet;
+
+      if (this.state.filter) {
+        return null;
+      }
+
+      return (
+        <ButtonGroup additionalClass="contact-controls">
+          <Button additionalClass="secondary"
+            caption={this.state.importBusy ? mozL10n.get("importing_contacts_progress_button") :
+                                             mozL10n.get("import_contacts_button3")}
+              disabled={this.state.importBusy}
+              onClick={this.handleImportButtonClick} >
+              <div className={cx({"contact-import-spinner": true,
+                                 spinner: true,
+              busy: this.state.importBusy})} />
+          </Button>
+          <Button additionalClass="primary"
+            caption={mozL10n.get("new_contact_button")}
+            onClick={this.handleAddContactButtonClick} />
+        </ButtonGroup>
+      );
+    },
+
+    _renderGravatarPromoMessage: function() {
+      if (this.state.filter) {
+        return null;
+      }
+
+      return (
+        <div className="content-area">
+          <GravatarPromo handleUse={this.handleUseGravatar}/>
         </div>
       );
     },
 
     render: function() {
-      let cx = React.addons.classSet;
-      let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
-                       MIN_CONTACTS_FOR_FILTERING;
-
       return (
         <div>
-          <div className="content-area">
-            {showFilter ?
-            <input className="contact-filter"
-                   placeholder={mozL10n.get("contacts_search_placesholder")}
-                   valueLink={this.linkState("filter")} />
-            : null }
-            <GravatarPromo handleUse={this.handleUseGravatar}/>
-          </div>
+          {this._renderContactsFilter()}
+          {this._renderGravatarPromoMessage()}
           {this._renderContactsList()}
-          <ButtonGroup additionalClass="contact-controls">
-            <Button additionalClass="secondary"
-              caption={this.state.importBusy
-                ? mozL10n.get("importing_contacts_progress_button")
-                : mozL10n.get("import_contacts_button3")}
-                disabled={this.state.importBusy}
-                onClick={this.handleImportButtonClick} >
-                <div className={cx({"contact-import-spinner": true,
-                                   spinner: true,
-                busy: this.state.importBusy})} />
-            </Button>
-            <Button additionalClass="primary"
-              caption={mozL10n.get("new_contact_button")}
-              onClick={this.handleAddContactButtonClick} />
-          </ButtonGroup>
+          {this._renderAddContactButtons()}
         </div>
       );
     }
   });
 
   const ContactDetailsForm = React.createClass({
     mixins: [React.addons.LinkedStateMixin],
 
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/empty_search.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="92px" height="99px" viewBox="0 0 92 99" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <path d="M16,25.4921875 C16,21.7812314 17.1913943,18.0215034 19.5742188,14.2128906 C21.9570432,10.4042778 25.4335709,7.2500125 30.0039062,4.75 C34.5742416,2.2499875 39.9062195,1 46,1 C51.6640908,1 56.6640408,2.04491143 61,4.13476562 C65.3359592,6.22461982 68.6855351,9.06638828 71.0488281,12.6601562 C73.4121212,16.2539242 74.59375,20.1601352 74.59375,24.3789062 C74.59375,27.6992354 73.9199286,30.6093625 72.5722656,33.109375 C71.2246026,35.6093875 69.6230562,37.767569 67.7675781,39.5839844 C65.9121001,41.4003997 62.5820553,44.4570098 57.7773438,48.7539062 C56.4492121,49.9648498 55.3847696,51.0292923 54.5839844,51.9472656 C53.7831991,52.865239 53.187502,53.7050743 52.796875,54.4667969 C52.406248,55.2285194 52.1035167,55.9902306 51.8886719,56.7519531 C51.6738271,57.5136757 51.3515646,58.8515529 50.921875,60.765625 C50.1796838,64.8281453 47.8554883,66.859375 43.9492188,66.859375 C41.9179586,66.859375 40.2089913,66.1953191 38.8222656,64.8671875 C37.4355399,63.5390559 36.7421875,61.5664193 36.7421875,58.9492188 C36.7421875,55.6679523 37.2499949,52.8261839 38.265625,50.4238281 C39.2812551,48.0214724 40.6288979,45.9121185 42.3085938,44.0957031 C43.9882896,42.2792878 46.253892,40.1211062 49.1054688,37.6210938 C51.6054813,35.4335828 53.4121038,33.7832087 54.5253906,32.6699219 C55.6386774,31.5566351 56.5761681,30.3164131 57.3378906,28.9492188 C58.0996132,27.5820244 58.4804688,26.0976643 58.4804688,24.4960938 C58.4804688,21.3710781 57.318371,18.7343857 54.9941406,16.5859375 C52.6699103,14.4374893 49.6718934,13.3632812 46,13.3632812 C41.7031035,13.3632812 38.5390727,14.4472548 36.5078125,16.6152344 C34.4765523,18.783214 32.7578195,21.9765414 31.3515625,26.1953125 C30.0234309,30.6093971 27.5039248,32.8164062 23.7929688,32.8164062 C21.6054578,32.8164062 19.7597731,32.0449296 18.2558594,30.5019531 C16.7519456,28.9589767 16,27.2890715 16,25.4921875 L16,25.4921875 Z M44.59375,89.7109375 C42.2109256,89.7109375 40.1308683,88.9394608 38.3535156,87.3964844 C36.576163,85.8535079 35.6875,83.6953264 35.6875,80.921875 C35.6875,78.4609252 36.5468664,76.3906334 38.265625,74.7109375 C39.9843836,73.0312416 42.0937375,72.1914062 44.59375,72.1914062 C47.0546998,72.1914062 49.1249916,73.0312416 50.8046875,74.7109375 C52.4843834,76.3906334 53.3242188,78.4609252 53.3242188,80.921875 C53.3242188,83.6562637 52.4453213,85.8046797 50.6875,87.3671875 C48.9296787,88.9296953 46.898449,89.7109375 44.59375,89.7109375 L44.59375,89.7109375 Z" id="?-copy" fill="#D8D8D8"></path>
+        <path d="M30.0307824,90.7518487 C26.2851884,95.2457309 20.7911271,98.0823793 14.6681559,98.0823793 C9.0246635,98.0823793 3.91544008,95.6726018 0.217089748,91.7765187 C0.144175866,91.6997064 0.0718103773,91.6223165 -1.41595069e-13,91.5443559 C3.2164267,86.2421507 8.85313514,82.7267408 15.269241,82.7267408 C21.3600311,82.7267408 26.7484556,85.8947045 30.0307824,90.7518487 L30.0307824,90.7518487 Z M15.269241,80.2680577 C20.5820599,80.2680577 24.8889507,75.7308984 24.8889507,70.1340288 C24.8889507,64.5371593 20.5820599,60 15.269241,60 C9.95642199,60 5.64953124,64.5371593 5.64953124,70.1340288 C5.64953124,75.7308984 9.95642199,80.2680577 15.269241,80.2680577 Z" id="Mask-Copy-7" fill-opacity="0.8" fill="#D8D8D8"></path>
+        <path d="M91.0307824,90.7518487 C87.2851884,95.2457309 81.7911271,98.0823793 75.6681559,98.0823793 C70.0246635,98.0823793 64.9154401,95.6726018 61.2170897,91.7765187 C61.1441759,91.6997064 61.0718104,91.6223165 61,91.5443559 C64.2164267,86.2421507 69.8531351,82.7267408 76.269241,82.7267408 C82.3600311,82.7267408 87.7484556,85.8947045 91.0307824,90.7518487 L91.0307824,90.7518487 Z M76.269241,80.2680577 C81.5820599,80.2680577 85.8889507,75.7308984 85.8889507,70.1340288 C85.8889507,64.5371593 81.5820599,60 76.269241,60 C70.956422,60 66.6495312,64.5371593 66.6495312,70.1340288 C66.6495312,75.7308984 70.956422,80.2680577 76.269241,80.2680577 Z" id="Mask-Copy-8" fill-opacity="0.8" fill="#D8D8D8"></path>
+    </g>
+</svg>
\ No newline at end of file
--- a/browser/components/loop/content/shared/img/icons-14x14.svg
+++ b/browser/components/loop/content/shared/img/icons-14x14.svg
@@ -37,16 +37,18 @@
     <path id="pause-shape" fill-rule="evenodd" d="M4.75,1h-1.5C2.836,1,2.5,1.336,2.5,1.75v10.5 C2.5,12.664,2.836,13,3.25,13h1.5c0.414,0,0.75-0.336,0.75-0.75V1.75C5.5,1.336,5.164,1,4.75,1z M10.75,1h-1.5 C8.836,1,8.5,1.336,8.5,1.75v10.5C8.5,12.664,8.836,13,9.25,13h1.5c0.414,0,0.75-0.336,0.75-0.75V1.75C11.5,1.336,11.164,1,10.75,1 z"/>
     <path id="video-shape" d="M1.59247473,11.4075253 C1.9956945,11.7983901 2.46237364,12 3.02957694,12 L7.98333957,12 C8.53762636,12 9.01666043,11.7983901 9.40752527,11.4075253 C9.81130663,11.0043055 10,10.5376264 10,9.97042306 L10,8.81074504 L12.8360165,11.6467615 C12.9247473,11.7354923 13.0252714,11.7731187 13.1516286,11.7731187 C13.2145264,11.7731187 13.2650693,11.7607638 13.3279671,11.7354923 C13.517222,11.659678 13.6053912,11.5209659 13.6053912,11.319356 L13.6053912,3.66772744 C13.6053912,3.47903407 13.517222,3.34032198 13.3279671,3.25215275 C13.2650693,3.23923624 13.2145264,3.22688132 13.1516286,3.22688132 C13.0252714,3.22688132 12.9247473,3.26450768 12.8360165,3.3526769 L10,6.17633845 L10,5.01666043 C10,4.46181206 9.81130663,3.98333957 9.40752527,3.59247473 C9.01666043,3.18869337 8.53762636,3 7.98333957,3 L3.02957694,3 C2.46237364,3 1.9956945,3.18869337 1.59247473,3.59247473 C1.20160988,3.98333957 1,4.46181206 1,5.01666043 L1,9.97042306 C1,10.5376264 1.20160988,11.0043055 1.59247473,11.4075253" fill="#fff" fill-rule="evenodd"/>
     <g id="volume-shape">
       <path fill-rule="evenodd" d="M3.513,4.404H1.896c-0.417,0-0.756,0.338-0.756,0.755v3.679 c0,0.417,0.338,0.755,0.756,0.755H3.51l2.575,2.575c0.261,0.261,0.596,0.4,0.938,0.422V1.409C6.682,1.431,6.346,1.57,6.085,1.831 L3.513,4.404z M8.555,5.995C8.619,6.32,8.653,6.656,8.653,7c0,0.344-0.034,0.679-0.098,1.004l0.218,0.142 C8.852,7.777,8.895,7.393,8.895,7c0-0.394-0.043-0.777-0.123-1.147L8.555,5.995z M12.224,3.6l-0.475,0.31 c0.359,0.962,0.557,2.003,0.557,3.09c0,1.087-0.198,2.128-0.557,3.09l0.475,0.31c0.41-1.054,0.635-2.201,0.635-3.4 C12.859,5.8,12.634,4.654,12.224,3.6z M10.061,5.012C10.25,5.642,10.353,6.308,10.353,7c0,0.691-0.103,1.358-0.293,1.987 l0.351,0.229C10.634,8.517,10.756,7.772,10.756,7c0-0.773-0.121-1.517-0.345-2.216L10.061,5.012z"/>
       <path d="M7.164,12.74l-0.15-0.009c-0.389-0.024-0.754-0.189-1.028-0.463L3.452,9.735H1.896 C1.402,9.735,1,9.333,1,8.838V5.16c0-0.494,0.402-0.896,0.896-0.896h1.558l2.531-2.531C6.26,1.458,6.625,1.293,7.014,1.269 l0.15-0.009V12.74z M1.896,4.545c-0.339,0-0.615,0.276-0.615,0.615v3.679c0,0.339,0.276,0.615,0.615,0.615h1.672l2.616,2.616 c0.19,0.19,0.434,0.316,0.697,0.363V1.568C6.619,1.615,6.375,1.741,6.185,1.931L3.571,4.545H1.896z M12.292,10.612l-0.714-0.467 l0.039-0.105C11.981,9.067,12.165,8.044,12.165,7c0-1.044-0.184-2.067-0.548-3.041l-0.039-0.105l0.714-0.467l0.063,0.162 C12.783,4.649,13,5.81,13,7s-0.217,2.351-0.645,3.451L12.292,10.612z M11.92,10.033l0.234,0.153 c0.374-1.019,0.564-2.09,0.564-3.186s-0.19-2.167-0.564-3.186L11.92,3.966C12.27,4.94,12.447,5.96,12.447,7 C12.447,8.04,12.27,9.059,11.92,10.033z M10.489,9.435L9.895,9.047l0.031-0.101C10.116,8.315,10.212,7.66,10.212,7 c0-0.661-0.096-1.316-0.287-1.947L9.895,4.952l0.594-0.388l0.056,0.176C10.779,5.471,10.897,6.231,10.897,7 c0,0.769-0.118,1.529-0.351,2.259L10.489,9.435z M10.225,8.926l0.106,0.069C10.52,8.348,10.615,7.677,10.615,7 c0-0.677-0.095-1.348-0.284-1.996l-0.106,0.07C10.403,5.699,10.494,6.347,10.494,7C10.494,7.652,10.403,8.3,10.225,8.926z M8.867,8.376L8.398,8.07l0.018-0.093C8.48,7.654,8.512,7.325,8.512,7S8.48,6.345,8.417,6.022L8.398,5.929l0.469-0.306l0.043,0.2 C8.994,6.211,9.036,6.607,9.036,7c0,0.393-0.042,0.789-0.126,1.176L8.867,8.376z"/>
     </g>
     <path id="contacts-shape" fill-rule="evenodd" transform="translate(-79.000000, -59.000000)" d="M91.5000066,69.9765672 C91.5000066,68.2109401 91.0859436,65.4999994 88.7968783,65.4999994 C88.5546906,65.4999994 87.5312518,66.5859382 86,66.5859382 C84.4687482,66.5859382 83.4453095,65.4999994 83.2031217,65.4999994 C80.9140564,65.4999994 80.4999935,68.2109401 80.4999935,69.9765672 C80.4999935,71.2421938 81.3437445,72.0000072 82.5859334,72.0000072 L89.4140666,72.0000072 C90.6562555,72.0000072 91.5000066,71.2421938 91.5000066,69.9765672 L91.5000066,69.9765672 L91.5000066,69.9765672 Z M89.0000036,62.9999964 C89.0000036,61.3437444 87.656252,59.9999928 86,59.9999928 C84.343748,59.9999928 82.9999964,61.3437444 82.9999964,62.9999964 C82.9999964,64.6562484 84.343748,66 86,66 C87.656252,66 89.0000036,64.6562484 89.0000036,62.9999964 L89.0000036,62.9999964 L89.0000036,62.9999964 Z" />
     <path id="hello-shape" fill-rule="evenodd" transform="translate(-261.000000, -59.000000)" d="M268.273778,60 C264.809073,60 262,62.4730749 262,65.523237 C262,67.0417726 262.697086,68.4174001 263.822897,69.4155754 C263.627626,70.1061164 263.240356,71.0442922 262.474542,71.959559 C262.605451,72.1919211 264.761073,71.3737446 266.2807,70.7617485 C266.907968,70.946111 267.577782,71.046474 268.274868,71.046474 C271.740664,71.046474 274.549737,68.5733991 274.549737,65.523237 C274.549737,62.4730749 271.739573,60 268.274868,60 L268.273778,60 Z M270.15122,63.3119786 C270.609399,63.3119786 270.980306,63.6850671 270.980306,64.1432459 C270.980306,64.6036066 270.609399,64.9756042 270.15122,64.9756042 C269.693041,64.9756042 269.321044,64.6036066 269.321044,64.1432459 C269.321044,63.6850671 269.693041,63.3119786 270.15122,63.3119786 L270.15122,63.3119786 Z M266.36579,63.3119786 C266.823969,63.3119786 267.195966,63.6850671 267.195966,64.1432459 C267.195966,64.6036066 266.823969,64.9756042 266.36579,64.9756042 C265.907611,64.9756042 265.535613,64.6036066 265.535613,64.1432459 C265.535613,63.6850671 265.907611,63.3119786 266.36579,63.3119786 L266.36579,63.3119786 Z M268.283596,69.3675757 L268.258505,69.3664848 L268.233414,69.3675757 C266.557789,69.3675757 264.685801,68.2777646 264.254894,66.4428674 C265.38616,66.9675913 266.967968,67.1966807 268.258505,67.1966807 C269.549042,67.1966807 271.13085,66.9675913 272.262115,66.4428674 C271.8323,68.2777646 269.959221,69.3675757 268.283596,69.3675757 L268.283596,69.3675757 Z" />
+    <path id="clear-shape" d="M215.55504,63.8820643 C215.688665,63.7472288 215.688665,63.6111212 215.55504,63.4686535 L214.529316,62.463747 C214.382965,62.3174632 214.245523,62.3174632 214.116989,62.463747 L211.98791,64.6007632 L210.042087,62.6621843 C209.983547,62.6049428 209.917371,62.574414 209.841014,62.574414 C209.76593,62.574414 209.694664,62.6049428 209.631034,62.6621843 L208.666394,63.6518263 C208.520044,63.7866618 208.520044,63.9227694 208.666394,64.0639651 L210.586765,65.9821915 L208.466594,68.1192077 C208.408054,68.1789933 208.377511,68.245139 208.377511,68.3227331 C208.377511,68.3990551 208.408054,68.4690169 208.466594,68.5313465 L209.47323,69.5489733 C209.53177,69.6074868 209.599218,69.6380156 209.67812,69.6405597 C209.758295,69.6418317 209.827016,69.612575 209.885556,69.5489733 L212.023543,67.4208613 L213.942642,69.3568962 C214.001182,69.4141377 214.06863,69.4433945 214.150077,69.4433945 C214.228979,69.4433945 214.298973,69.4141377 214.357513,69.3568962 L215.347605,68.3672542 C215.481229,68.2324187 215.481229,68.0963111 215.347605,67.9551154 L213.399236,66.036889 L215.556313,63.8807923 L215.55504,63.8820643 Z M216.958731,61.0505179 C217.642123,61.7335999 218.152441,62.5057242 218.490955,63.3630747 C218.82947,64.2229693 219,65.0994003 219,66 C219,66.8980556 218.82947,67.7783027 218.490955,68.6394694 C218.152441,69.4955479 217.640851,70.2664001 216.958731,70.9507541 C216.274066,71.6338361 215.501591,72.1477376 214.63876,72.4873705 C213.775929,72.8295475 212.897827,73 212.001909,73 C211.103445,73 210.224071,72.8295475 209.36124,72.4873705 C208.498409,72.1464656 207.728479,71.6325641 207.043814,70.9507541 C206.360422,70.2664001 205.848832,69.4955479 205.509045,68.6394694 C205.17053,67.7795748 205,66.8993276 205,66 C205,65.0994003 205.171803,64.2216973 205.514135,63.3592586 C205.855195,62.4968199 206.36424,61.7285117 207.042542,61.0492459 C207.727207,60.3661639 208.498409,59.8548065 209.359967,59.5126295 C210.222798,59.1704525 211.1009,59 212.000636,59 C212.896555,59 213.774657,59.1704525 214.637488,59.5126295 C215.500318,59.8548065 216.272793,60.3674359 216.957458,61.0492459 L216.958731,61.0505179 Z" transform="translate(-205 -59)" fill="#9B9B9B" fill-rule="evenodd"/>
+    <path id="magnifier-shape" d="M191.499904,64.0475302 C191.499904,66.8377127 193.779753,69.0950604 196.596937,69.0950604 C199.414121,69.0950604 201.693971,66.8377127 201.693971,64.0475302 C201.693971,61.2573477 199.414121,59 196.596937,59 C193.779753,59 191.499904,61.2573477 191.499904,64.0475302 L191.499904,64.0475302 Z M193.353125,64.0475302 C193.353125,62.2762143 194.80852,60.8352201 196.596937,60.8352201 C198.385354,60.8352201 199.840749,62.2762143 199.840749,64.0475302 C199.840749,65.8188461 198.385354,67.2598403 196.596937,67.2598403 C194.80852,67.2598403 193.353125,65.8188461 193.353125,64.0475302 L193.353125,64.0475302 Z M200.600399,69.8475096 L203.046759,72.2704681 C203.324877,72.5458861 203.927017,72.5827885 204.418449,72.3343723 C204.946783,72.0679548 205.150197,70.9941846 204.881979,70.6548624 L202.472522,68.2229033 C202.055794,67.7089697 201.23044,67.7179703 200.693105,68.2508051 C200.211574,68.7278363 200.192672,69.4442832 200.600399,69.8475096 L200.600399,69.8475096 Z" transform="translate(-191 -59)" fill="#9B9B9B" fill-rule="evenodd"/>
   </defs>
   <use id="audio" xlink:href="#audio-shape"/>
   <use id="audio-active" xlink:href="#audio-shape"/>
   <use id="audio-disabled" xlink:href="#audio-shape"/>
   <use id="contacts" xlink:href="#contacts-shape"/>
   <use id="contacts-hover" xlink:href="#contacts-shape"/>
   <use id="contacts-active" xlink:href="#contacts-shape"/>
   <use id="facemute" xlink:href="#facemute-shape"/>
@@ -72,9 +74,11 @@
   <use id="pause-disabled" xlink:href="#pause-shape"/>
   <use id="video" xlink:href="#video-shape"/>
   <use id="video-white" xlink:href="#video-shape"/>
   <use id="video-active" xlink:href="#video-shape"/>
   <use id="video-disabled" xlink:href="#video-shape"/>
   <use id="volume" xlink:href="#volume-shape"/>
   <use id="volume-active" xlink:href="#volume-shape"/>
   <use id="volume-disabled" xlink:href="#volume-shape"/>
+  <use id="clear" xlink:href="#clear-shape"/>
+  <use id="magnifier" xlink:href="#magnifier-shape"/>
 </svg>
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -70,16 +70,17 @@ browser.jar:
   content/browser/loop/shared/img/vivo@2x.png                   (content/shared/img/vivo@2x.png)
   content/browser/loop/shared/img/02.png                        (content/shared/img/02.png)
   content/browser/loop/shared/img/02@2x.png                     (content/shared/img/02@2x.png)
   content/browser/loop/shared/img/telefonica.png                (content/shared/img/telefonica.png)
   content/browser/loop/shared/img/telefonica@2x.png             (content/shared/img/telefonica@2x.png)
   content/browser/loop/shared/img/ellipsis-v.svg                (content/shared/img/ellipsis-v.svg)
   content/browser/loop/shared/img/empty_contacts.svg            (content/shared/img/empty_contacts.svg)
   content/browser/loop/shared/img/empty_conversations.svg       (content/shared/img/empty_conversations.svg)
+  content/browser/loop/shared/img/empty_search.svg              (content/shared/img/empty_search.svg)
   content/browser/loop/shared/img/avatars.svg                   (content/shared/img/avatars.svg)
 
   # Shared scripts
   content/browser/loop/shared/js/actions.js             (content/shared/js/actions.js)
   content/browser/loop/shared/js/conversationStore.js   (content/shared/js/conversationStore.js)
   content/browser/loop/shared/js/store.js               (content/shared/js/store.js)
   content/browser/loop/shared/js/roomStates.js          (content/shared/js/roomStates.js)
   content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js)
--- a/browser/components/loop/test/desktop-local/contacts_test.js
+++ b/browser/components/loop/test/desktop-local/contacts_test.js
@@ -8,17 +8,17 @@ describe("loop.contacts", function() {
   var expect = chai.expect;
   var TestUtils = React.addons.TestUtils;
 
   var fakeAddContactButtonText = "Fake Add Contact Button";
   var fakeAddContactTitleText = "Fake Add Contact Title";
   var fakeEditContactButtonText = "Fake Edit Contact";
   var fakeDoneButtonText = "Fake Done";
   // The fake contacts array is copied each time mozLoop.contacts.getAll() is called.
-  var fakeContacts = [{
+  var fakeManyContacts = [{
     id: 1,
     _guid: 1,
     name: ["Ally Avocado"],
     email: [{
       "pref": true,
       "type": ["work"],
       "value": "ally@mail.com"
     }],
@@ -66,17 +66,55 @@ describe("loop.contacts", function() {
     email: [{
       "pref": true,
       "type": ["work"],
       "value": "dd@dragons.net"
     }],
     category: ["google"],
     published: 1406798311748,
     updated: 1406798311748
+  }, {
+    id: 5,
+    _guid: 5,
+    name: ["Erin J. Bazile"],
+    email: [{
+      "pref": true,
+      "type": ["work"],
+      "value": "erinjbazile@armyspy.com"
+    }],
+    category: ["google"],
+    published: 1406798311748,
+    updated: 1406798311748
+  }, {
+    id: 6,
+    _guid: 6,
+    name: ["Kelly F. Maldanado"],
+    email: [{
+      "pref": true,
+      "type": ["work"],
+      "value": "kellyfmaldonado@jourrapide.com"
+    }],
+    category: ["google"],
+    published: 1406798311748,
+    updated: 1406798311748
+  }, {
+    id: 7,
+    _guid: 7,
+    name: ["John J. Brown"],
+    email: [{
+      "pref": true,
+      "type": ["work"],
+      "value": "johnjbrow@johndoe.com"
+    }],
+    category: ["google"],
+    published: 1406798311748,
+    updated: 1406798311748,
+    blocked: true
   }];
+  var fakeFewerContacts = fakeManyContacts.slice(0, 4);
   var sandbox;
   var fakeWindow;
   var notifications;
   var listView;
   var oldMozLoop = navigator.mozLoop;
   var mozL10nGetSpy;
 
   beforeEach(function() {
@@ -110,17 +148,17 @@ describe("loop.contacts", function() {
       getUserAvatar: function() {
         if (navigator.mozLoop.getLoopPref("contacts.gravatars.show")) {
           return "gravatarsEnabled";
         }
         return "gravatarsDisabled";
       },
       contacts: {
         getAll: function(callback) {
-          callback(null, [].concat(fakeContacts));
+          callback(null, [].concat(fakeFewerContacts));
         },
         on: sandbox.stub()
       },
       calls: {
         startDirectCall: function() {},
         clearCallInProgress: function() {}
       }
     };
@@ -143,20 +181,20 @@ describe("loop.contacts", function() {
   });
 
   describe("GravatarsPromo", function() {
     function checkGravatarContacts(enabled) {
       var node = listView.getDOMNode();
 
       // When gravatars are enabled, contacts should be rendered with gravatars.
       var gravatars = node.querySelectorAll(".contact img[src=gravatarsEnabled]");
-      expect(gravatars.length).to.equal(enabled ? fakeContacts.length : 0);
+      expect(gravatars.length).to.equal(enabled ? fakeFewerContacts.length : 0);
       // Sanity check the reverse:
       gravatars = node.querySelectorAll(".contact img[src=gravatarsDisabled]");
-      expect(gravatars.length).to.equal(enabled ? 0 : fakeContacts.length);
+      expect(gravatars.length).to.equal(enabled ? 0 : fakeFewerContacts.length);
     }
 
     it("should show the gravatars promo box", function() {
       listView = TestUtils.renderIntoDocument(
         React.createElement(loop.contacts.ContactsList, {
           mozLoop: navigator.mozLoop,
           notifications: notifications,
           startForm: function() {}
@@ -292,31 +330,145 @@ describe("loop.contacts", function() {
         sinon.assert.calledWithExactly(mozL10nGetSpy,
                                        "no_contacts_import_or_add");
       });
     });
 
     describe("#RenderWithContacts", function() {
       beforeEach(function() {
         sandbox.stub(navigator.mozLoop.contacts, "getAll", function(cb) {
-          cb(null, [].concat(fakeContacts));
+          cb(null, [].concat(fakeFewerContacts));
         });
         listView = TestUtils.renderIntoDocument(
           React.createElement(loop.contacts.ContactsList, {
             mozLoop: navigator.mozLoop,
             notifications: notifications,
             startForm: function() {}
           }));
         node = listView.getDOMNode();
       });
 
       it("should show a contacts title", function() {
         expect(node.querySelector(".contact-list-title")).not.to.eql(null);
         sinon.assert.calledWithExactly(mozL10nGetSpy, "contact_list_title");
       });
+
+      it("should not render the filter view unless MIN_CONTACTS_FOR_FILTERING",
+         function() {
+           var filterView = listView.getDOMNode()
+             .querySelector(".contact-filter-container");
+
+           expect(filterView).to.eql(null);
+         });
+    });
+
+    describe("ContactsFiltering", function() {
+      beforeEach(function() {
+        navigator.mozLoop.contacts = {
+          getAll: function(callback) {
+            callback(null, [].concat(fakeManyContacts));
+          },
+          on: sandbox.stub()
+        };
+        listView = TestUtils.renderIntoDocument(
+          React.createElement(loop.contacts.ContactsList, {
+            mozLoop: navigator.mozLoop,
+            notifications: notifications,
+            startForm: function() {}
+          }));
+        node = listView.getDOMNode();
+      });
+
+      it("should filter a non-existent user name", function() {
+        expect(listView.filterContact("foo")(fakeFewerContacts[0]))
+          .to.eql(false);
+      });
+
+      it("should display search returned no contacts view", function() {
+        listView.setState({
+          filter: "foo"
+        });
+
+        var view = node.querySelector(".contact-search-list-empty");
+
+        expect(view).to.not.eql(null);
+      });
+
+      it("should display the no search results strings", function() {
+        listView.setState({
+          filter: "foo"
+        });
+
+        sinon.assert.calledWithExactly(mozL10nGetSpy,
+                                       "no_search_results_message_heading");
+        sinon.assert.calledWithExactly(mozL10nGetSpy,
+                                       "no_search_results_message_subheading");
+      });
+
+      it("should filter the user name correctly", function() {
+        expect(listView.filterContact("ally")(fakeFewerContacts[0]))
+          .to.eql(true);
+      });
+
+      it("should filter and render a contact", function() {
+        listView.setState({
+          filter: "Ally"
+        });
+
+        var contacts = node.querySelectorAll(".contact");
+
+        expect(contacts.length).to.eql(1);
+      });
+
+      it("should render a list of contacts", function() {
+        var contactList = listView.getDOMNode().querySelectorAll(".contact");
+
+        expect(contactList.length).to.eql(fakeManyContacts.length);
+      });
+
+      it("should render the filter view for >= MIN_CONTACTS_FOR_FILTERING",
+         function() {
+           var filterView = listView.getDOMNode()
+             .querySelector(".contact-filter-container");
+
+           expect(filterView).to.not.eql(null);
+         });
+
+      it("should filter by name", function() {
+        var input = listView.getDOMNode()
+          .querySelector(".contact-filter-container input");
+
+        React.addons.TestUtils.Simulate.change(input,
+                                               { target: { value: "Ally" } });
+        var contactList = listView.getDOMNode().querySelectorAll(".contact");
+
+        expect(contactList.length).to.eql(1);
+      });
+
+      it("should filter by email", function() {
+        var input = listView.getDOMNode()
+          .querySelector(".contact-filter-container input");
+
+        React.addons.TestUtils.Simulate.change(input,
+                                               { target: { value: "@hotmail" } });
+        var contactList = listView.getDOMNode().querySelectorAll(".contact");
+
+        expect(contactList.length).to.eql(1);
+      });
+
+      it("should filter by phone number", function() {
+        var input = listView.getDOMNode()
+          .querySelector(".contact-filter-container input");
+
+        React.addons.TestUtils.Simulate.change(input,
+                                               { target: { value: "12345678" } });
+        var contactList = listView.getDOMNode().querySelectorAll(".contact");
+
+        expect(contactList.length).to.eql(1);
+      });
     });
 
     describe("#handleContactAction", function() {
       beforeEach(function() {
         sandbox.stub(navigator.mozLoop.contacts, "getAll", function(cb) {
           cb(null, []);
         });
         listView = TestUtils.renderIntoDocument(
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -49,17 +49,17 @@ var fakeRooms = [
     "expiresAt": 1405534180,
     "participants": [
        { "displayName": "Alexis", "account": "alexis@example.com", "roomConnectionId": "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
        { "displayName": "Adam", "roomConnectionId": "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
      ]
   }
 ];
 
-var fakeContacts = [{
+var fakeManyContacts = [{
   id: 1,
   _guid: 1,
   name: ["Ally Avocado"],
   email: [{
     "pref": true,
     "type": ["work"],
     "value": "ally@mail.com"
   }],
@@ -107,17 +107,54 @@ var fakeContacts = [{
   email: [{
     "pref": true,
     "type": ["work"],
     "value": "dd@dragons.net"
   }],
   category: ["google"],
   published: 1406798311748,
   updated: 1406798311748
+}, {
+  id: 5,
+  _guid: 5,
+  name: ["Erin J. Bazile"],
+  email: [{
+    "pref": true,
+    "type": ["work"],
+    "value": "erinjbazile@armyspy.com"
+  }],
+  category: ["google"],
+  published: 1406798311748,
+  updated: 1406798311748
+}, {
+  id: 6,
+  _guid: 6,
+  name: ["Kelly F. Maldanado"],
+  email: [{
+    "pref": true,
+    "type": ["work"],
+    "value": "kellyfmaldonado@jourrapide.com"
+  }],
+  category: ["google"],
+  published: 1406798311748,
+  updated: 1406798311748
+}, {
+  id: 7,
+  _guid: 7,
+  name: ["John J. Brown"],
+  email: [{
+    "pref": true,
+    "type": ["work"],
+    "value": "johnjbrow@johndoe.com"
+  }],
+  category: ["google"],
+  published: 1406798311748,
+  updated: 1406798311748
 }];
+var fakeFewerContacts = fakeManyContacts.slice(0, 4);
 
 (function() {
   "use strict";
 
   /**
    * Faking the mozLoop object which doesn't exist in regular web pages.
    * @type {Object}
    */
@@ -147,17 +184,17 @@ var fakeContacts = [{
       callback({
         previews: ["chrome://branding/content/about-logo.png"],
         description: "sample webpage description",
         url: "https://www.example.com"
       });
     },
     contacts: {
       getAll: function(callback) {
-        callback(null, [].concat(fakeContacts));
+        callback(null, [].concat(fakeManyContacts));
       },
       on: function() {}
     },
     rooms: {
       getAll: function(version, callback) {
         callback(null, [].concat(fakeRooms));
       },
       on: function() {}
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* global Frame:false uncaughtError:true fakeContacts:true */
+/* global Frame:false uncaughtError:true fakeManyContacts:true fakeFewerContacts:true */
 
 (function() {
   "use strict";
 
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener("DOMContentLoaded", loop.panel.init);
   document.removeEventListener("DOMContentLoaded", loop.conversation.init);
 
@@ -464,24 +464,36 @@
   mockMozLoopLoggedInLongEmail.userProfile = {
     email: "reallyreallylongtext@example.com",
     uid: "0354b278a381d3cb408bb46ffc01266"
   };
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mozLoopNoContacts = _.cloneDeep(navigator.mozLoop);
+  mozLoopNoContacts.contacts.getAll = function(callback) {
+    callback(null, []);
+  };
   mozLoopNoContacts.userProfile = {
     email: "reallyreallylongtext@example.com",
     uid: "0354b278a381d3cb408bb46ffc01266"
   };
   mozLoopNoContacts.contacts.getAll = function(callback) {
     callback(null, []);
   };
 
+  var mozLoopNoContactsFilter = _.cloneDeep(navigator.mozLoop);
+  mozLoopNoContactsFilter.userProfile = {
+    email: "reallyreallylongtext@example.com",
+    uid: "0354b278a381d3cb408bb46ffc01266"
+  };
+  mozLoopNoContactsFilter.contacts.getAll = function(callback) {
+    callback(null, fakeFewerContacts); // Defined in fake-mozLoop.js.
+  };
+
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
@@ -534,17 +546,17 @@
       ],
       "14x14": ["audio", "audio-active", "audio-disabled", "facemute",
         "facemute-active", "facemute-disabled", "hangup", "hangup-active",
         "hangup-disabled", "hello", "hello-hover", "hello-active",
         "incoming", "incoming-active", "incoming-disabled",
         "link", "link-active", "link-disabled", "mute", "mute-active",
         "mute-disabled", "pause", "pause-active", "pause-disabled", "video",
         "video-white", "video-active", "video-disabled", "volume", "volume-active",
-        "volume-disabled"
+        "volume-disabled", "clear", "magnifier"
       ],
       "16x16": ["add", "add-hover", "add-active", "audio", "audio-hover", "audio-active",
         "block", "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
         "contacts-active", "copy", "checkmark", "delete", "globe", "google", "google-hover",
         "google-active", "history", "history-hover", "history-active", "leave",
         "screen-white", "screenmute-white", "settings", "settings-hover", "settings-active",
         "share-darkgrey", "tag", "tag-hover", "tag-active", "trash", "unblock",
         "unblock-hover", "unblock-active", "video", "video-hover", "video-active", "tour",
@@ -751,16 +763,30 @@
                            notifications: notifications, 
                            roomStore: roomStore, 
                            selectedTab: "contacts"})
               )
             ), 
             React.createElement(FramedExample, {cssClass: "fx-embedded-panel", 
                            dashed: true, 
                            height: 410, 
+                           summary: "Contact list tab (no search filter)", 
+                           width: 332}, 
+              React.createElement("div", {className: "panel"}, 
+                React.createElement(PanelView, {client: mockClient, 
+                           dispatcher: dispatcher, 
+                           mozLoop: mozLoopNoContactsFilter, 
+                           notifications: notifications, 
+                           roomStore: roomStore, 
+                           selectedTab: "contacts"})
+              )
+            ), 
+            React.createElement(FramedExample, {cssClass: "fx-embedded-panel", 
+                           dashed: true, 
+                           height: 410, 
                            summary: "Contact list tab long email", 
                            width: 332}, 
               React.createElement("div", {className: "panel"}, 
                 React.createElement(PanelView, {client: mockClient, 
                            dispatcher: dispatcher, 
                            mozLoop: mockMozLoopLoggedInLongEmail, 
                            notifications: notifications, 
                            roomStore: roomStore, 
@@ -877,17 +903,17 @@
 
           React.createElement(Section, {name: "ContactDetail"}, 
             React.createElement(FramedExample, {cssClass: "fx-embedded-panel", 
                            dashed: true, 
                            height: 272, 
                            summary: "ContactDetail", 
                            width: 300}, 
               React.createElement("div", {className: "panel force-menu-show"}, 
-                React.createElement(ContactDetail, {contact: fakeContacts[0], 
+                React.createElement(ContactDetail, {contact: fakeManyContacts[0], 
                                handleContactAction: function() {}})
               )
             )
           ), 
 
           React.createElement(Section, {name: "ContactDropdown"}, 
             React.createElement(FramedExample, {cssClass: "fx-embedded-panel", 
                            dashed: true, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* global Frame:false uncaughtError:true fakeContacts:true */
+/* global Frame:false uncaughtError:true fakeManyContacts:true fakeFewerContacts:true */
 
 (function() {
   "use strict";
 
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener("DOMContentLoaded", loop.panel.init);
   document.removeEventListener("DOMContentLoaded", loop.conversation.init);
 
@@ -464,24 +464,36 @@
   mockMozLoopLoggedInLongEmail.userProfile = {
     email: "reallyreallylongtext@example.com",
     uid: "0354b278a381d3cb408bb46ffc01266"
   };
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mozLoopNoContacts = _.cloneDeep(navigator.mozLoop);
+  mozLoopNoContacts.contacts.getAll = function(callback) {
+    callback(null, []);
+  };
   mozLoopNoContacts.userProfile = {
     email: "reallyreallylongtext@example.com",
     uid: "0354b278a381d3cb408bb46ffc01266"
   };
   mozLoopNoContacts.contacts.getAll = function(callback) {
     callback(null, []);
   };
 
+  var mozLoopNoContactsFilter = _.cloneDeep(navigator.mozLoop);
+  mozLoopNoContactsFilter.userProfile = {
+    email: "reallyreallylongtext@example.com",
+    uid: "0354b278a381d3cb408bb46ffc01266"
+  };
+  mozLoopNoContactsFilter.contacts.getAll = function(callback) {
+    callback(null, fakeFewerContacts); // Defined in fake-mozLoop.js.
+  };
+
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
@@ -534,17 +546,17 @@
       ],
       "14x14": ["audio", "audio-active", "audio-disabled", "facemute",
         "facemute-active", "facemute-disabled", "hangup", "hangup-active",
         "hangup-disabled", "hello", "hello-hover", "hello-active",
         "incoming", "incoming-active", "incoming-disabled",
         "link", "link-active", "link-disabled", "mute", "mute-active",
         "mute-disabled", "pause", "pause-active", "pause-disabled", "video",
         "video-white", "video-active", "video-disabled", "volume", "volume-active",
-        "volume-disabled"
+        "volume-disabled", "clear", "magnifier"
       ],
       "16x16": ["add", "add-hover", "add-active", "audio", "audio-hover", "audio-active",
         "block", "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
         "contacts-active", "copy", "checkmark", "delete", "globe", "google", "google-hover",
         "google-active", "history", "history-hover", "history-active", "leave",
         "screen-white", "screenmute-white", "settings", "settings-hover", "settings-active",
         "share-darkgrey", "tag", "tag-hover", "tag-active", "trash", "unblock",
         "unblock-hover", "unblock-active", "video", "video-hover", "video-active", "tour",
@@ -751,16 +763,30 @@
                            notifications={notifications}
                            roomStore={roomStore}
                            selectedTab="contacts" />
               </div>
             </FramedExample>
             <FramedExample cssClass="fx-embedded-panel"
                            dashed={true}
                            height={410}
+                           summary="Contact list tab (no search filter)"
+                           width={332}>
+              <div className="panel">
+                <PanelView client={mockClient}
+                           dispatcher={dispatcher}
+                           mozLoop={mozLoopNoContactsFilter}
+                           notifications={notifications}
+                           roomStore={roomStore}
+                           selectedTab="contacts" />
+              </div>
+            </FramedExample>
+            <FramedExample cssClass="fx-embedded-panel"
+                           dashed={true}
+                           height={410}
                            summary="Contact list tab long email"
                            width={332}>
               <div className="panel">
                 <PanelView client={mockClient}
                            dispatcher={dispatcher}
                            mozLoop={mockMozLoopLoggedInLongEmail}
                            notifications={notifications}
                            roomStore={roomStore}
@@ -877,17 +903,17 @@
 
           <Section name="ContactDetail">
             <FramedExample cssClass="fx-embedded-panel"
                            dashed={true}
                            height={272}
                            summary="ContactDetail"
                            width={300}>
               <div className="panel force-menu-show">
-                <ContactDetail contact={fakeContacts[0]}
+                <ContactDetail contact={fakeManyContacts[0]}
                                handleContactAction={function() {}} />
               </div>
             </FramedExample>
           </Section>
 
           <Section name="ContactDropdown">
             <FramedExample cssClass="fx-embedded-panel"
                            dashed={true}
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1788,33 +1788,16 @@ BrowserGlue.prototype = {
       // Need to migrate only if toolbar is customized and the element is not found.
       if (currentset &&
           currentset.indexOf("bookmarks-menu-button-container") == -1) {
         currentset += ",bookmarks-menu-button-container";
         xulStore.setValue(BROWSER_DOCURL, "nav-bar", "currentset", currentset);
       }
     }
 
-    if (currentUIVersion < 3) {
-      // This code merges the reload/stop/go button into the url bar.
-      let currentset = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "currentset");
-      // Need to migrate only if toolbar is customized and all 3 elements are found.
-      if (currentset &&
-          currentset.indexOf("reload-button") != -1 &&
-          currentset.indexOf("stop-button") != -1 &&
-          currentset.indexOf("urlbar-container") != -1 &&
-          currentset.indexOf("urlbar-container,reload-button,stop-button") == -1) {
-        currentset = currentset.replace(/(^|,)reload-button($|,)/, "$1$2")
-                               .replace(/(^|,)stop-button($|,)/, "$1$2")
-                               .replace(/(^|,)urlbar-container($|,)/,
-                                        "$1urlbar-container,reload-button,stop-button$2");
-        xulStore.setValue(BROWSER_DOCURL, "nav-bar", "currentset", currentset);
-      }
-    }
-
     if (currentUIVersion < 4) {
       // This code moves the home button to the immediate left of the bookmarks menu button.
       let currentset = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "currentset");
       // Need to migrate only if toolbar is customized and the elements are found.
       if (currentset &&
           currentset.indexOf("home-button") != -1 &&
           currentset.indexOf("bookmarks-menu-button-container") != -1) {
         currentset = currentset.replace(/(^|,)home-button($|,)/, "$1$2")
@@ -1916,20 +1899,17 @@ BrowserGlue.prototype = {
     if (currentUIVersion < 14) {
       // DOM Storage doesn't specially handle about: pages anymore.
       let path = OS.Path.join(OS.Constants.Path.profileDir,
                               "chromeappsstore.sqlite");
       OS.File.remove(path);
     }
 
     if (currentUIVersion < 16) {
-      let isCollapsed = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "collapsed");
-      if (isCollapsed == "true") {
-        xulStore.setValue(BROWSER_DOCURL, "nav-bar", "collapsed", "false");
-      }
+      xulStore.removeValue(BROWSER_DOCURL, "nav-bar", "collapsed");
     }
 
     // Insert the bookmarks-menu-button into the nav-bar if it isn't already
     // there.
     if (currentUIVersion < 17) {
       let currentset = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "currentset");
       // Need to migrate only if toolbar is customized.
       if (currentset) {
@@ -1979,22 +1959,16 @@ BrowserGlue.prototype = {
       }
     }
 
     if (currentUIVersion < 20) {
       // Remove persisted collapsed state from TabsToolbar.
       xulStore.removeValue(BROWSER_DOCURL, "TabsToolbar", "collapsed");
     }
 
-    if (currentUIVersion < 21) {
-      // Make sure the 'toolbarbutton-1' class will always be present from here
-      // on out.
-      xulStore.removeValue(BROWSER_DOCURL, "bookmarks-menu-button", "class");
-    }
-
     if (currentUIVersion < 22) {
       // Reset the Sync promobox count to promote the new FxAccount-based Sync.
       Services.prefs.clearUserPref("browser.syncPromoViewsLeft");
       Services.prefs.clearUserPref("browser.syncPromoViewsLeftMap");
     }
 
     if (currentUIVersion < 23) {
       const kSelectedEnginePref = "browser.search.selectedEngine";
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -111,19 +111,16 @@ var gPrivacyPane = {
     setEventListener("privateBrowsingAutoStart", "command",
                      gPrivacyPane.updateAutostart);
     setEventListener("cookieExceptions", "command",
                      gPrivacyPane.showCookieExceptions);
     setEventListener("showCookiesButton", "command",
                      gPrivacyPane.showCookies);
     setEventListener("clearDataSettings", "command",
                      gPrivacyPane.showClearPrivateDataSettings);
-
-    document.getElementById("searchesSuggestion").hidden = 
-      !Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
   },
 
   // HISTORY MODE
 
   /**
    * The list of preferences which affect the initial history mode settings.
    * If the auto start private browsing mode pref is active, the initial
    * history mode would be set to "Don't remember anything".
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -36,19 +36,16 @@
               name="browser.urlbar.suggest.bookmark"
               type="bool"/>
   <preference id="browser.urlbar.suggest.history"
               name="browser.urlbar.suggest.history"
               type="bool"/>
   <preference id="browser.urlbar.suggest.openpage"
               name="browser.urlbar.suggest.openpage"
               type="bool"/>
-  <preference id="browser.urlbar.suggest.searches"
-              name="browser.urlbar.suggest.searches"
-              type="bool"/>
 
   <!-- History -->
   <preference id="places.history.enabled"
               name="places.history.enabled"
               type="bool"/>
   <preference id="browser.formfill.enable"
               name="browser.formfill.enable"
               type="bool"/>
@@ -256,13 +253,12 @@
             accesskey="&locbar.history.accesskey;"
             preference="browser.urlbar.suggest.history"/>
   <checkbox id="bookmarkSuggestion" label="&locbar.bookmarks.label;"
             accesskey="&locbar.bookmarks.accesskey;"
             preference="browser.urlbar.suggest.bookmark"/>
   <checkbox id="openpageSuggestion" label="&locbar.openpage.label;"
             accesskey="&locbar.openpage.accesskey;"
             preference="browser.urlbar.suggest.openpage"/>
-  <checkbox id="searchesSuggestion" label="&locbar.searches.label;"
-            hidden="true"
-            accesskey="&locbar.searches.accesskey;"
-            preference="browser.urlbar.suggest.searches"/>
+  <label class="text-link" onclick="if (event.button == 0) gotoPref('search')">
+    &suggestionSettings.label;
+  </label>
 </groupbox>
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -24,16 +24,35 @@ document.addEventListener("Initialized",
   if (document.location.hash == "#search")
     document.location.hash = "";
 });
 
 var gEngineView = null;
 
 var gSearchPane = {
 
+  /**
+   * Initialize autocomplete to ensure prefs are in sync.
+   */
+  _initAutocomplete: function () {
+    let unifiedCompletePref = false;
+    try {
+      unifiedCompletePref =
+        Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+    } catch (ex) {}
+
+    if (unifiedCompletePref) {
+      Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
+                .getService(Components.interfaces.mozIPlacesAutoComplete);
+    } else {
+      Components.classes["@mozilla.org/autocomplete/search;1?name=history"]
+                .getService(Components.interfaces.mozIPlacesAutoComplete);
+    }
+  },
+
   init: function ()
   {
     gEngineView = new EngineView(new EngineStore());
     document.getElementById("engineList").view = gEngineView;
     this.buildDefaultEngineDropDown();
 
     window.addEventListener("click", this, false);
     window.addEventListener("command", this, false);
@@ -41,16 +60,28 @@ var gSearchPane = {
     window.addEventListener("keypress", this, false);
     window.addEventListener("select", this, false);
     window.addEventListener("blur", this, true);
 
     Services.obs.addObserver(this, "browser-search-engine-modified", false);
     window.addEventListener("unload", () => {
       Services.obs.removeObserver(this, "browser-search-engine-modified", false);
     });
+
+    this._initAutocomplete();
+
+    let urlbarSuggests = document.getElementById("urlBarSuggestion");
+    urlbarSuggests.hidden = !Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
+
+    let suggestsPref = document.getElementById("browser.search.suggest.enabled")
+    let updateSuggestsCheckbox = () => {
+      urlbarSuggests.disabled = !suggestsPref.value;
+    }
+    suggestsPref.addEventListener("change", updateSuggestsCheckbox);
+    updateSuggestsCheckbox();
   },
 
   buildDefaultEngineDropDown: function() {
     // This is called each time something affects the list of engines.
     let list = document.getElementById("defaultEngine");
     let currentEngine;
 
     // First, try to preserve the current selection.
--- a/browser/components/preferences/in-content/search.xul
+++ b/browser/components/preferences/in-content/search.xul
@@ -1,14 +1,18 @@
     <preferences id="searchPreferences" hidden="true" data-category="paneSearch">
 
       <preference id="browser.search.suggest.enabled"
                   name="browser.search.suggest.enabled"
                   type="bool"/>
 
+      <preference id="browser.urlbar.suggest.searches"
+                  name="browser.urlbar.suggest.searches"
+                  type="bool"/>
+
       <preference id="browser.search.hiddenOneOffs"
                   name="browser.search.hiddenOneOffs"
                   type="unichar"/>
 
       <preference id="browser.search.redirectWindowsSearch"
                   name="browser.search.redirectWindowsSearch"
                   type="bool"/>
 
@@ -32,16 +36,22 @@
       <label>&chooseYourDefaultSearchEngine.label;</label>
       <menulist id="defaultEngine">
         <menupopup/>
       </menulist>
       <checkbox id="suggestionsInSearchFieldsCheckbox"
                 label="&provideSearchSuggestions.label;"
                 accesskey="&provideSearchSuggestions.accesskey;"
                 preference="browser.search.suggest.enabled"/>
+      <hbox class="indent">
+        <checkbox id="urlBarSuggestion" label="&showURLBarSuggestions.label;"
+                  hidden="true"
+                  accesskey="&showURLBarSuggestions.accesskey;"
+                  preference="browser.urlbar.suggest.searches"/>
+      </hbox>
       <checkbox id="redirectSearchCheckbox"
                 label="&redirectWindowsSearch.label;"
                 accesskey="&redirectWindowsSearch.accesskey;"
                 preference="browser.search.redirectWindowsSearch"/>
     </groupbox>
 
     <groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch">
       <caption label="&oneClickSearchEngines.label;"/>
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -20,14 +20,15 @@ skip-if = !healthreport || (os == 'linux
 [browser_proxy_backup.js]
 [browser_privacypane_1.js]
 [browser_privacypane_3.js]
 [browser_privacypane_4.js]
 [browser_privacypane_5.js]
 [browser_privacypane_8.js]
 skip-if = e10s # Bug ?????? -  "leaked until shutdown [nsGlobalWindow #99 about:preferences]"
 [browser_sanitizeOnShutdown_prefLocked.js]
+[browser_searchsuggestions.js]
 [browser_subdialogs.js]
 skip-if = e10s # Bug 1087114
 support-files = subdialog.xul
 [browser_telemetry.js]
 # Skip this test on Android and B2G as FHR and Telemetry are separate systems there.
 skip-if = !healthreport || !telemetry || (os == 'linux' && debug) || (os == 'android') || (os == 'b2g')
--- a/browser/components/preferences/in-content/tests/browser_privacypane_5.js
+++ b/browser/components/preferences/in-content/tests/browser_privacypane_5.js
@@ -15,13 +15,10 @@ function test() {
   let tests = [
     test_locbar_suggestion_retention("history", true),
     test_locbar_suggestion_retention("bookmark", true),
     test_locbar_suggestion_retention("openpage", false),
     test_locbar_suggestion_retention("history", true),
     test_locbar_suggestion_retention("history", false),
   ];
 
-  if (Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete"))
-    tests.push(test_locbar_suggestion_retention("searches", true));
-
   run_test_subset(tests);
 }
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_searchsuggestions.js
@@ -0,0 +1,43 @@
+let original = Services.prefs.getBoolPref("browser.search.suggest.enabled");
+
+registerCleanupFunction(() => {
+  Services.prefs.setBoolPref("browser.search.suggest.enabled", original);
+});
+
+// Open with suggestions enabled
+add_task(function*() {
+  Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+
+  yield openPreferencesViaOpenPreferencesAPI("search", undefined, { leaveOpen: true });
+
+  let doc = gBrowser.selectedBrowser.contentDocument;
+  let urlbarBox = doc.getElementById("urlBarSuggestion");
+  ok(!urlbarBox.disabled, "Checkbox should be enabled");
+
+  Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+
+  ok(urlbarBox.disabled, "Checkbox should be disabled");
+
+  gBrowser.removeCurrentTab();
+});
+
+// Open with suggestions disabled
+add_task(function*() {
+  Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+
+  yield openPreferencesViaOpenPreferencesAPI("search", undefined, { leaveOpen: true });
+
+  let doc = gBrowser.selectedBrowser.contentDocument;
+  let urlbarBox = doc.getElementById("urlBarSuggestion");
+  ok(urlbarBox.disabled, "Checkbox should be disabled");
+
+  Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+
+  ok(!urlbarBox.disabled, "Checkbox should be enabled");
+
+  gBrowser.removeCurrentTab();
+});
+
+add_task(function*() {
+  Services.prefs.setBoolPref("browser.search.suggest.enabled", original);
+});
--- a/browser/devtools/shared/telemetry.js
+++ b/browser/devtools/shared/telemetry.js
@@ -146,19 +146,19 @@ Telemetry.prototype = {
       timerHistogram: "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS"
     },
     netmonitor: {
       histogram: "DEVTOOLS_NETMONITOR_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_NETMONITOR_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS"
     },
     storage: {
-       histogram: "DEVTOOLS_STORAGE_OPENED_BOOLEAN",
-       userHistogram: "DEVTOOLS_STORAGE_OPENED_PER_USER_FLAG",
-       timerHistogram: "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS"
+      histogram: "DEVTOOLS_STORAGE_OPENED_BOOLEAN",
+      userHistogram: "DEVTOOLS_STORAGE_OPENED_PER_USER_FLAG",
+      timerHistogram: "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS"
     },
     tilt: {
       histogram: "DEVTOOLS_TILT_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_TILT_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_TILT_TIME_ACTIVE_SECONDS"
     },
     paintflashing: {
       histogram: "DEVTOOLS_PAINTFLASHING_OPENED_BOOLEAN",
@@ -192,16 +192,33 @@ Telemetry.prototype = {
       userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
     },
     webide: {
       histogram: "DEVTOOLS_WEBIDE_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_WEBIDE_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS"
     },
+    webideProjectEditor: {
+      histogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_BOOLEAN",
+      userHistogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_PER_USER_FLAG",
+      timerHistogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_TIME_ACTIVE_SECONDS"
+    },
+    webideProjectEditorSave: {
+      histogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_SAVE_BOOLEAN",
+      userHistogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_SAVE_PER_USER_FLAG",
+    },
+    webideNewProject: {
+      histogram: "DEVTOOLS_WEBIDE_NEW_PROJECT_BOOLEAN",
+      userHistogram: "DEVTOOLS_WEBIDE_NEW_PROJECT_PER_USER_FLAG",
+    },
+    webideImportProject: {
+      histogram: "DEVTOOLS_WEBIDE_IMPORT_PROJECT_BOOLEAN",
+      userHistogram: "DEVTOOLS_WEBIDE_IMPORT_PROJECT_PER_USER_FLAG",
+    },
     custom: {
       histogram: "DEVTOOLS_CUSTOM_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_CUSTOM_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS"
     }
   },
 
   /**
@@ -220,16 +237,25 @@ Telemetry.prototype = {
     if (charts.userHistogram) {
       this.logOncePerBrowserVersion(charts.userHistogram, true);
     }
     if (charts.timerHistogram) {
       this.startTimer(charts.timerHistogram);
     }
   },
 
+  /**
+   * Record that an action occurred.  Aliases to `toolOpened`, so it's just for
+   * readability at the call site for cases where we aren't actually opening
+   * tools.
+   */
+  actionOccurred(id) {
+    this.toolOpened(id);
+  },
+
   toolClosed: function(id) {
     let charts = this._histograms[id];
 
     if (!charts || !charts.timerHistogram) {
       return;
     }
 
     this.stopTimer(charts.timerHistogram);
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -142,16 +142,18 @@ let UI = {
     AppManager.off("app-manager-update", this.appManagerUpdate);
     AppManager.destroy();
     Simulators.off("configure", this.configureSimulator);
     projectList.destroy();
     runtimeList.destroy();
     window.removeEventListener("message", this.onMessage);
     this.updateConnectionTelemetry();
     this._telemetry.toolClosed("webide");
+    this._telemetry.toolClosed("webideProjectEditor");
+    this._telemetry.destroy();
   },
 
   canCloseProject: function() {
     if (this.projecteditor) {
       return this.projecteditor.confirmUnsaved();
     }
     return true;
   },
@@ -665,39 +667,45 @@ let UI = {
 
   destroyProjectEditor: function() {
     if (this.projecteditor) {
       this.projecteditor.destroy();
       this.projecteditor = null;
     }
   },
 
-  updateProjectEditorMenusVisibility: function() {
+  /**
+   * Called when selecting or deselecting the project editor panel.
+   */
+  onChangeProjectEditorSelected: function() {
     if (this.projecteditor) {
       let panel = document.querySelector("#deck").selectedPanel;
       if (panel && panel.id == "deck-panel-projecteditor") {
         this.projecteditor.menuEnabled = true;
+        this._telemetry.toolOpened("webideProjectEditor");
       } else {
         this.projecteditor.menuEnabled = false;
+        this._telemetry.toolClosed("webideProjectEditor");
       }
     }
   },
 
   getProjectEditor: function() {
     if (this.projecteditor) {
       return this.projecteditor.loaded;
     }
 
     let projecteditorIframe = document.querySelector("#deck-panel-projecteditor");
     this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe, {
       menubar: document.querySelector("#main-menubar"),
       menuindex: 1
     });
-    this.projecteditor.on("onEditorSave", (editor, resource) => {
+    this.projecteditor.on("onEditorSave", () => {
       AppManager.validateAndUpdateProject(AppManager.selectedProject);
+      this._telemetry.actionOccurred("webideProjectEditorSave");
     });
     return this.projecteditor.loaded;
   },
 
   updateProjectEditorHeader: function() {
     let project = AppManager.selectedProject;
     if (!project || !this.projecteditor) {
       return;
@@ -743,21 +751,21 @@ let UI = {
         !this.isProjectEditorEnabled() ||
         forceDetailsOnly) {
       this.selectDeckPanel("details");
       return;
     }
 
     // Show ProjectEditor
 
-    this.selectDeckPanel("projecteditor");
-
     this.getProjectEditor().then(() => {
       this.updateProjectEditorHeader();
     }, console.error);
+
+    this.selectDeckPanel("projecteditor");
   },
 
   autoStartProject: Task.async(function*() {
     let project = AppManager.selectedProject;
 
     if (!project) {
       return;
     }
@@ -801,16 +809,18 @@ let UI = {
         project = AppProjects.get(isPackaged ? source.path : source);
       } else {
         throw e;
       }
     }
 
     // Select project
     AppManager.selectedProject = project;
+
+    this._telemetry.actionOccurred("webideImportProject");
   }),
 
   // Remember the last selected project on the runtime
   saveLastSelectedProject: function() {
     let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject");
     if (!shouldRestore) {
       return;
     }
@@ -914,25 +924,25 @@ let UI = {
     this.resetFocus();
     let panel = deck.querySelector("#deck-panel-" + id);
     let lazysrc = panel.getAttribute("lazysrc");
     if (lazysrc) {
       panel.removeAttribute("lazysrc");
       panel.setAttribute("src", lazysrc);
     }
     deck.selectedPanel = panel;
-    this.updateProjectEditorMenusVisibility();
+    this.onChangeProjectEditorSelected();
     this.updateToolboxFullscreenState();
   },
 
   resetDeck: function() {
     this.resetFocus();
     let deck = document.querySelector("#deck");
     deck.selectedPanel = null;
-    this.updateProjectEditorMenusVisibility();
+    this.onChangeProjectEditorSelected();
   },
 
   buildIDToDate(buildID) {
     let fields = buildID.match(/(\d{4})(\d{2})(\d{2})/);
     // Date expects 0 - 11 for months
     return new Date(fields[1], Number.parseInt(fields[2]) - 1, fields[3]);
   },
 
--- a/browser/devtools/webide/modules/project-list.js
+++ b/browser/devtools/webide/modules/project-list.js
@@ -6,28 +6,30 @@ const {Cu} = require("chrome");
 
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {AppProjects} = require("devtools/app-manager/app-projects");
 const {AppManager} = require("devtools/webide/app-manager");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 const utils = require("devtools/webide/utils");
+const Telemetry = require("devtools/shared/telemetry");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 let ProjectList;
 
 module.exports = ProjectList = function(win, parentWindow) {
   EventEmitter.decorate(this);
   this._doc = win.document;
   this._UI = parentWindow.UI;
   this._parentWindow = parentWindow;
   this._panelNodeEl = "toolbarbutton";
   this._sidebarsEnabled = Services.prefs.getBoolPref("devtools.webide.sidebars");
+  this._telemetry = new Telemetry();
 
   if (this._sidebarsEnabled) {
     this._panelNodeEl = "div";
   }
 
   this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
   this._UI.on("webide-update", this.onWebIDEUpdate);
 
@@ -72,28 +74,31 @@ ProjectList.prototype = {
    * testOptions: {       chrome mochitest support
    *   folder: nsIFile,   where to store the app
    *   index: Number,     index of the app in the template list
    *   name: String       name of the app
    * }
    */
   newApp: function(testOptions) {
     let parentWindow = this._parentWindow;
+    let self = this;
     return this._UI.busyUntil(Task.spawn(function*() {
       // Open newapp.xul, which will feed ret.location
       let ret = {location: null, testOptions: testOptions};
       parentWindow.openDialog("chrome://webide/content/newapp.xul", "newapp", "chrome,modal", ret);
       if (!ret.location)
         return;
 
       // Retrieve added project
       let project = AppProjects.get(ret.location);
 
       // Select project
       AppManager.selectedProject = project;
+
+      self._telemetry.actionOccurred("webideNewProject");
     }), "creating new app");
   },
 
   importPackagedApp: function(location) {
     let parentWindow = this._parentWindow;
     let UI = this._UI;
     return UI.busyUntil(Task.spawn(function*() {
       let directory = utils.getPackagedDirectory(parentWindow, location);
--- a/browser/devtools/webide/test/sidebars/test_telemetry.html
+++ b/browser/devtools/webide/test/sidebars/test_telemetry.html
@@ -159,35 +159,45 @@
           startConnection(win, docRuntime, type, index);
           yield waitUntilConnected(win);
         });
       }
 
       function checkResults() {
         let result = Telemetry.prototype.telemetryInfo;
         for (let [histId, value] of Iterator(result)) {
-          if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+          if (histId.endsWith("_PER_USER_FLAG")) {
             ok(value.length === 1 && !!value[0],
                "Per user value " + histId + " has a single value of true");
-          } else if (histId.endsWith("OPENED_BOOLEAN")) {
+          } else if (histId === "DEVTOOLS_WEBIDE_IMPORT_PROJECT_BOOLEAN") {
+            ok(value.length === 1 && !!value[0],
+               histId + " has 1 successful entry");
+          } else if (histId ===
+                     "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_BOOLEAN") {
+            ok(value.length === 1 && !!value[0],
+               histId + " has 1 successful entry");
+          } else if (histId === "DEVTOOLS_WEBIDE_OPENED_BOOLEAN") {
             ok(value.length > 1, histId + " has more than one entry");
 
             let okay = value.every(function(element) {
               return !!element;
             });
 
             ok(okay, "All " + histId + " entries are true");
-          } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+          } else if (histId.endsWith("WEBIDE_TIME_ACTIVE_SECONDS")) {
             ok(value.length > 1, histId + " has more than one entry");
 
             let okay = value.every(function(element) {
               return element > 0;
             });
 
             ok(okay, "All " + histId + " entries have time > 0");
+          } else if (histId.endsWith("EDITOR_TIME_ACTIVE_SECONDS")) {
+            ok(value.length === 1 && value[0] > 0,
+               histId + " has 1 entry with time > 0");
           } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_RESULT") {
             ok(value.length === 6, histId + " has 6 connection results");
 
             let okay = value.every(function(element) {
               return !!element;
             });
 
             ok(okay, "All " + histId + " connections succeeded");
--- a/browser/devtools/webide/test/test_telemetry.html
+++ b/browser/devtools/webide/test/test_telemetry.html
@@ -159,35 +159,45 @@
           startConnection(win, type, index);
           yield waitUntilConnected(win);
         });
       }
 
       function checkResults() {
         let result = Telemetry.prototype.telemetryInfo;
         for (let [histId, value] of Iterator(result)) {
-          if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+          if (histId.endsWith("_PER_USER_FLAG")) {
             ok(value.length === 1 && !!value[0],
                "Per user value " + histId + " has a single value of true");
-          } else if (histId.endsWith("OPENED_BOOLEAN")) {
+          } else if (histId === "DEVTOOLS_WEBIDE_IMPORT_PROJECT_BOOLEAN") {
+            ok(value.length === 1 && !!value[0],
+               histId + " has 1 successful entry");
+          } else if (histId ===
+                     "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_BOOLEAN") {
+            ok(value.length === 1 && !!value[0],
+               histId + " has 1 successful entry");
+          } else if (histId === "DEVTOOLS_WEBIDE_OPENED_BOOLEAN") {
             ok(value.length > 1, histId + " has more than one entry");
 
             let okay = value.every(function(element) {
               return !!element;
             });
 
             ok(okay, "All " + histId + " entries are true");
-          } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+          } else if (histId.endsWith("WEBIDE_TIME_ACTIVE_SECONDS")) {
             ok(value.length > 1, histId + " has more than one entry");
 
             let okay = value.every(function(element) {
               return element > 0;
             });
 
             ok(okay, "All " + histId + " entries have time > 0");
+          } else if (histId.endsWith("EDITOR_TIME_ACTIVE_SECONDS")) {
+            ok(value.length === 1 && value[0] > 0,
+               histId + " has 1 entry with time > 0");
           } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_RESULT") {
             ok(value.length === 6, histId + " has 6 connection results");
 
             let okay = value.every(function(element) {
               return !!element;
             });
 
             ok(okay, "All " + histId + " connections succeeded");
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -81,17 +81,17 @@ settings_menu_item_settings=Settings
 settings_menu_item_signout=Sign Out
 settings_menu_item_signin=Sign In
 settings_menu_button_tooltip=Settings
 
 # Contact Strings (Panel)
 
 ## LOCALIZATION NOTE(contacts_search_placeholder): This is the placeholder text for
 ## the search field.
-contacts_search_placesholder=Search…
+contacts_search_placesholder=Type in contact name, email, phone number
 
 ## LOCALIZATION NOTE (new_contact_button): This is the button to open the
 ## new contact sub-panel.
 new_contact_button=New Contact
 ## LOCALIZATION NOTE (contact_form_*_placeholder):
 ## These are the placeholders for the inputs for entering or editing a contact
 ## Click the 'New Contact' button to see the fields.
 contact_form_name_placeholder=Name
@@ -126,28 +126,32 @@ import_contacts_failure_message=Some con
 ## when user's contacts have been successfully imported.
 ## Semicolon-separated list of plural forms. See:
 ## http://developer.mozilla.org/en/docs/Localization_and_Plurals
 ## In this item, don't translate the part between {{..}}
 import_contacts_success_message={{total}} contact was successfully imported.;{{total}} contacts were successfully imported.
 ## LOCALIZATION NOTE(sync_contacts_button): This button is displayed in place of
 ## importing_contacts_button once contacts have been imported once.
 sync_contacts_button=Sync Contacts
-## LOCALIZATION NOTE(no_contacts_message_heading): Title shown when user has no
+# LOCALIZATION NOTE(no_contacts_message_heading): Title shown when user has no
 ## contacts in his address book
 no_contacts_message_heading=No contacts yet
 ## LOCALIZATION NOTE(no_contacts_import_or_add): Subheading inviting the user
 ## to add people to his contact list
 no_contacts_import_or_add=Import or add someone
 ## LOCALIZATION NOTE(no_conversations_message_heading): Title shown when user
 ## has no conversations available.
 no_conversations_message_heading=There are no conversations yet
 ## LOCALIZATION NOTE(no_converastions_start_message): Subheading inviting the
 ## user to start a new conversation.
 no_conversations_start_message=start a new conversation!
+## LOCALIZATION NOTE(no_search_results_message_heading): Title to show when
+## search returned no matching results.
+no_search_results_message_heading=No matching results
+no_search_results_message_subheading=with your search, try again!
 
 ## LOCALIZATION NOTE(import_failed_description simple): Displayed when an import of
 ## contacts fails. This is displayed in the error field.
 import_failed_description_simple=Sorry, contact import failed
 import_failed_description_some=Some contacts could not be imported
 import_failed_support_button=Help
 
 ## LOCALIZATION NOTE(remove_contact_menu_button2): Displayed in the contact list in
deleted file mode 100644
--- a/browser/themes/linux/devtools/floating-scrollbars-light.css
+++ /dev/null
@@ -1,10 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-@import url("floating-scrollbars.css");
-
-scrollbar thumb {
-  background-color: rgba(170,170,170,0.2) !important;
-}
deleted file mode 100644
--- a/browser/themes/linux/devtools/floating-scrollbars.css
+++ /dev/null
@@ -1,35 +0,0 @@
-@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
-
-scrollbar {
-  -moz-appearance: none !important;
-  position: relative;
-  background-color: transparent;
-  background-image: none;
-  z-index: 2147483647;
-  padding: 2px;
-}
-
-/* Scrollbar code will reset the margin to the correct side depending on
-   where layout actually puts the scrollbar */
-scrollbar[orient="vertical"] {
-  margin-left: -10px;
-  min-width: 10px;
-  max-width: 10px;
-}
-
-scrollbar[orient="horizontal"] {
-  margin-top: -10px;
-  min-height: 10px;
-  max-height: 10px;
-}
-
-scrollbar thumb {
-  -moz-appearance: none !important;
-  border-width: 0px !important;
-  background-color: rgba(170,170,170,0.2) !important;
-  border-radius: 3px !important;
-}
-
-scrollbar scrollbarbutton, scrollbar gripper {
-  display: none;
-}
deleted file mode 100644
--- a/browser/themes/linux/devtools/netmonitor.css
+++ /dev/null
@@ -1,32 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-%include ../../shared/devtools/netmonitor.inc.css
-
-#headers-summary-resend {
-  padding: 4px;
-}
-
-#toggle-raw-headers {
-  padding: 4px;
-}
-
-.requests-menu-status-and-method {
-  width: 9em;
-}
-
-.requests-menu-security-and-domain {
-  width: 16vw;
-}
-
-.requests-menu-size {
-  width: 6em;
-}
-
-/* Responsive sidebar */
-@media (max-width: 700px) {
-  .requests-menu-header-button {
-    font-size: 85%;
-  }
-}
deleted file mode 100644
--- a/browser/themes/linux/devtools/webconsole.css
+++ /dev/null
@@ -1,9 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-%include ../../shared/devtools/webconsole.inc.css
-
-.jsterm-input-node {
-  width: 98%;
-}
deleted file mode 100644
--- a/browser/themes/linux/devtools/webconsole_networkpanel.css
+++ /dev/null
@@ -1,99 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-body {
-  font-family: sans-serif;
-  font-size: 11px;
-  background: #EEE;
-  color: #000;
-}
-
-#header {
-  padding: 5px;
-  overflow-x:auto;
-  display: block;
-}
-
-h1 {
-  font-size: 13px;
-  line-height: 15px;
-  padding: 3px 10px;
-  vertical-align: bottom;
-  margin: 0px;
-  background: linear-gradient(#BBB, #999);
-  border-radius: 2px;
-  text-shadow: #FFF 0px 1px 0px;
-}
-
-h1 .info {
-  font-size: 11px;
-  line-height: 15px;
-  vertical-align: bottom;
-  float: right;
-  color: #333;
-  padding-right: 3px;
-}
-
-.property-table {
-  padding: 2px 5px;
-  background: linear-gradient(#FFF, #F8F8F8);
-  color: #333;
-  width: 100%;
-  max-height: 330px;
-  overflow: auto;
-  display: block;
-}
-
-.property-name {
-  font-size: 11px;
-  font-weight: bold;
-  color: #000;
-  white-space: nowrap;
-  text-align: end;
-  vertical-align: top;
-  width: 10%;
-}
-
-.property-value {
-  padding-right: 5px;
-  font-size: 11px;
-  word-wrap: break-word;
-  width: 90%;
-}
-
-div.group {
-  margin-top: 10px;
-}
-
-div.group,
-#header {
-  background: #FFF;
-  border-color: #E1E1E1;
-  border-style: solid;
-  border-width: 1px;
-  box-shadow: 0 1px 1.5px rgba(0, 0, 0, 0.2);
-  border-radius: 4px 4px 4px 4px;
-}
-
-img#responseImageNode {
-  box-shadow: rgba(0,0,0,0.2) 0px 3px 3.5px;
-  max-width: 100%;
-}
-
-#responseImageNodeDiv {
-  padding: 5px;
-}
-
-#responseBodyFetchLink, #requestBodyFetchLink {
-  padding: 5px;
-  margin: 0;
-  cursor: pointer;
-  font-weight: bold;
-  font-size: 1.1em;
-  text-decoration: underline;
-}
-
-.longStringEllipsis {
-  margin-left: 0.6em;
-}
deleted file mode 100644
--- a/browser/themes/linux/devtools/widgets.css
+++ /dev/null
@@ -1,11 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-%include ../../shared/devtools/widgets.inc.css
-
-.side-menu-widget-group-checkbox .checkbox-spacer-box,
-.side-menu-widget-item-checkbox .checkbox-spacer-box {
-  margin: 0;
-  border: none;
-}
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -296,17 +296,17 @@ browser.jar:
   skin/classic/browser/devtools/controls.png          (../shared/devtools/images/controls.png)
   skin/classic/browser/devtools/controls@2x.png       (../shared/devtools/images/controls@2x.png)
   skin/classic/browser/devtools/animation-fast-track.svg (../shared/devtools/images/animation-fast-track.svg)
   skin/classic/browser/devtools/performance-icons.svg  (../shared/devtools/images/performance-icons.svg)
   skin/classic/browser/devtools/newtab.png             (../shared/devtools/images/newtab.png)
   skin/classic/browser/devtools/newtab@2x.png          (../shared/devtools/images/newtab@2x.png)
   skin/classic/browser/devtools/newtab-inverted.png    (../shared/devtools/images/newtab-inverted.png)
   skin/classic/browser/devtools/newtab-inverted@2x.png (../shared/devtools/images/newtab-inverted@2x.png)
-* skin/classic/browser/devtools/widgets.css           (devtools/widgets.css)
+* skin/classic/browser/devtools/widgets.css           (../shared/devtools/widgets.css)
   skin/classic/browser/devtools/power.svg                     (../shared/devtools/images/power.svg)
   skin/classic/browser/devtools/filetype-dir-close.svg        (../shared/devtools/images/filetypes/dir-close.svg)
   skin/classic/browser/devtools/filetype-dir-open.svg         (../shared/devtools/images/filetypes/dir-open.svg)
   skin/classic/browser/devtools/filetype-globe.svg            (../shared/devtools/images/filetypes/globe.svg)
   skin/classic/browser/devtools/filetype-store.svg            (../shared/devtools/images/filetypes/store.svg)
   skin/classic/browser/devtools/commandline-icon.png          (../shared/devtools/images/commandline-icon.png)
   skin/classic/browser/devtools/commandline-icon@2x.png       (../shared/devtools/images/commandline-icon@2x.png)
   skin/classic/browser/devtools/command-paintflashing.png     (../shared/devtools/images/command-paintflashing.png)
@@ -327,33 +327,33 @@ browser.jar:
   skin/classic/browser/devtools/command-console@2x.png        (../shared/devtools/images/command-console@2x.png)
   skin/classic/browser/devtools/command-eyedropper.png        (../shared/devtools/images/command-eyedropper.png)
   skin/classic/browser/devtools/command-eyedropper@2x.png     (../shared/devtools/images/command-eyedropper@2x.png)
   skin/classic/browser/devtools/command-rulers.png            (../shared/devtools/images/command-rulers.png)
   skin/classic/browser/devtools/command-rulers@2x.png         (../shared/devtools/images/command-rulers@2x.png)
   skin/classic/browser/devtools/alerticon-warning.png (../shared/devtools/images/alerticon-warning.png)
   skin/classic/browser/devtools/alerticon-warning@2x.png      (../shared/devtools/images/alerticon-warning@2x.png)
 * skin/classic/browser/devtools/ruleview.css          (../shared/devtools/ruleview.css)
-* skin/classic/browser/devtools/webconsole.css                  (devtools/webconsole.css)
-  skin/classic/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
+* skin/classic/browser/devtools/webconsole.css                  (../shared/devtools/webconsole.css)
+  skin/classic/browser/devtools/webconsole_networkpanel.css     (../shared/devtools/webconsole_networkpanel.css)
   skin/classic/browser/devtools/webconsole.svg                  (../shared/devtools/images/webconsole.svg)
   skin/classic/browser/devtools/commandline.css              (../shared/devtools/commandline.css)
   skin/classic/browser/devtools/markup-view.css       (../shared/devtools/markup-view.css)
   skin/classic/browser/devtools/editor-error.png       (../shared/devtools/images/editor-error.png)
   skin/classic/browser/devtools/editor-breakpoint.png  (../shared/devtools/images/editor-breakpoint.png)
   skin/classic/browser/devtools/editor-debug-location.png (../shared/devtools/images/editor-debug-location.png)
   skin/classic/browser/devtools/editor-debug-location@2x.png (../shared/devtools/images/editor-debug-location@2x.png)
   skin/classic/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
   skin/classic/browser/devtools/animationinspector.css          (../shared/devtools/animationinspector.css)
 * skin/classic/browser/devtools/canvasdebugger.css    (../shared/devtools/canvasdebugger.css)
   skin/classic/browser/devtools/debugger.css          (../shared/devtools/debugger.css)
   skin/classic/browser/devtools/eyedropper.css        (../shared/devtools/eyedropper.css)
-* skin/classic/browser/devtools/netmonitor.css        (devtools/netmonitor.css)
+* skin/classic/browser/devtools/netmonitor.css        (../shared/devtools/netmonitor.css)
   skin/classic/browser/devtools/performance.css       (../shared/devtools/performance.css)
   skin/classic/browser/devtools/promisedebugger.css   (../shared/devtools/promisedebugger.css)
   skin/classic/browser/devtools/timeline-filter.svg   (../shared/devtools/images/timeline-filter.svg)
 * skin/classic/browser/devtools/scratchpad.css        (../shared/devtools/scratchpad.css)
   skin/classic/browser/devtools/shadereditor.css      (../shared/devtools/shadereditor.css)
 * skin/classic/browser/devtools/splitview.css         (../shared/devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css       (../shared/devtools/styleeditor.css)
   skin/classic/browser/devtools/storage.css           (../shared/devtools/storage.css)
@@ -410,18 +410,18 @@ browser.jar:
   skin/classic/browser/devtools/responsiveui-screenshot.png      (../shared/devtools/images/responsivemode/responsiveui-screenshot.png)
   skin/classic/browser/devtools/responsiveui-screenshot@2x.png   (../shared/devtools/images/responsivemode/responsiveui-screenshot@2x.png)
   skin/classic/browser/devtools/toggle-tools.png          (../shared/devtools/images/toggle-tools.png)
   skin/classic/browser/devtools/toggle-tools@2x.png       (../shared/devtools/images/toggle-tools@2x.png)
   skin/classic/browser/devtools/dock-bottom@2x.png        (../shared/devtools/images/dock-bottom@2x.png)
   skin/classic/browser/devtools/dock-bottom-minimize@2x.png (../shared/devtools/images/dock-bottom-minimize@2x.png)
   skin/classic/browser/devtools/dock-bottom-maximize@2x.png (../shared/devtools/images/dock-bottom-maximize@2x.png)
   skin/classic/browser/devtools/dock-side@2x.png          (../shared/devtools/images/dock-side@2x.png)
-  skin/classic/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
-  skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
+* skin/classic/browser/devtools/floating-scrollbars.css   (../shared/devtools/floating-scrollbars.css)
+  skin/classic/browser/devtools/floating-scrollbars-light.css (../shared/devtools/floating-scrollbars-light.css)
 * skin/classic/browser/devtools/inspector.css               (../shared/devtools/inspector.css)
   skin/classic/browser/devtools/profiler-stopwatch.svg      (../shared/devtools/images/profiler-stopwatch.svg)
   skin/classic/browser/devtools/profiler-stopwatch-checked.svg      (../shared/devtools/images/profiler-stopwatch-checked.svg)
   skin/classic/browser/devtools/tool-options.svg            (../shared/devtools/images/tool-options.svg)
   skin/classic/browser/devtools/tool-webconsole.svg         (../shared/devtools/images/tool-webconsole.svg)
   skin/classic/browser/devtools/tool-debugger.svg           (../shared/devtools/images/tool-debugger.svg)
   skin/classic/browser/devtools/tool-debugger-paused.svg    (../shared/devtools/images/tool-debugger-paused.svg)
   skin/classic/browser/devtools/tool-inspector.svg          (../shared/devtools/images/tool-inspector.svg)
deleted file mode 100644
--- a/browser/themes/osx/devtools/floating-scrollbars.css
+++ /dev/null
@@ -1,35 +0,0 @@
-@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
-
-scrollbar {
-  -moz-appearance: none;
-  position: relative;
-  background-color: transparent;
-  background-image: none;
-  border: 0px solid transparent;
-  z-index: 2147483647;
-  padding: 2px;
-}
-
-/* Scrollbar code will reset the margin to the correct side depending on
-   where layout actually puts the scrollbar */
-scrollbar[orient="vertical"] {
-  margin-left: -8px;
-  min-width: 8px;
-  max-width: 8px;
-}
-
-scrollbar[orient="horizontal"] {
-  margin-top: -8px;
-  min-height: 8px;
-  max-height: 8px;
-}
-
-slider {
-  -moz-appearance: none !important;
-}
-
-thumb {
-  -moz-appearance: none !important;
-  background-color: rgba(0,0,0,0.2);
-  border-radius: 3px;
-}
deleted file mode 100644
--- a/browser/themes/osx/devtools/netmonitor.css
+++ /dev/null
@@ -1,6 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-%include ../shared.inc
-%include ../../shared/devtools/netmonitor.inc.css
deleted file mode 100644
--- a/browser/themes/osx/devtools/webconsole.css
+++ /dev/null
@@ -1,6 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-%include ../shared.inc
-%include ../../shared/devtools/webconsole.inc.css
\ No newline at end of file
deleted file mode 100644
--- a/browser/themes/osx/devtools/widgets.css
+++ /dev/null
@@ -1,6 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-%include ../shared.inc
-%include ../../shared/devtools/widgets.inc.css
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -396,17 +396,17 @@ browser.jar:
   skin/classic/browser/devtools/controls.png                (../shared/devtools/images/controls.png)
   skin/classic/browser/devtools/controls@2x.png             (../shared/devtools/images/controls@2x.png)
   skin/classic/browser/devtools/animation-fast-track.svg    (../shared/devtools/images/animation-fast-track.svg)
   skin/classic/browser/devtools/performance-icons.svg       (../shared/devtools/images/performance-icons.svg)
   skin/classic/browser/devtools/newtab.png                  (../shared/devtools/images/newtab.png)
   skin/classic/browser/devtools/newtab@2x.png               (../shared/devtools/images/newtab@2x.png)
   skin/classic/browser/devtools/newtab-inverted.png         (../shared/devtools/images/newtab-inverted.png)
   skin/classic/browser/devtools/newtab-inverted@2x.png      (../shared/devtools/images/newtab-inverted@2x.png)
-* skin/classic/browser/devtools/widgets.css                 (devtools/widgets.css)
+* skin/classic/browser/devtools/widgets.css                 (../shared/devtools/widgets.css)
   skin/classic/browser/devtools/power.svg                   (../shared/devtools/images/power.svg)
   skin/classic/browser/devtools/filetype-dir-close.svg      (../shared/devtools/images/filetypes/dir-close.svg)
   skin/classic/browser/devtools/filetype-dir-open.svg       (../shared/devtools/images/filetypes/dir-open.svg)
   skin/classic/browser/devtools/filetype-globe.svg          (../shared/devtools/images/filetypes/globe.svg)
   skin/classic/browser/devtools/filetype-store.svg          (../shared/devtools/images/filetypes/store.svg)
   skin/classic/browser/devtools/commandline-icon.png        (../shared/devtools/images/commandline-icon.png)
   skin/classic/browser/devtools/commandline-icon@2x.png     (../shared/devtools/images/commandline-icon@2x.png)
   skin/classic/browser/devtools/command-paintflashing.png     (../shared/devtools/images/command-paintflashing.png)
@@ -434,27 +434,27 @@ browser.jar:
 * skin/classic/browser/devtools/ruleview.css                (../shared/devtools/ruleview.css)
   skin/classic/browser/devtools/commandline.css             (../shared/devtools/commandline.css)
   skin/classic/browser/devtools/markup-view.css             (../shared/devtools/markup-view.css)
   skin/classic/browser/devtools/editor-error.png             (../shared/devtools/images/editor-error.png)
   skin/classic/browser/devtools/editor-breakpoint.png        (../shared/devtools/images/editor-breakpoint.png)
   skin/classic/browser/devtools/editor-breakpoint@2x.png        (../shared/devtools/images/editor-breakpoint@2x.png)
   skin/classic/browser/devtools/editor-debug-location.png    (../shared/devtools/images/editor-debug-location.png)
   skin/classic/browser/devtools/editor-debug-location@2x.png    (../shared/devtools/images/editor-debug-location@2x.png)
-* skin/classic/browser/devtools/webconsole.css                  (devtools/webconsole.css)
-  skin/classic/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
+* skin/classic/browser/devtools/webconsole.css                  (../shared/devtools/webconsole.css)
+  skin/classic/browser/devtools/webconsole_networkpanel.css     (../shared/devtools/webconsole_networkpanel.css)
   skin/classic/browser/devtools/webconsole.svg                  (../shared/devtools/images/webconsole.svg)
   skin/classic/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
   skin/classic/browser/devtools/animationinspector.css          (../shared/devtools/animationinspector.css)
 * skin/classic/browser/devtools/canvasdebugger.css          (../shared/devtools/canvasdebugger.css)
   skin/classic/browser/devtools/debugger.css                (../shared/devtools/debugger.css)
   skin/classic/browser/devtools/eyedropper.css              (../shared/devtools/eyedropper.css)
-* skin/classic/browser/devtools/netmonitor.css              (devtools/netmonitor.css)
+* skin/classic/browser/devtools/netmonitor.css              (../shared/devtools/netmonitor.css)
   skin/classic/browser/devtools/performance.css             (../shared/devtools/performance.css)
   skin/classic/browser/devtools/promisedebugger.css         (../shared/devtools/promisedebugger.css)
   skin/classic/browser/devtools/timeline-filter.svg         (../shared/devtools/images/timeline-filter.svg)
 * skin/classic/browser/devtools/scratchpad.css              (../shared/devtools/scratchpad.css)
   skin/classic/browser/devtools/shadereditor.css            (../shared/devtools/shadereditor.css)
 * skin/classic/browser/devtools/splitview.css               (../shared/devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css             (../shared/devtools/styleeditor.css)
   skin/classic/browser/devtools/storage.css                 (../shared/devtools/storage.css)
@@ -493,18 +493,18 @@ browser.jar:
   skin/classic/browser/devtools/debugger-blackbox.png       (../shared/devtools/images/debugger-blackbox.png)
   skin/classic/browser/devtools/debugger-blackbox@2x.png    (../shared/devtools/images/debugger-blackbox@2x.png)
   skin/classic/browser/devtools/debugger-prettyprint.png    (../shared/devtools/images/debugger-prettyprint.png)
   skin/classic/browser/devtools/debugger-prettyprint@2x.png (../shared/devtools/images/debugger-prettyprint@2x.png)
   skin/classic/browser/devtools/debugger-toggleBreakpoints.png (../shared/devtools/images/debugger-toggleBreakpoints.png)
   skin/classic/browser/devtools/debugger-toggleBreakpoints@2x.png (../shared/devtools/images/debugger-toggleBreakpoints@2x.png)
   skin/classic/browser/devtools/tracer-icon.png             (../shared/devtools/images/tracer-icon.png)
   skin/classic/browser/devtools/tracer-icon@2x.png          (../shared/devtools/images/tracer-icon@2x.png)
-  skin/classic/browser/devtools/floating-scrollbars.css     (devtools/floating-scrollbars.css)
-  skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
+* skin/classic/browser/devtools/floating-scrollbars.css     (../shared/devtools/floating-scrollbars.css)
+  skin/classic/browser/devtools/floating-scrollbars-light.css (../shared/devtools/floating-scrollbars-light.css)
   skin/classic/browser/devtools/responsive-se-resizer.png          (../shared/devtools/images/responsivemode/responsive-se-resizer.png)
   skin/classic/browser/devtools/responsive-se-resizer@2x.png       (../shared/devtools/images/responsivemode/responsive-se-resizer@2x.png)
   skin/classic/browser/devtools/responsive-vertical-resizer.png    (../shared/devtools/images/responsivemode/responsive-vertical-resizer.png)
   skin/classic/browser/devtools/responsive-vertical-resizer@2x.png (../shared/devtools/images/responsivemode/responsive-vertical-resizer@2x.png)
   skin/classic/browser/devtools/responsive-horizontal-resizer.png  (../shared/devtools/images/responsivemode/responsive-horizontal-resizer.png)
   skin/classic/browser/devtools/responsive-horizontal-resizer@2x.png  (../shared/devtools/images/responsivemode/responsive-horizontal-resizer@2x.png)
   skin/classic/browser/devtools/responsiveui-rotate.png          (../shared/devtools/images/responsivemode/responsiveui-rotate.png)
   skin/classic/browser/devtools/responsiveui-rotate@2x.png       (../shared/devtools/images/responsivemode/responsiveui-rotate@2x.png)
rename from browser/themes/osx/devtools/floating-scrollbars-light.css
rename to browser/themes/shared/devtools/floating-scrollbars-light.css
rename from browser/themes/windows/devtools/floating-scrollbars.css
rename to browser/themes/shared/devtools/floating-scrollbars.css
--- a/browser/themes/windows/devtools/floating-scrollbars.css
+++ b/browser/themes/shared/devtools/floating-scrollbars.css
@@ -2,34 +2,48 @@
 
 scrollbar {
   -moz-appearance: none !important;
   position: relative;
   background-color: transparent;
   background-image: none;
   z-index: 2147483647;
   padding: 2px;
+%ifdef XP_MACOSX
+  border: 0px solid transparent;
+%endif
 }
 
 /* Scrollbar code will reset the margin to the correct side depending on
    where layout actually puts the scrollbar */
 scrollbar[orient="vertical"] {
   margin-left: -10px;
   min-width: 10px;
   max-width: 10px;
 }
 
 scrollbar[orient="horizontal"] {
   margin-top: -10px;
   min-height: 10px;
   max-height: 10px;
 }
 
+%ifdef XP_MACOSX
+slider {
+  -moz-appearance: none !important;
+}
+
+thumb {
+  -moz-appearance: none !important;
+  background-color: rgba(0,0,0,0.2);
+  border-radius: 3px;
+}
+%else
 scrollbar thumb {
   -moz-appearance: none !important;
   border-width: 0px !important;
   background-color: rgba(170,170,170,0.2) !important;
   border-radius: 3px !important;
 }
-
 scrollbar scrollbarbutton, scrollbar gripper {
   display: none;
 }
+%endif
rename from browser/themes/shared/devtools/netmonitor.inc.css
rename to browser/themes/shared/devtools/netmonitor.css
--- a/browser/themes/shared/devtools/netmonitor.inc.css
+++ b/browser/themes/shared/devtools/netmonitor.css
@@ -822,8 +822,62 @@ box.requests-menu-status[code^="5"] {
   .requests-menu-size {
     width: 16vw;
     border-width: 0 !important;
     box-shadow: none !important;
     /* The "Timeline" header is not visible anymore, and thus the
        right border and box-shadow of "Size" column should be hidden. */
   }
 }
+
+/* Platform overrides (copied in from the old platform specific files) */
+%ifdef XP_WIN
+.requests-menu-header-button > .button-box {
+  padding: 0;
+}
+
+.requests-menu-timings-division {
+  padding-top: 1px;
+  font-size: 90%;
+}
+
+.requests-menu-footer-button,
+.requests-menu-footer-label {
+  padding-top: 0px;
+  padding-bottom: 0px;
+}
+
+/* Responsive sidebar */
+@media (max-width: 700px) {
+  .requests-menu-footer-button,
+  .requests-menu-footer-label {
+    padding-top: 0px;
+    padding-bottom: 0px;
+  }
+}
+%elifdef XP_LINUX
+#headers-summary-resend {
+  padding: 4px;
+}
+
+#toggle-raw-headers {
+  padding: 4px;
+}
+
+.requests-menu-status-and-method {
+  width: 9em;
+}
+
+.requests-menu-security-and-domain {
+  width: 16vw;
+}
+
+.requests-menu-size {
+  width: 6em;
+}
+
+/* Responsive sidebar */
+@media (max-width: 700px) {
+  .requests-menu-header-button {
+    font-size: 85%;
+  }
+}
+%endif
rename from browser/themes/shared/devtools/webconsole.inc.css
rename to browser/themes/shared/devtools/webconsole.css
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.css
@@ -590,8 +590,19 @@ a {
   .devtools-toolbarbutton {
     margin-top: 3px;
   }
   .hud-console-filter-toolbar .hud-filter-box,
   .hud-console-filter-toolbar .devtools-toolbarbutton {
     margin-top: 5px;
   }
 }
+
+%ifdef XP_WIN
+/*
+ * This hardcoded width likely due to a toolkit Windows specific bug.
+ * See http://hg.mozilla.org/mozilla-central/annotate/f38d6df93cad/toolkit/themes/winstripe/global/textbox-aero.css#l7
+ */
+
+.hud-filter-box {
+  width: 200px;
+}
+%endif
rename from browser/themes/osx/devtools/webconsole_networkpanel.css
rename to browser/themes/shared/devtools/webconsole_networkpanel.css
rename from browser/themes/shared/devtools/widgets.inc.css
rename to browser/themes/shared/devtools/widgets.css
--- a/browser/themes/shared/devtools/widgets.inc.css
+++ b/browser/themes/shared/devtools/widgets.css
@@ -215,16 +215,22 @@
 }
 
 .breadcrumbs-widget-item > .button-box {
   border: none;
   padding-top: 0;
   padding-bottom: 0;
 }
 
+%ifdef XP_WIN
+.breadcrumbs-widget-item:-moz-focusring > .button-box {
+  border-width: 0;
+}
+%endif
+
 .breadcrumbs-widget-item:not([checked]) {
   background: -moz-element(#breadcrumb-separator-normal) no-repeat center left;
 }
 
 .breadcrumbs-widget-item[checked] + .breadcrumbs-widget-item {
   background: -moz-element(#breadcrumb-separator-after) no-repeat 0 0;
 }
 
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1230,25 +1230,16 @@ toolbarbutton[constrain-size="true"][cui
   -moz-padding-end: 2px;
 }
 
 /* overlap the urlbar's border */
 #PopupAutoCompleteRichResult {
   margin-top: -1px;
 }
 
-@media (-moz-os-version: windows-xp),
-       (-moz-os-version: windows-vista),
-       (-moz-os-version: windows-win7) {
-  #urlbar,
-  .searchbar-textbox {
-    border-radius: 2px;
-  }
-}
-
 @media (-moz-windows-default-theme) {
   #urlbar,
   .searchbar-textbox {
     @navbarTextboxCustomBorder@
     border-radius: 1px;
   }
 
   @media (-moz-os-version: windows-vista),
deleted file mode 100644
--- a/browser/themes/windows/devtools/floating-scrollbars-light.css
+++ /dev/null
@@ -1,10 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-@import url("floating-scrollbars.css");
-
-scrollbar thumb {
-  background-color: rgba(170,170,170,0.2) !important;
-}
deleted file mode 100644
--- a/browser/themes/windows/devtools/netmonitor.css
+++ /dev/null
@@ -1,29 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-%include ../../shared/devtools/netmonitor.inc.css
-
-.requests-menu-header-button > .button-box {
-  padding: 0;
-}
-
-.requests-menu-timings-division {
-  padding-top: 1px;
-  font-size: 90%;
-}
-
-.requests-menu-footer-button,
-.requests-menu-footer-label {
-  padding-top: 0px;
-  padding-bottom: 0px;
-}
-
-/* Responsive sidebar */
-@media (max-width: 700px) {
-  .requests-menu-footer-button,
-  .requests-menu-footer-label {
-    padding-top: 0px;
-    padding-bottom: 0px;
-  }
-}
deleted file mode 100644
--- a/browser/themes/windows/devtools/webconsole.css
+++ /dev/null
@@ -1,14 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-%include ../../shared/devtools/webconsole.inc.css
-
-/*
- * This hardcoded width likely due to a toolkit Windows specific bug.
- * See http://hg.mozilla.org/mozilla-central/annotate/f38d6df93cad/toolkit/themes/winstripe/global/textbox-aero.css#l7
- */
-
-.hud-filter-box {
-  width: 200px;
-}
deleted file mode 100644
--- a/browser/themes/windows/devtools/webconsole_networkpanel.css
+++ /dev/null
@@ -1,100 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-body {
-  font-family: sans-serif;
-  font-size: 11px;
-  background: #EEE;
-  color: #000;
-}
-
-#header {
-  padding: 5px;
-  overflow-x: auto;
-  display: block;
-}
-
-h1 {
-  font-size: 13px;
-  line-height: 15px;
-  padding: 3px 10px;
-  vertical-align: bottom;
-  margin: 0px;
-  background: linear-gradient(#BBB, #999);
-  border-radius: 2px;
-  text-shadow: #FFF 0px 1px 0px;
-}
-
-h1 .info {
-  font-size: 11px;
-  line-height: 15px;
-  vertical-align: bottom;
-  float: right;
-  color: #333;
-  padding-right: 3px;
-}
-
-.property-table {
-  padding: 2px 5px;
-  background: linear-gradient(#FFF, #F8F8F8);
-  color: #333;
-  width: 100%;
-  max-height: 330px;
-  overflow: auto;
-  display: block;
-}
-
-.property-table-header {
-  font-size: 11px;
-  font-weight: bold;
-  padding-right: 4px;
-  color: #000;
-  white-space: nowrap;
-  text-align: end;
-  vertical-align: top;
-  width: 10%;
-}
-
-.property-table-value {
-  padding-right: 5px;
-  font-size: 11px;
-  word-wrap: break-word;
-  width: 90%;
-}
-
-div.group {
-  margin-top: 10px;
-}
-
-div.group,
-#header {
-  background: #FFF;
-  border-color: #E1E1E1;
-  border-style: solid;
-  border-width: 1px;
-  box-shadow: 0 1px 1.5px rgba(0, 0, 0, 0.2);
-  border-radius: 4px 4px 4px 4px;
-}
-
-img#responseImageNode {
-  box-shadow: rgba(0,0,0,0.2) 0px 3px 3.5px;
-  max-width: 100%;
-}
-
-#responseImageNodeDiv {
-  padding: 5px;
-}
-
-#responseBodyFetchLink, #requestBodyFetchLink {
-  padding: 5px;
-  margin: 0;
-  cursor: pointer;
-  font-weight: bold;
-  font-size: 1.1em;
-  text-decoration: underline;
-}
-
-.longStringEllipsis {
-  margin-left: 0.6em;
-}
deleted file mode 100644
--- a/browser/themes/windows/devtools/widgets.css
+++ /dev/null
@@ -1,9 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-%include ../../shared/devtools/widgets.inc.css
-
-.breadcrumbs-widget-item:-moz-focusring > .button-box {
-  border-width: 0;
-}
\ No newline at end of file
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -420,17 +420,17 @@ browser.jar:
         skin/classic/browser/devtools/controls.png                  (../shared/devtools/images/controls.png)
         skin/classic/browser/devtools/controls@2x.png               (../shared/devtools/images/controls@2x.png)
         skin/classic/browser/devtools/animation-fast-track.svg      (../shared/devtools/images/animation-fast-track.svg)
         skin/classic/browser/devtools/performance-icons.svg         (../shared/devtools/images/performance-icons.svg)
         skin/classic/browser/devtools/newtab.png                    (../shared/devtools/images/newtab.png)
         skin/classic/browser/devtools/newtab@2x.png                 (../shared/devtools/images/newtab@2x.png)
         skin/classic/browser/devtools/newtab-inverted.png           (../shared/devtools/images/newtab-inverted.png)
         skin/classic/browser/devtools/newtab-inverted@2x.png        (../shared/devtools/images/newtab-inverted@2x.png)
-*       skin/classic/browser/devtools/widgets.css                   (devtools/widgets.css)
+*       skin/classic/browser/devtools/widgets.css                   (../shared/devtools/widgets.css)
         skin/classic/browser/devtools/power.svg                     (../shared/devtools/images/power.svg)
         skin/classic/browser/devtools/filetype-dir-close.svg        (../shared/devtools/images/filetypes/dir-close.svg)
         skin/classic/browser/devtools/filetype-dir-open.svg         (../shared/devtools/images/filetypes/dir-open.svg)
         skin/classic/browser/devtools/filetype-globe.svg            (../shared/devtools/images/filetypes/globe.svg)
         skin/classic/browser/devtools/filetype-store.svg            (../shared/devtools/images/filetypes/store.svg)
         skin/classic/browser/devtools/commandline-icon.png          (../shared/devtools/images/commandline-icon.png)
         skin/classic/browser/devtools/commandline-icon@2x.png          (../shared/devtools/images/commandline-icon@2x.png)
         skin/classic/browser/devtools/alerticon-warning.png         (../shared/devtools/images/alerticon-warning.png)
@@ -458,27 +458,27 @@ browser.jar:
         skin/classic/browser/devtools/command-rulers.png            (../shared/devtools/images/command-rulers.png)
         skin/classic/browser/devtools/command-rulers@2x.png         (../shared/devtools/images/command-rulers@2x.png)
         skin/classic/browser/devtools/markup-view.css               (../shared/devtools/markup-view.css)
         skin/classic/browser/devtools/editor-error.png              (../shared/devtools/images/editor-error.png)
         skin/classic/browser/devtools/editor-breakpoint.png         (../shared/devtools/images/editor-breakpoint.png)
         skin/classic/browser/devtools/editor-breakpoint@2x.png         (../shared/devtools/images/editor-breakpoint@2x.png)
         skin/classic/browser/devtools/editor-debug-location.png     (../shared/devtools/images/editor-debug-location.png)
         skin/classic/browser/devtools/editor-debug-location@2x.png     (../shared/devtools/images/editor-debug-location@2x.png)
-*       skin/classic/browser/devtools/webconsole.css                (devtools/webconsole.css)
-        skin/classic/browser/devtools/webconsole_networkpanel.css   (devtools/webconsole_networkpanel.css)
+*       skin/classic/browser/devtools/webconsole.css                (../shared/devtools/webconsole.css)
+        skin/classic/browser/devtools/webconsole_networkpanel.css   (../shared/devtools/webconsole_networkpanel.css)
         skin/classic/browser/devtools/webconsole.svg                (../shared/devtools/images/webconsole.svg)
         skin/classic/browser/devtools/breadcrumbs-divider@2x.png    (../shared/devtools/images/breadcrumbs-divider@2x.png)
         skin/classic/browser/devtools/breadcrumbs-scrollbutton.png  (../shared/devtools/images/breadcrumbs-scrollbutton.png)
         skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
         skin/classic/browser/devtools/animationinspector.css        (../shared/devtools/animationinspector.css)
         skin/classic/browser/devtools/eyedropper.css                (../shared/devtools/eyedropper.css)
 *       skin/classic/browser/devtools/canvasdebugger.css            (../shared/devtools/canvasdebugger.css)
         skin/classic/browser/devtools/debugger.css                  (../shared/devtools/debugger.css)
-*       skin/classic/browser/devtools/netmonitor.css                (devtools/netmonitor.css)
+*       skin/classic/browser/devtools/netmonitor.css                (../shared/devtools/netmonitor.css)
         skin/classic/browser/devtools/performance.css               (../shared/devtools/performance.css)
         skin/classic/browser/devtools/promisedebugger.css           (../shared/devtools/promisedebugger.css)
         skin/classic/browser/devtools/timeline-filter.svg           (../shared/devtools/images/timeline-filter.svg)
 *       skin/classic/browser/devtools/scratchpad.css                (../shared/devtools/scratchpad.css)
         skin/classic/browser/devtools/shadereditor.css              (../shared/devtools/shadereditor.css)
         skin/classic/browser/devtools/storage.css                   (../shared/devtools/storage.css)
 *       skin/classic/browser/devtools/splitview.css                 (../shared/devtools/splitview.css)
         skin/classic/browser/devtools/styleeditor.css               (../shared/devtools/styleeditor.css)
@@ -535,18 +535,18 @@ browser.jar:
         skin/classic/browser/devtools/responsiveui-screenshot.png   (../shared/devtools/images/responsivemode/responsiveui-screenshot.png)
         skin/classic/browser/devtools/responsiveui-screenshot@2x.png   (../shared/devtools/images/responsivemode/responsiveui-screenshot@2x.png)
         skin/classic/browser/devtools/toggle-tools.png              (../shared/devtools/images/toggle-tools.png)
         skin/classic/browser/devtools/toggle-tools@2x.png              (../shared/devtools/images/toggle-tools@2x.png)
         skin/classic/browser/devtools/dock-bottom@2x.png            (../shared/devtools/images/dock-bottom@2x.png)
         skin/classic/browser/devtools/dock-bottom-minimize@2x.png   (../shared/devtools/images/dock-bottom-minimize@2x.png)
         skin/classic/browser/devtools/dock-bottom-maximize@2x.png   (../shared/devtools/images/dock-bottom-maximize@2x.png)
         skin/classic/browser/devtools/dock-side@2x.png              (../shared/devtools/images/dock-side@2x.png)
-        skin/classic/browser/devtools/floating-scrollbars.css       (devtools/floating-scrollbars.css)
-        skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
+*       skin/classic/browser/devtools/floating-scrollbars.css       (../shared/devtools/floating-scrollbars.css)
+        skin/classic/browser/devtools/floating-scrollbars-light.css (../shared/devtools/floating-scrollbars-light.css)
 *       skin/classic/browser/devtools/inspector.css                 (../shared/devtools/inspector.css)
         skin/classic/browser/devtools/profiler-stopwatch.svg        (../shared/devtools/images/profiler-stopwatch.svg)
         skin/classic/browser/devtools/profiler-stopwatch-checked.svg  (../shared/devtools/images/profiler-stopwatch-checked.svg)
         skin/classic/browser/devtools/tool-options.svg              (../shared/devtools/images/tool-options.svg)
         skin/classic/browser/devtools/tool-webconsole.svg           (../shared/devtools/images/tool-webconsole.svg)
         skin/classic/browser/devtools/tool-debugger.svg             (../shared/devtools/images/tool-debugger.svg)
         skin/classic/browser/devtools/tool-debugger-paused.svg      (../shared/devtools/images/tool-debugger-paused.svg)
         skin/classic/browser/devtools/tool-inspector.svg            (../shared/devtools/images/tool-inspector.svg)
--- a/mobile/android/base/widget/RoundedCornerLayout.java
+++ b/mobile/android/base/widget/RoundedCornerLayout.java
@@ -3,91 +3,63 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.widget;
 
 import org.mozilla.gecko.R;
 
 import android.content.Context;
-import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
+import android.graphics.Path;
 import android.graphics.RectF;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.TypedValue;
 import android.widget.LinearLayout;
 
 public class RoundedCornerLayout extends LinearLayout {
     private static final String LOGTAG = "Gecko" + RoundedCornerLayout.class.getSimpleName();
-    private Bitmap maskBitmap;
-    private Paint paint, maskPaint;
     private float cornerRadius;
 
+    private Path path;
+
     public RoundedCornerLayout(Context context) {
         super(context);
         init(context);
     }
 
     public RoundedCornerLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
         init(context);
     }
 
     public RoundedCornerLayout(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         init(context);
     }
 
     private void init(Context context) {
-        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+        final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
 
         cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
                 getResources().getDimensionPixelSize(R.dimen.doorhanger_rounded_corner_radius), metrics);
-        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
-        maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
-        maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
 
         setWillNotDraw(false);
     }
 
-
     @Override
-    protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
-        super.onLayout(changed, l, t, r, b);
-        if (changed) {
-            maskBitmap = createMask(r, b);
-        }
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        final RectF r = new RectF(0, 0, w, h);
+        path = new Path();
+        path.addRoundRect(r, cornerRadius, cornerRadius, Path.Direction.CW);
+        path.close();
     }
 
-
     @Override
     public void draw(Canvas canvas) {
-        Bitmap offscreenBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
-        Canvas offscreenCanvas = new Canvas(offscreenBitmap);
-
-        super.draw(offscreenCanvas);
-
-        offscreenCanvas.drawBitmap(maskBitmap, 0f, 0f, maskPaint);
-        canvas.drawBitmap(offscreenBitmap, 0f, 0f, paint);
+        canvas.save();
+        canvas.clipPath(path);
+        super.draw(canvas);
+        canvas.restore();
     }
-
-    private Bitmap createMask(int width, int height) {
-        Bitmap mask = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8);
-        Canvas canvas = new Canvas(mask);
-
-        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        paint.setColor(Color.WHITE);
-
-        canvas.drawRect(0, 0, width, height, paint);
-
-        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
-
-        canvas.drawRoundRect(new RectF(0, 0, width, height), cornerRadius, cornerRadius, paint);
-
-        return mask;
-    }
-}
\ No newline at end of file
+}
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6690,16 +6690,36 @@
     "kind": "boolean",
     "description": "How many times has the devtool's Developer Toolbar been opened via the toolbox button?"
   },
   "DEVTOOLS_WEBIDE_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "How many times has the DevTools WebIDE been opened?"
   },
+  "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_BOOLEAN": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "How many times has the DevTools WebIDE project editor been opened?"
+  },
+  "DEVTOOLS_WEBIDE_PROJECT_EDITOR_SAVE_BOOLEAN": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "How many times has a file been saved in the DevTools WebIDE project editor?"
+  },
+  "DEVTOOLS_WEBIDE_NEW_PROJECT_BOOLEAN": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "How many times has a new project been created in the DevTools WebIDE?"
+  },
+  "DEVTOOLS_WEBIDE_IMPORT_PROJECT_BOOLEAN": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "How many times has a project been imported into the DevTools WebIDE?"
+  },
   "DEVTOOLS_CUSTOM_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "How many times has a custom developer tool been opened via the toolbox button?"
   },
   "DEVTOOLS_TOOLBOX_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
@@ -6835,16 +6855,36 @@
     "kind": "flag",
     "description": "How many users have opened the devtool's Developer Toolbar been opened via the toolbox button?"
   },
   "DEVTOOLS_WEBIDE_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "How many users have opened the DevTools WebIDE?"
   },
+  "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_PER_USER_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "How many users have opened the DevTools WebIDE project editor?"
+  },
+  "DEVTOOLS_WEBIDE_PROJECT_EDITOR_SAVE_PER_USER_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "How many users have saved a file in the DevTools WebIDE project editor?"
+  },
+  "DEVTOOLS_WEBIDE_NEW_PROJECT_PER_USER_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "How many users have created a new project in the DevTools WebIDE?"
+  },
+  "DEVTOOLS_WEBIDE_IMPORT_PROJECT_PER_USER_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "How many users have imported a project into the DevTools WebIDE?"
+  },
   "DEVTOOLS_CUSTOM_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "How many users have opened a custom developer tool via the toolbox button?"
   },
   "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
@@ -7015,16 +7055,23 @@
   },
   "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has WebIDE been active (seconds)"
   },
+  "DEVTOOLS_WEBIDE_PROJECT_EDITOR_TIME_ACTIVE_SECONDS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000000",
+    "n_buckets": 100,
+    "description": "How long has WebIDE's project editor been active (seconds)"
+  },
   "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has a custom developer tool been active (seconds)"
   },
   "DEVTOOLS_WEBIDE_CONNECTION_RESULT": {
--- a/toolkit/components/telemetry/TelemetryReportingPolicy.jsm
+++ b/toolkit/components/telemetry/TelemetryReportingPolicy.jsm
@@ -179,16 +179,21 @@ let TelemetryReportingPolicyImpl = {
     return this._logger;
   },
 
   /**
    * Get the date the policy was notified.
    * @return {Object} A date object or null on errors.
    */
   get dataSubmissionPolicyNotifiedDate() {
+    if (!Preferences.has(PREF_ACCEPTED_POLICY_DATE)) {
+      this._log.info("get dataSubmissionPolicyNotifiedDate - No date stored yet.");
+      return null;
+    }
+
     let prefString = Preferences.get(PREF_ACCEPTED_POLICY_DATE, 0);
     let valueInteger = parseInt(prefString, 10);
 
     // If nothing or an invalid value is saved in the prefs, bail out.
     if (Number.isNaN(valueInteger) || valueInteger == 0) {
       this._log.error("get dataSubmissionPolicyNotifiedDate - Invalid date stored.");
       return null;
     }
--- a/toolkit/components/telemetry/TelemetryStorage.jsm
+++ b/toolkit/components/telemetry/TelemetryStorage.jsm
@@ -1252,22 +1252,24 @@ let TelemetryStorageImpl = {
     }
 
     // Try to load the ping file. Update the related histograms on failure.
     let ping;
     try {
       ping = yield this.loadPingFile(info.path, false);
     } catch(e) {
       // If we failed to load the ping, check what happened and update the histogram.
-      // Then propagate the rejection.
       if (e instanceof PingReadError) {
         Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").add();
       } else if (e instanceof PingParseError) {
         Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").add();
       }
+      // Remove the ping from the cache, so we don't try to load it again.
+      this._pendingPings.delete(id);
+      // Then propagate the rejection.
       throw e;
     };
 
     return ping;
   }),
 
   removePendingPing: function(id) {
     let info = this._pendingPings.get(id);
@@ -1437,29 +1439,34 @@ let TelemetryStorageImpl = {
     if (aCompressed) {
       options.compression = "lz4";
     }
 
     let array;
     try {
       array = yield OS.File.read(aFilePath, options);
     } catch(e) {
+      this._log.trace("loadPingfile - unreadable ping " + aFilePath, e);
       throw new PingReadError(e.message);
-    };
+    }
 
     let decoder = new TextDecoder();
     let string = decoder.decode(array);
     let ping;
     try {
       ping = JSON.parse(string);
       // The ping's payload used to be stringified JSON.  Deal with that.
       if (typeof(ping.payload) == "string") {
         ping.payload = JSON.parse(ping.payload);
       }
     } catch (e) {
+      this._log.trace("loadPingfile - unparseable ping " + aFilePath, e);
+      yield OS.File.remove(aFilePath).catch((ex) => {
+        this._log.error("loadPingFile - failed removing unparseable ping file", ex);
+      });
       throw new PingParseError(e.message);
     }
 
     return ping;
   }),
 
   /**
    * Archived pings are saved with file names of the form:
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -311,16 +311,19 @@ add_task(function* test_corrupted_pendin
   yield Assert.rejects(TelemetryStorage.loadPendingPing(pendingPingId),
                        "Telemetry must fail loading a corrupted ping");
 
   h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot();
   Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");
   h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot();
   Assert.equal(h.sum, 1, "Telemetry must report a pending ping parse failure");
 
+  let exists = yield OS.File.exists(getSavePathForPingId(pendingPingId));
+  Assert.ok(!exists, "The unparseable ping should have been removed");
+
   yield clearPendingPings();
 });
 
 /**
  * Create some recent and overdue pings and verify that they get sent.
  */
 add_task(function* test_overdue_pings_trigger_send() {
   let pingTypes = [
--- a/toolkit/devtools/webconsole/test/chrome.ini
+++ b/toolkit/devtools/webconsole/test/chrome.ini
@@ -6,16 +6,17 @@ support-files =
   data.json
   data.json^headers^
   network_requests_iframe.html
   sandboxed_iframe.html
 
 [test_basics.html]
 [test_bug819670_getter_throws.html]
 [test_cached_messages.html]
+[test_commands_other.html]
 [test_commands_registration.html]
 [test_consoleapi.html]
 [test_consoleapi_innerID.html]
 [test_file_uri.html]
 [test_reflow.html]
 [test_jsterm.html]
 [test_jsterm_cd_iframe.html]
 [test_jsterm_last_result.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/test_commands_other.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for the other command helpers</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript;version=1.8" src="common.js"></script>
+  <!-- Any copyright is dedicated to the Public Domain.
+     - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the querySelector / querySelectorAll helpers</p>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+let gState;
+let gWin;
+let tests;
+
+function evaluateJS(input) {
+  return new Promise((resolve) => gState.client.evaluateJS(input, resolve));
+}
+
+function startTest() {
+  info ("Content window opened, attaching console to it");
+
+  let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
+  ok (!gWin.document.nodePrincipal.equals(systemPrincipal),
+      "The test document is not using the system principal");
+
+  attachConsole([], state => {
+    gState = state;
+    runTests(tests, testEnd);
+  }, true);
+}
+
+tests = [
+  Task.async(function* keys() {
+    let response = yield evaluateJS("keys({foo: 'bar'})");
+    checkObject(response, {
+      from: gState.actor,
+      result: {
+        class: "Array",
+        preview: {
+          items: ["foo"]
+        }
+      }
+    });
+    nextTest();
+  }),
+  Task.async(function* values() {
+    let response = yield evaluateJS("values({foo: 'bar'})");
+    checkObject(response, {
+      from: gState.actor,
+      result: {
+        class: "Array",
+        preview: {
+          items: ["bar"]
+        }
+      }
+    });
+    nextTest();
+  }),
+];
+
+function testEnd() {
+  gWin.close();
+  gWin = null;
+  closeDebugger(gState, function() {
+    gState = null;
+    SimpleTest.finish();
+  });
+}
+
+window.onload = function() {
+  // Open a content window to test XRay functionality on built in functions.
+  gWin = window.open("data:text/html,");
+  info ("Waiting for content window to load");
+  gWin.onload = startTest;
+}
+</script>
+</body>
+</html>
--- a/toolkit/devtools/webconsole/test/test_jsterm_queryselector.html
+++ b/toolkit/devtools/webconsole/test/test_jsterm_queryselector.html
@@ -9,34 +9,63 @@
      - http://creativecommons.org/publicdomain/zero/1.0/ -->
 </head>
 <body>
 <p>Test for the querySelector / querySelectorAll helpers</p>
 
 <script class="testbody" type="text/javascript;version=1.8">
 SimpleTest.waitForExplicitFinish();
 let gState;
+let gWin;
 
 function evaluateJS(input) {
   return new Promise((resolve) => gState.client.evaluateJS(input, resolve));
 }
 
 function startTest() {
-  removeEventListener("load", startTest);
+  info ("Content window opened, attaching console to it");
+
+  let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
+  ok (!gWin.document.nodePrincipal.equals(systemPrincipal),
+      "The test document is not using the system principal");
+
   attachConsole([], state => {
     gState = state;
     let tests = [
+      setupWindow,
+      checkQuerySelector,
       checkQuerySelectorAll,
       checkQuerySelectorAllNotExist,
       checkQuerySelectorAllException
     ];
     runTests(tests, testEnd);
   }, true);
 }
 
+let setupWindow = Task.async(function*() {
+  info ("Shimming window functions for the content privileged tab");
+  yield evaluateJS("document.querySelector = function() { throw 'should not call qS'; }");
+  yield evaluateJS("document.querySelectorAll = function() { throw 'should not call qSA'; }");
+  nextTest();
+});
+
+let checkQuerySelector = Task.async(function*() {
+  info ("$ returns an DOMNode");
+  let response = yield evaluateJS("$('body')");
+  basicResultCheck(response, "$('body')", {
+    type: "object",
+    class: "HTMLBodyElement",
+    preview: {
+      kind: "DOMNode",
+      nodeName: "body"
+    }
+  });
+  nextTest();
+});
+
 let checkQuerySelectorAll = Task.async(function*() {
   info ("$$ returns an array");
   let response = yield evaluateJS("$$('body')");
   basicResultCheck(response, "$$('body')", {
     type: "object",
     class: "Array",
     preview: {
       length: 1
@@ -81,18 +110,25 @@ function basicResultCheck(response, inpu
     input: input,
     result: output,
   });
   ok(!response.exception, "no eval exception");
   ok(!response.helperResult, "no helper result");
 }
 
 function testEnd() {
+  gWin.close();
+  gWin = null;
   closeDebugger(gState, function() {
     gState = null;
     SimpleTest.finish();
   });
 }
 
-addEventListener("load", startTest);
+window.onload = function() {
+  // Open a content window to test XRay functionality on built in functions.
+  gWin = window.open("data:text/html,");
+  info ("Waiting for content window to load");
+  gWin.onload = startTest;
+}
 </script>
 </body>
 </html>
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -38,31 +38,16 @@ const CONSOLE_ENTRY_THRESHOLD = 5;
 const MAX_AUTOCOMPLETE_ATTEMPTS = exports.MAX_AUTOCOMPLETE_ATTEMPTS = 100000;
 
 const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [ 'SharedWorker', 'ServiceWorker', 'Worker' ];
 
 // Prevent iterating over too many properties during autocomplete suggestions.
 const MAX_AUTOCOMPLETIONS = exports.MAX_AUTOCOMPLETIONS = 1500;
 
 let WebConsoleUtils = {
-  /**
-   * Convenience function to unwrap a wrapped object.
-   *
-   * @param aObject the object to unwrap.
-   * @return aObject unwrapped.
-   */
-  unwrap: function WCU_unwrap(aObject)
-  {
-    try {
-      return XPCNativeWrapper.unwrap(aObject);
-    }
-    catch (ex) {
-      return aObject;
-    }
-  },
 
   /**
    * Wrap a string in an nsISupportsString object.
    *
    * @param string aString
    * @return nsISupportsString
    */
   supportsString: function WCU_supportsString(aString)
@@ -1445,16 +1430,19 @@ ConsoleAPIListener.prototype =
    *        The message topic received from the observer service.
    */
   observe: function CAL_observe(aMessage, aTopic)
   {
     if (!this.owner) {
       return;
     }
 
+    // Here, wrappedJSObject is not a security wrapper but a property defined
+    // by the XPCOM component which allows us to unwrap the XPCOM interface and
+    // access the underlying JSObject.
     let apiMessage = aMessage.wrappedJSObject;
     if (this.window && CONSOLE_WORKER_IDS.indexOf(apiMessage.innerID) == -1) {
       let msgWindow = Services.wm.getCurrentInnerWindowWithId(apiMessage.innerID);
       if (!msgWindow || !this.layoutHelpers.isIncludedInTopLevelWindow(msgWindow)) {
         // Not the same window!
         return;
       }
     }
@@ -1636,20 +1624,25 @@ WebConsoleCommands._registerOriginal("$"
  *
  * @param string aSelector
  *        A string that is passed to window.document.querySelectorAll.
  * @return nsIDOMNodeList
  *         Returns the result of document.querySelectorAll(aSelector).
  */
 WebConsoleCommands._registerOriginal("$$", function JSTH_$$(aOwner, aSelector)
 {
-  let results = aOwner.window.document.querySelectorAll(aSelector);
-  let nodes = aOwner.window.wrappedJSObject.Array.from(results);
+  let nodes = aOwner.window.document.querySelectorAll(aSelector);
 
-  return nodes;
+  // Calling aOwner.window.Array.from() doesn't work without accessing the
+  // wrappedJSObject, so just loop through the results instead.
+  let result = new aOwner.window.Array();
+  for (let i = 0; i < nodes.length; i++) {
+    result.push(nodes[i]);
+  }
+  return result;
 });
 
 /**
  * Returns the result of the last console input evaluation
  *
  * @return object|undefined
  * Returns last console evaluation or undefined
  */
@@ -1666,18 +1659,21 @@ WebConsoleCommands._registerOriginal("$_
  * @param string aXPath
  *        xPath search query to execute.
  * @param [optional] nsIDOMNode aContext
  *        Context to run the xPath query on. Uses window.document if not set.
  * @return array of nsIDOMNode
  */
 WebConsoleCommands._registerOriginal("$x", function JSTH_$x(aOwner, aXPath, aContext)
 {
-  let nodes = new aOwner.window.wrappedJSObject.Array();
-  let doc = aOwner.window.document;
+  let nodes = new aOwner.window.Array();
+
+  // Not waiving Xrays, since we want the original Document.evaluate function,
+  // instead of anything that's been redefined.
+  let doc =  aOwner.window.document;
   aContext = aContext || doc;
 
   let results = doc.evaluate(aXPath, aContext, null,
                              Ci.nsIDOMXPathResult.ANY_TYPE, null);
   let node;
   while ((node = results.iterateNext())) {
     nodes.push(node);
   }
@@ -1721,36 +1717,39 @@ WebConsoleCommands._registerOriginal("cl
  * Returns the result of Object.keys(aObject).
  *
  * @param object aObject
  *        Object to return the property names from.
  * @return array of strings
  */
 WebConsoleCommands._registerOriginal("keys", function JSTH_keys(aOwner, aObject)
 {
-  return aOwner.window.wrappedJSObject.Object.keys(WebConsoleUtils.unwrap(aObject));
+  // Need to waive Xrays so we can iterate functions and accessor properties
+  return Cu.cloneInto(Object.keys(Cu.waiveXrays(aObject)), aOwner.window);
 });
 
 /**
  * Returns the values of all properties on aObject.
  *
  * @param object aObject
  *        Object to display the values from.
  * @return array of string
  */
 WebConsoleCommands._registerOriginal("values", function JSTH_values(aOwner, aObject)
 {
-  let arrValues = new aOwner.window.wrappedJSObject.Array();
-  let obj = WebConsoleUtils.unwrap(aObject);
+  let values = [];
+  // Need to waive Xrays so we can iterate functions and accessor properties
+  let waived = Cu.waiveXrays(aObject);
+  let names = Object.getOwnPropertyNames(waived);
 
-  for (let prop in obj) {
-    arrValues.push(obj[prop]);
+  for (let name of names) {
+    values.push(waived[name]);
   }
 
-  return arrValues;
+  return Cu.cloneInto(values, aOwner.window);
 });
 
 /**
  * Opens a help window in MDN.
  */
 WebConsoleCommands._registerOriginal("help", function JSTH_help(aOwner)
 {
   aOwner.helperResult = { type: "help" };
@@ -1828,17 +1827,17 @@ WebConsoleCommands._registerOriginal("pp
   aOwner.helperResult = { rawOutput: true };
 
   if (typeof aObject == "function") {
     return aObject + "\n";
   }
 
   let output = [];
 
-  let obj = WebConsoleUtils.unwrap(aObject);
+  let obj = aObject;
   for (let name in obj) {
     let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {};
     if (desc.get || desc.set) {
       // TODO: Bug 842672 - toolkit/ imports modules from browser/.
       let getGrip = VariablesView.getGrip(desc.get);
       let setGrip = VariablesView.getGrip(desc.set);
       let getString = VariablesView.getString(getGrip);
       let setString = VariablesView.getString(setGrip);