merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 18 Sep 2015 12:03:30 +0200
changeset 295753 67275f876c1555bb816ad1d9120b9f746b7e1345
parent 295741 11dc79e232110ba6de5179e46dfbda77b52a88c3 (current diff)
parent 295752 a97f5b00b7727d6fadeea03b778dd10e3024d625 (diff)
child 295792 37c7812ce0e6d10c7e7182f12e752832835e1d67
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 fx-team to mozilla-central a=merge
--- a/browser/base/content/aboutaccounts/aboutaccounts.js
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -214,18 +214,18 @@ var wrapper = {
     let newAccountEmail = accountData.email;
     // The hosted code may have already checked for the relink situation
     // by sending the can_link_account command. If it did, then
     // it will indicate we don't need to ask twice.
     if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) {
       // we need to tell the page we successfully received the message, but
       // then bail without telling fxAccounts
       this.injectData("message", { status: "login" });
-      // and re-init the page by navigating to about:accounts
-      window.location = "about:accounts";
+      // after a successful login we return to preferences
+      openPrefs();
       return;
     }
     delete accountData.verifiedCanLinkAccount;
 
     // Remember who it was so we can log out next time.
     setPreviousAccountNameHash(newAccountEmail);
 
     // A sync-specific hack - we want to ensure sync has been initialized
@@ -328,17 +328,19 @@ function getStarted() {
 }
 
 function retry() {
   show("remote");
   wrapper.retry();
 }
 
 function openPrefs() {
-  window.openPreferences("paneSync");
+  // Bug 1199303 calls for this tab to always be replaced with Preferences
+  // rather than it opening in a different tab.
+  window.location = "about:preferences#sync";
 }
 
 function init() {
   fxAccounts.getSignedInUser().then(user => {
     // tests in particular might cause the window to start closing before
     // getSignedInUser has returned.
     if (window.closed) {
       return;
@@ -506,18 +508,19 @@ document.addEventListener("DOMContentLoa
 function initObservers() {
   function observe(subject, topic, data) {
     log("about:accounts observed " + topic);
     if (topic == fxAccountsCommon.ONLOGOUT_NOTIFICATION) {
       // All about:account windows get changed to action=signin on logout.
       window.location = "about:accounts?action=signin";
       return;
     }
-    // must be onverified - just about:accounts is loaded.
-    window.location = "about:accounts";
+
+    // must be onverified - we want to open preferences.
+    openPrefs();
   }
 
   for (let topic of OBSERVER_TOPICS) {
     Services.obs.addObserver(observe, topic, false);
   }
   window.addEventListener("unload", function(event) {
     log("about:accounts unloading")
     for (let topic of OBSERVER_TOPICS) {
--- a/browser/components/loop/content/css/contacts.css
+++ b/browser/components/loop/content/css/contacts.css
@@ -131,17 +131,16 @@ html[dir="rtl"] .contact-filter {
 
 .contact,
 .contact-separator {
   padding: .5rem 15px;
   font-size: 13px;
 }
 
 .contact {
-  position: relative;
   display: flex;
   flex-direction: row;
   align-items: center;
   color: #666;
 }
 
 .contact-separator {
   background-color: #eee;
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -152,64 +152,65 @@ loop.contacts = (function(_, mozL10n) {
     }
   });
 
   const ContactDropdown = React.createClass({displayName: "ContactDropdown",
     propTypes: {
       // If the contact is blocked or not.
       blocked: React.PropTypes.bool,
       canEdit: React.PropTypes.bool,
+      // Position of mouse when opening menu
+      eventPosY: React.PropTypes.number.isRequired,
+      // callback function that provides height and top coordinate for contacts container
+      getContainerCoordinates: React.PropTypes.func.isRequired,
       handleAction: React.PropTypes.func.isRequired
     },
 
-    getInitialState: function () {
+    getInitialState: function() {
       return {
         openDirUp: false
       };
     },
 
-    componentDidMount: function () {
-      // This method is called once when the dropdown menu is added to the DOM
-      // inside the contact item.  If the menu extends outside of the visible
-      // area of the scrollable list, it is re-rendered in different direction.
-
-      let menuNode = this.getDOMNode();
-      let menuNodeRect = menuNode.getBoundingClientRect();
-
-      let listNode = document.getElementsByClassName("contact-list")[0];
-      // XXX Workaround the contact-list element not being available in tests.
-      // Assumptions about the embedded DOM are a bad idea, and this needs
-      // reworking. For example, tests use a virtual DOM. Really we should
-      // rework this view with the DropdownMenuMixin, which would save some of this pain.
-      if (!listNode) {
-        return;
-      }
-      let listNodeRect = listNode.getBoundingClientRect();
-
-      if (menuNodeRect.top + menuNodeRect.height >=
-          listNodeRect.top + listNodeRect.height) {
-        this.setState({
-          openDirUp: true
-        });
-      }
-    },
-
     onItemClick: function(event) {
       this.props.handleAction(event.currentTarget.dataset.action);
     },
 
+    componentDidMount: function() {
+      var menuNode = this.getDOMNode();
+      var menuNodeRect = menuNode.getBoundingClientRect();
+      var listNodeCoords = this.props.getContainerCoordinates();
+
+      // Click offset to not display the menu right next to the area clicked.
+      var offset = 10;
+
+      if (this.props.eventPosY + menuNodeRect.height >=
+        listNodeCoords.top + listNodeCoords.height) {
+
+        // Position above click area.
+        menuNode.style.top = this.props.eventPosY - menuNodeRect.height
+          - offset + "px";
+      } else {
+        // Position below click area.
+        menuNode.style.top = this.props.eventPosY + offset + "px";
+      }
+    },
+
     render: function() {
       var cx = React.addons.classSet;
+      var dropdownClasses = cx({
+        "dropdown-menu": true,
+        "dropdown-menu-up": this.state.openDirUp
+      });
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
-        React.createElement("ul", {className: cx({ "dropdown-menu": true,
-                            "dropdown-menu-up": this.state.openDirUp })}, 
+        React.createElement("ul", {className: dropdownClasses}, 
           React.createElement("li", {className: cx({ "dropdown-menu-item": true,
                               "disabled": this.props.blocked,
                               "video-call-item": true }), 
               "data-action": "video-call", 
               onClick: this.onItemClick}, 
             React.createElement("i", {className: "icon icon-video-call"}), 
             mozL10n.get("video_call_menu_button")
           ), 
@@ -239,109 +240,123 @@ loop.contacts = (function(_, mozL10n) {
             mozL10n.get("confirm_delete_contact_remove_button")
           )
         )
       );
     }
   });
 
   const ContactDetail = React.createClass({displayName: "ContactDetail",
-    getInitialState: function() {
-      return {
-        showMenu: false
-      };
-    },
-
     propTypes: {
       contact: React.PropTypes.object.isRequired,
+      getContainerCoordinates: React.PropTypes.func.isRequired,
       handleContactAction: React.PropTypes.func
     },
 
-    _onBodyClick: function() {
-      // Hide the menu after other click handlers have been invoked.
-      setTimeout(this.hideDropdownMenu, 10);
+    mixins: [
+      sharedMixins.DropdownMenuMixin()
+    ],
+
+    getInitialState: function() {
+      return {
+        eventPosY: 0
+      };
     },
 
-    showDropdownMenu: function() {
-      document.body.addEventListener("click", this._onBodyClick);
-      this.setState({showMenu: true});
+    handleShowDropdownClick: function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      this.setState({
+        eventPosY: e.pageY
+      });
+
+      this.toggleDropdownMenu();
     },
 
-    hideDropdownMenu: function() {
-      document.body.removeEventListener("click", this._onBodyClick);
+    hideDropdownMenuHandler: function() {
       // Since this call may be deferred, we need to guard it, for example in
       // case the contact was removed in the meantime.
       if (this.isMounted()) {
-        this.setState({showMenu: false});
+        this.hideDropdownMenu();
       }
     },
 
-    componentWillUnmount: function() {
-      document.body.removeEventListener("click", this._onBodyClick);
-    },
-
     shouldComponentUpdate: function(nextProps, nextState) {
       let currContact = this.props.contact;
       let nextContact = nextProps.contact;
       let currContactEmail = getPreferred(currContact, "email").value;
       let nextContactEmail = getPreferred(nextContact, "email").value;
       return (
         currContact.name[0] !== nextContact.name[0] ||
         currContact.blocked !== nextContact.blocked ||
         currContactEmail !== nextContactEmail ||
         nextState.showMenu !== this.state.showMenu
       );
     },
 
     handleAction: function(actionName) {
       if (this.props.handleContactAction) {
         this.props.handleContactAction(this.props.contact, actionName);
+        this.hideDropdownMenuHandler();
       }
     },
 
     canEdit: function() {
       // We cannot modify imported contacts.  For the moment, the check for
       // determining whether the contact is imported is based on its category.
       return this.props.contact.category[0] !== "google";
     },
 
+    /**
+     * Callback called when moving cursor away from the conversation entry.
+     * Will close the dropdown menu.
+     */
+    _handleMouseOut: function() {
+      if (this.state.showMenu) {
+        this.toggleDropdownMenu();
+      }
+    },
+
     render: function() {
       let names = getContactNames(this.props.contact);
       let email = getPreferred(this.props.contact, "email");
       let avatarSrc = navigator.mozLoop.getUserAvatar(email.value);
       let cx = React.addons.classSet;
       let contactCSSClass = cx({
         contact: true,
         blocked: this.props.contact.blocked
       });
       let avatarCSSClass = cx({
         avatar: true,
         defaultAvatar: !avatarSrc
       });
-
       return (
-        React.createElement("li", {className: contactCSSClass, onMouseLeave: this.hideDropdownMenu}, 
+        React.createElement("li", {className: contactCSSClass, 
+            onMouseLeave: this._handleMouseOut}, 
           React.createElement("div", {className: avatarCSSClass}, 
             avatarSrc ? React.createElement("img", {src: avatarSrc}) : null
           ), 
           React.createElement("div", {className: "details"}, 
             React.createElement("div", {className: "username"}, React.createElement("strong", null, names.firstName), " ", names.lastName, 
               React.createElement("i", {className: cx({"icon icon-blocked": this.props.contact.blocked})})
             ), 
             React.createElement("div", {className: "email"}, email.value)
           ), 
           React.createElement("div", {className: "icons"}, 
             React.createElement("i", {className: "icon icon-contact-video-call", 
                onClick: this.handleAction.bind(null, "video-call")}), 
             React.createElement("i", {className: "icon icon-vertical-ellipsis icon-contact-menu-button", 
-               onClick: this.showDropdownMenu})
+               onClick: this.handleShowDropdownClick})
           ), 
           this.state.showMenu
             ? React.createElement(ContactDropdown, {blocked: this.props.contact.blocked, 
                                canEdit: this.canEdit(), 
+                               eventPosY: this.state.eventPosY, 
+                               getContainerCoordinates: this.props.getContainerCoordinates, 
                                handleAction: this.handleAction})
             : null
           
         )
       );
     }
   });
 
@@ -631,16 +646,27 @@ loop.contacts = (function(_, mozL10n) {
       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;
     },
 
+    getCoordinates: function() {
+      // Returns coordinates for use by child elements to place menus etc that are absolutely positioned
+      var domNode = this.getDOMNode();
+      var domNodeRect = domNode.getBoundingClientRect();
+
+      return {
+        "top": domNodeRect.top,
+        "height": domNodeRect.height
+      };
+    },
+
     _renderFilterClearButton: function() {
       if (this.state.filter) {
         return (
           React.createElement("button", {className: "clear-search", 
                   onClick: this._handleFilterClear})
         );
       }
 
@@ -663,16 +689,17 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     _renderContactsList: function() {
       let cx = React.addons.classSet;
       let shownContacts = this._filterContactsList();
       let viewForItem = item => {
         return (
           React.createElement(ContactDetail, {contact: item, 
+                         getContainerCoordinates: this.getCoordinates, 
                          handleContactAction: this.handleContactAction, 
                          key: item._guid})
         );
       };
 
       // If no contacts to show and filter is set, then none match the search.
       if (!shownContacts.available && !shownContacts.blocked &&
           this.state.filter) {
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -152,64 +152,65 @@ loop.contacts = (function(_, mozL10n) {
     }
   });
 
   const ContactDropdown = React.createClass({
     propTypes: {
       // If the contact is blocked or not.
       blocked: React.PropTypes.bool,
       canEdit: React.PropTypes.bool,
+      // Position of mouse when opening menu
+      eventPosY: React.PropTypes.number.isRequired,
+      // callback function that provides height and top coordinate for contacts container
+      getContainerCoordinates: React.PropTypes.func.isRequired,
       handleAction: React.PropTypes.func.isRequired
     },
 
-    getInitialState: function () {
+    getInitialState: function() {
       return {
         openDirUp: false
       };
     },
 
-    componentDidMount: function () {
-      // This method is called once when the dropdown menu is added to the DOM
-      // inside the contact item.  If the menu extends outside of the visible
-      // area of the scrollable list, it is re-rendered in different direction.
-
-      let menuNode = this.getDOMNode();
-      let menuNodeRect = menuNode.getBoundingClientRect();
-
-      let listNode = document.getElementsByClassName("contact-list")[0];
-      // XXX Workaround the contact-list element not being available in tests.
-      // Assumptions about the embedded DOM are a bad idea, and this needs
-      // reworking. For example, tests use a virtual DOM. Really we should
-      // rework this view with the DropdownMenuMixin, which would save some of this pain.
-      if (!listNode) {
-        return;
-      }
-      let listNodeRect = listNode.getBoundingClientRect();
-
-      if (menuNodeRect.top + menuNodeRect.height >=
-          listNodeRect.top + listNodeRect.height) {
-        this.setState({
-          openDirUp: true
-        });
-      }
-    },
-
     onItemClick: function(event) {
       this.props.handleAction(event.currentTarget.dataset.action);
     },
 
+    componentDidMount: function() {
+      var menuNode = this.getDOMNode();
+      var menuNodeRect = menuNode.getBoundingClientRect();
+      var listNodeCoords = this.props.getContainerCoordinates();
+
+      // Click offset to not display the menu right next to the area clicked.
+      var offset = 10;
+
+      if (this.props.eventPosY + menuNodeRect.height >=
+        listNodeCoords.top + listNodeCoords.height) {
+
+        // Position above click area.
+        menuNode.style.top = this.props.eventPosY - menuNodeRect.height
+          - offset + "px";
+      } else {
+        // Position below click area.
+        menuNode.style.top = this.props.eventPosY + offset + "px";
+      }
+    },
+
     render: function() {
       var cx = React.addons.classSet;
+      var dropdownClasses = cx({
+        "dropdown-menu": true,
+        "dropdown-menu-up": this.state.openDirUp
+      });
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
-        <ul className={cx({ "dropdown-menu": true,
-                            "dropdown-menu-up": this.state.openDirUp })}>
+        <ul className={dropdownClasses}>
           <li className={cx({ "dropdown-menu-item": true,
                               "disabled": this.props.blocked,
                               "video-call-item": true })}
               data-action="video-call"
               onClick={this.onItemClick}>
             <i className="icon icon-video-call" />
             {mozL10n.get("video_call_menu_button")}
           </li>
@@ -239,109 +240,123 @@ loop.contacts = (function(_, mozL10n) {
             {mozL10n.get("confirm_delete_contact_remove_button")}
           </li>
         </ul>
       );
     }
   });
 
   const ContactDetail = React.createClass({
-    getInitialState: function() {
-      return {
-        showMenu: false
-      };
-    },
-
     propTypes: {
       contact: React.PropTypes.object.isRequired,
+      getContainerCoordinates: React.PropTypes.func.isRequired,
       handleContactAction: React.PropTypes.func
     },
 
-    _onBodyClick: function() {
-      // Hide the menu after other click handlers have been invoked.
-      setTimeout(this.hideDropdownMenu, 10);
+    mixins: [
+      sharedMixins.DropdownMenuMixin()
+    ],
+
+    getInitialState: function() {
+      return {
+        eventPosY: 0
+      };
     },
 
-    showDropdownMenu: function() {
-      document.body.addEventListener("click", this._onBodyClick);
-      this.setState({showMenu: true});
+    handleShowDropdownClick: function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      this.setState({
+        eventPosY: e.pageY
+      });
+
+      this.toggleDropdownMenu();
     },
 
-    hideDropdownMenu: function() {
-      document.body.removeEventListener("click", this._onBodyClick);
+    hideDropdownMenuHandler: function() {
       // Since this call may be deferred, we need to guard it, for example in
       // case the contact was removed in the meantime.
       if (this.isMounted()) {
-        this.setState({showMenu: false});
+        this.hideDropdownMenu();
       }
     },
 
-    componentWillUnmount: function() {
-      document.body.removeEventListener("click", this._onBodyClick);
-    },
-
     shouldComponentUpdate: function(nextProps, nextState) {
       let currContact = this.props.contact;
       let nextContact = nextProps.contact;
       let currContactEmail = getPreferred(currContact, "email").value;
       let nextContactEmail = getPreferred(nextContact, "email").value;
       return (
         currContact.name[0] !== nextContact.name[0] ||
         currContact.blocked !== nextContact.blocked ||
         currContactEmail !== nextContactEmail ||
         nextState.showMenu !== this.state.showMenu
       );
     },
 
     handleAction: function(actionName) {
       if (this.props.handleContactAction) {
         this.props.handleContactAction(this.props.contact, actionName);
+        this.hideDropdownMenuHandler();
       }
     },
 
     canEdit: function() {
       // We cannot modify imported contacts.  For the moment, the check for
       // determining whether the contact is imported is based on its category.
       return this.props.contact.category[0] !== "google";
     },
 
+    /**
+     * Callback called when moving cursor away from the conversation entry.
+     * Will close the dropdown menu.
+     */
+    _handleMouseOut: function() {
+      if (this.state.showMenu) {
+        this.toggleDropdownMenu();
+      }
+    },
+
     render: function() {
       let names = getContactNames(this.props.contact);
       let email = getPreferred(this.props.contact, "email");
       let avatarSrc = navigator.mozLoop.getUserAvatar(email.value);
       let cx = React.addons.classSet;
       let contactCSSClass = cx({
         contact: true,
         blocked: this.props.contact.blocked
       });
       let avatarCSSClass = cx({
         avatar: true,
         defaultAvatar: !avatarSrc
       });
-
       return (
-        <li className={contactCSSClass} onMouseLeave={this.hideDropdownMenu}>
+        <li className={contactCSSClass}
+            onMouseLeave={this._handleMouseOut}>
           <div className={avatarCSSClass}>
             {avatarSrc ? <img src={avatarSrc} /> : null}
           </div>
           <div className="details">
             <div className="username"><strong>{names.firstName}</strong> {names.lastName}
               <i className={cx({"icon icon-blocked": this.props.contact.blocked})} />
             </div>
             <div className="email">{email.value}</div>
           </div>
           <div className="icons">
             <i className="icon icon-contact-video-call"
                onClick={this.handleAction.bind(null, "video-call")} />
             <i className="icon icon-vertical-ellipsis icon-contact-menu-button"
-               onClick={this.showDropdownMenu} />
+               onClick={this.handleShowDropdownClick} />
           </div>
           {this.state.showMenu
             ? <ContactDropdown blocked={this.props.contact.blocked}
                                canEdit={this.canEdit()}
+                               eventPosY={this.state.eventPosY}
+                               getContainerCoordinates={this.props.getContainerCoordinates}
                                handleAction={this.handleAction} />
             : null
           }
         </li>
       );
     }
   });
 
@@ -631,16 +646,27 @@ loop.contacts = (function(_, mozL10n) {
       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;
     },
 
+    getCoordinates: function() {
+      // Returns coordinates for use by child elements to place menus etc that are absolutely positioned
+      var domNode = this.getDOMNode();
+      var domNodeRect = domNode.getBoundingClientRect();
+
+      return {
+        "top": domNodeRect.top,
+        "height": domNodeRect.height
+      };
+    },
+
     _renderFilterClearButton: function() {
       if (this.state.filter) {
         return (
           <button className="clear-search"
                   onClick={this._handleFilterClear} />
         );
       }
 
@@ -663,16 +689,17 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     _renderContactsList: function() {
       let cx = React.addons.classSet;
       let shownContacts = this._filterContactsList();
       let viewForItem = item => {
         return (
           <ContactDetail contact={item}
+                         getContainerCoordinates={this.getCoordinates}
                          handleContactAction={this.handleContactAction}
                          key={item._guid} />
         );
       };
 
       // If no contacts to show and filter is set, then none match the search.
       if (!shownContacts.available && !shownContacts.blocked &&
           this.state.filter) {
--- a/browser/components/loop/test/desktop-local/contacts_test.js
+++ b/browser/components/loop/test/desktop-local/contacts_test.js
@@ -169,17 +169,19 @@ describe("loop.contacts", function() {
       calls: {
         startDirectCall: sinon.stub(),
         clearCallInProgress: sinon.stub()
       },
       generateUUID: sandbox.stub()
     };
 
     fakeWindow = {
-      close: sandbox.stub()
+      close: sandbox.stub(),
+      addEventListener: function() {},
+      removeEventListener: function() {}
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
     notifications = new loop.shared.models.NotificationCollection();
 
     document.mozL10n.initialize(fakeMozLoop);
   });
 
@@ -671,17 +673,18 @@ describe("loop.contacts", function() {
             callback(null, [contact]);
           };
 
           view = mountTestComponent();
           node = view.getDOMNode();
 
           // Open the menu
           var menuButton = node.querySelector(".icon-contact-menu-button");
-          TestUtils.Simulate.click(menuButton);
+          var eventStub = {"pageY": 20};
+          TestUtils.Simulate.click(menuButton, eventStub);
 
           // Get the menu for use in the tests.
           contactMenu = node.querySelector(".contact > .dropdown-menu");
         });
 
         describe("Video Conversation button", function() {
           it("should call startDirectCall when the button is clicked", function() {
             TestUtils.Simulate.click(contactMenu.querySelector(".video-call-item"));
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -1024,42 +1024,47 @@
           React.createElement(Section, {name: "ContactDetail"}, 
             React.createElement(FramedExample, {cssClass: "fx-embedded-panel", 
                            dashed: true, 
                            height: 50, 
                            summary: "ContactDetail", 
                            width: 334}, 
               React.createElement("div", {className: "panel force-menu-show"}, 
                 React.createElement(ContactDetail, {contact: fakeManyContacts[0], 
+                               getContainerCoordinates: function() { return {"top": 0, "height": 0 }; }, 
                                handleContactAction: function() {}})
               )
             )
           ), 
 
           React.createElement(Section, {name: "ContactDropdown"}, 
             React.createElement(FramedExample, {cssClass: "fx-embedded-panel", 
                            dashed: true, 
                            height: 272, 
                            summary: "ContactDropdown not blocked can edit", 
                            width: 300}, 
              React.createElement("div", {className: "panel"}, 
                React.createElement(ContactDropdown, {blocked: false, 
                                 canEdit: true, 
+                                eventPosY: 0, 
+                                getContainerCoordinates: function() { return {"top": 0, "height": 0 }; }, 
                                 handleAction: function () {}})
              )
             ), 
             React.createElement(FramedExample, {cssClass: "fx-embedded-panel", 
                            dashed: true, 
                            height: 272, 
                            summary: "ContactDropdown blocked can't edit", 
                            width: 300}, 
              React.createElement("div", {className: "panel"}, 
                React.createElement(ContactDropdown, {blocked: true, 
-                 canEdit: false, 
-                 handleAction: function () {}})
+                                canEdit: false, 
+                                eventPosY: 0, 
+                                getContainerCoordinates: function() { return {"top": 0, "height": 0 }; }, 
+                                handleAction: function () {}})
              )
             )
           ), 
 
           React.createElement(Section, {name: "AcceptCallView"}, 
             React.createElement(FramedExample, {dashed: true, 
                            height: 272, 
                            summary: "Default / incoming video call", 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -1024,42 +1024,47 @@
           <Section name="ContactDetail">
             <FramedExample cssClass="fx-embedded-panel"
                            dashed={true}
                            height={50}
                            summary="ContactDetail"
                            width={334}>
               <div className="panel force-menu-show">
                 <ContactDetail contact={fakeManyContacts[0]}
+                               getContainerCoordinates={function() { return {"top": 0, "height": 0 }; }}
                                handleContactAction={function() {}} />
               </div>
             </FramedExample>
           </Section>
 
           <Section name="ContactDropdown">
             <FramedExample cssClass="fx-embedded-panel"
                            dashed={true}
                            height={272}
                            summary="ContactDropdown not blocked can edit"
                            width={300}>
              <div className="panel">
                <ContactDropdown blocked={false}
                                 canEdit={true}
+                                eventPosY={0}
+                                getContainerCoordinates={function() { return {"top": 0, "height": 0 }; }}
                                 handleAction={function () {}} />
              </div>
             </FramedExample>
             <FramedExample cssClass="fx-embedded-panel"
                            dashed={true}
                            height={272}
                            summary="ContactDropdown blocked can't edit"
                            width={300}>
              <div className="panel">
                <ContactDropdown blocked={true}
-                 canEdit={false}
-                 handleAction={function () {}} />
+                                canEdit={false}
+                                eventPosY={0}
+                                getContainerCoordinates={function() { return {"top": 0, "height": 0 }; }}
+                                handleAction={function () {}} />
              </div>
             </FramedExample>
           </Section>
 
           <Section name="AcceptCallView">
             <FramedExample dashed={true}
                            height={272}
                            summary="Default / incoming video call"
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -602,20 +602,17 @@ var gSyncPane = {
   _openAboutAccounts: function(action) {
     let entryPoint = this._getEntryPoint();
     let params = new URLSearchParams();
     if (action) {
       params.set("action", action);
     }
     params.set("entrypoint", entryPoint);
 
-    this.openContentInBrowser("about:accounts?" + params, {
-      replaceQueryString: true
-    });
-
+    this.replaceTabWithUrl("about:accounts?" + params);
   },
 
   /**
    * Invoke the Sync setup wizard.
    *
    * @param wizardType
    *        Indicates type of wizard to launch:
    *          null    -- regular set up wizard
@@ -648,16 +645,27 @@ var gSyncPane = {
       // always use that as it prefers to open a new window rather than use
       // an existing one.
       gSyncUtils._openLink(url);
       return;
     }
     win.switchToTabHavingURI(url, true, options);
   },
 
+  // Replace the current tab with the specified URL.
+  replaceTabWithUrl(url) {
+    // Get the <browser> element hosting us.
+    let browser = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIWebNavigation)
+                        .QueryInterface(Ci.nsIDocShell)
+                        .chromeEventHandler;
+    // And tell it to load our URL.
+    browser.loadURI(url);
+  },
+
   openPrivacyPolicy: function(aEvent) {
     aEvent.stopPropagation();
     gSyncUtils.openPrivacyPolicy();
   },
 
   openToS: function(aEvent) {
     aEvent.stopPropagation();
     gSyncUtils.openToS();
--- a/mobile/android/base/docs/uitelemetry.rst
+++ b/mobile/android/base/docs/uitelemetry.rst
@@ -115,18 +115,18 @@ Events
   When the user chooses a locale in the browser locale picker. The selected
   locale is provided as the extra.
 
 ``locale.browser.unselected.1``
   When the user chose a different locale in the browser locale picker, this
   event is fired with the previous locale as the extra. If the previous locale
   could not be determined, "unknown" is provided.
 
-``setdefault.1``
-  Set default home panel.
+``neterror.1``
+  When the user performs actions on the in-content network error page. This should probably be a ``Session``, but it's difficult to start and stop the session reliably.
 
 ``pin.1``, ``unpin.1``
   Sent when the user pinned or unpinned a top site.
 
 ``policynotification.success.1:true``
   Sent when a user has accepted the data notification policy. Can be ``false``
   instead of ``true`` if an error occurs.
 
@@ -135,25 +135,117 @@ Events
 
 ``save.1`` ``unsave.1``
   Saving or unsaving a resource (reader, bookmark, etc.) for viewing later.
   Note: Only used in JavaScript for now.
 
 ``search.1``
   Sent when the user performs a search. Currently used in the search activity.
 
+``search.remove.1``
+  Sent when the user removes a search engine.
+
+``search.restore.1``
+  Sent when the user restores the search engine configuration back to the built-in configuration.
+
+``search.setdefault.1``
+  Sent when the user sets a search engine to be the default.
+
+``setdefault.1``
+  Set default home panel.
+
 ``share.1``
   Sharing content.
 
+``show.1``
+  Sent when a contextual UI element is shown to the user.
+
+``undo.1``
+  Sent when performing an undo-style action, like undoing a closed tab.
+
 Methods
 -------
+``actionbar``
+  Action triggered from an ActionBar UI.
+
+``back``
+  Action triggered from the back button.
+
 ``banner``
   Action triggered from a banner (such as HomeBanner).
+
+``button``
+  Action triggered from a button.
   Note: Only used in JavaScript for now.
 
 ``content``
   Action triggered from a content page.
 
+``contextmenu``
+  Action triggered from a contextmenu. Could be from chrome or content.
+
+``dialog``
+  Action triggered from a dialog.
+
+``griditem``
+  Action triggered from a griditem, such as those used in Top Sites panel.
+
+``homescreen``
+  Action triggered from a homescreen shortcut icon.
+
+``intent``
+  Action triggered from a system Intent, usually sent from the OS.
+
+``list``
+  Action triggered from an unmanaged list of items, usually provided by the OS.
+
+``listitem``
+  Action triggered from a listitem.
+
+``menu``
+  Action triggered from the main menu.
+
+``notification``
+  Action triggered from a system notification.
+
+``pageaction``
+  Action triggered from a pageaction, displayed in the URL bar.
+
+``settings``
+  Action triggered from a content page.
+
+``shareoverlay``
+  Action triggered from a content page.
+
+``suggestion``
+  Action triggered from a suggested result, like those from search engines or default tiles.
+
+``toast``
+  Action triggered from an unobtrusive, temporary notification.
+
+``widget``
+  Action triggered from a widget placed on the homescreen.
+
 Sessions
 --------
+``awesomescreen.1``
+  Awesomescreen (including frecency search) is active.
+
+``firstrun.1``
+  Started the very first time we believe the application has been launched.
+
+``frecency.1``
+  Awesomescreen frecency search is active.
+
+``homepanel.1``
+  Started when a user enters a given home panel.
+  Session name is dynamic, encoded as "homepanel.1:<panel_id>"
+  Built-in home panels have fixed IDs
+
+``reader.1``
+  Reader viewer becomes active in the foreground.
+
 ``searchactivity.1``
   Started when the user launches the search activity (onStart) and stopped
   when they leave the search activity.
+
+``settings.1``
+  Settings activity is active.
--- a/mobile/android/gradle/build.gradle
+++ b/mobile/android/gradle/build.gradle
@@ -1,8 +1,10 @@
+import java.util.regex.Pattern
+
 allprojects {
     // Expose the per-object-directory configuration to all projects.
     ext {
         mozconfig = gradle.mozconfig
         topsrcdir = gradle.mozconfig.topsrcdir
         topobjdir = gradle.mozconfig.topobjdir
     }
 
@@ -57,20 +59,25 @@ task generateCodeAndResources(type:Exec)
             throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
         }
     }
 }
 
 // Skip unit test for all build variants, unless if it was specifically requested by user.
 // The enabled property for the unit test tasks is reset based on the command line task names just before the task execution.
 // I bet there is a easier/cleaner way to do this, but this gets the job done for now.
+Pattern pattern = Pattern.compile('.*test(.+UnitTest)?.*')
+boolean startTasksIncludeTest = gradle.startParameter.taskNames.any {
+    taskName ->
+        taskName.matches(pattern)
+}
 gradle.taskGraph.beforeTask {
     Task task ->
-        if (task.name.startsWith('test') && task.name.endsWith('UnitTest')) {
-            task.enabled = gradle.startParameter.taskNames.contains('test')
+        if (task.name.matches(pattern)) {
+            task.enabled = startTasksIncludeTest
         }
 }
 
 afterEvaluate {
     subprojects {
         if (!hasProperty('android')) {
             return
         }
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4759,26 +4759,26 @@ pref("dom.inter-app-communication-api.en
 // Disable mapped array buffer by default.
 pref("dom.mapped_arraybuffer.enabled", false);
 
 // The tables used for Safebrowsing phishing and malware checks.
 pref("urlclassifier.malwareTable", "goog-malware-shavar,goog-unwanted-shavar,test-malware-simple,test-unwanted-simple");
 pref("urlclassifier.phishTable", "goog-phish-shavar,test-phish-simple");
 pref("urlclassifier.downloadBlockTable", "");
 pref("urlclassifier.downloadAllowTable", "");
-pref("urlclassifier.disallow_completions", "test-malware-simple,test-phish-simple,test-unwanted-simple,test-track-simple,test-trackwhite-simple,goog-downloadwhite-digest256,mozpub-track-digest256,mozpub-trackwhite-digest256");
+pref("urlclassifier.disallow_completions", "test-malware-simple,test-phish-simple,test-unwanted-simple,test-track-simple,test-trackwhite-simple,goog-downloadwhite-digest256,mozstd-track-digest256,mozstd-trackwhite-digest256");
 
 // The table and update/gethash URLs for Safebrowsing phishing and malware
 // checks.
-pref("urlclassifier.trackingTable", "test-track-simple,mozpub-track-digest256");
-pref("urlclassifier.trackingWhitelistTable", "test-trackwhite-simple,mozpub-trackwhite-digest256");
-
-pref("browser.safebrowsing.provider.mozilla.lists", "mozpub-track-digest256,mozpub-trackwhite-digest256");
-pref("browser.safebrowsing.provider.mozilla.updateURL", "https://tracking.services.mozilla.com/downloads?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
-pref("browser.safebrowsing.provider.mozilla.gethashURL", "https://tracking.services.mozilla.com/gethash?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
+pref("urlclassifier.trackingTable", "test-track-simple,mozstd-track-digest256");
+pref("urlclassifier.trackingWhitelistTable", "test-trackwhite-simple,mozstd-trackwhite-digest256");
+
+pref("browser.safebrowsing.provider.mozilla.lists", "mozstd-track-digest256,mozstd-trackwhite-digest256");
+pref("browser.safebrowsing.provider.mozilla.updateURL", "https://shavar.services.mozilla.com/downloads?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
+pref("browser.safebrowsing.provider.mozilla.gethashURL", "https://shavar.services.mozilla.com/gethash?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
 
 // Turn off Spatial navigation by default.
 pref("snav.enabled", false);
 
 // Original caret implementation on collapsed selection.
 pref("touchcaret.enabled", false);
 
 // This will inflate the size of the touch caret frame when checking if user
--- a/testing/mozharness/mozharness/mozilla/building/buildb2gbase.py
+++ b/testing/mozharness/mozharness/mozilla/building/buildb2gbase.py
@@ -361,20 +361,22 @@ class B2GBuildBaseScript(BuildbotMixin, 
         }]
         rev = self.vcs_checkout(**repos[0])
         self.set_buildbot_property("tools_revision", rev, write_to_file=True)
 
     def checkout_sources(self):
         dirs = self.query_abs_dirs()
         gecko_config = self.load_gecko_config()
         b2g_manifest_intree = gecko_config.get('b2g_manifest_intree')
+        b2g_repo = gecko_config.get('b2g_repo','https://git.mozilla.org/b2g/B2G.git')
+        b2g_branch = gecko_config.get('b2g_branch','master')
 
         if gecko_config.get('config_version') >= 2:
             repos = [
-                {'vcs': 'gittool', 'repo': 'https://git.mozilla.org/b2g/B2G.git', 'dest': dirs['work_dir']},
+                {'vcs': 'gittool', 'repo': b2g_repo, 'branch': b2g_branch, 'dest': dirs['work_dir']},
             ]
 
             if b2g_manifest_intree:
                 # Checkout top-level B2G repo now
                 self.vcs_checkout_repos(repos)
                 b2g_manifest_branch = 'master'
 
                 # That may have blown away our build-tools checkout. It would
--- a/toolkit/devtools/shared/system.js
+++ b/toolkit/devtools/shared/system.js
@@ -35,32 +35,38 @@ var CACHED_INFO = null;
 function *getSystemInfo() {
   if (CACHED_INFO) {
     return CACHED_INFO;
   }
 
   let appInfo = Services.appinfo;
   let win = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
   let [processor, compiler] = appInfo.XPCOMABI.split("-");
-  let dpi, useragent, width, height, os, hardware, version, brandName;
+  let dpi, useragent, width, height, os, brandName;
   let appid = appInfo.ID;
   let apptype = APP_MAP[appid];
   let geckoVersion = appInfo.platformVersion;
+  let hardware = "unknown";
+  let version = "unknown";
 
   // B2G specific
   if (apptype === "b2g") {
     os = "B2G";
-    hardware = yield exports.getSetting("deviceinfo.hardware");
-    version = yield exports.getSetting("deviceinfo.os");
+    // `getSetting` does not work in child processes on b2g.
+    // TODO bug 1205797, make this work in child processes.
+    try {
+      hardware = yield exports.getSetting("deviceinfo.hardware");
+      version = yield exports.getSetting("deviceinfo.os");
+    } catch (e) {
+    }
   }
   // Not B2G
   else {
     os = appInfo.OS;
     version = appInfo.version;
-    hardware = "unknown";
   }
 
   let bundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
   if (bundle) {
     brandName = bundle.GetStringFromName("brandFullName");
   } else {
     brandName = null;
   }
@@ -288,17 +294,26 @@ function getOSCPU() {
   // Other OS.
   return 12;
 }
 
 function getSetting(name) {
   let deferred = promise.defer();
 
   if ("@mozilla.org/settingsService;1" in Cc) {
-    let settingsService = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
+    let settingsService;
+
+    // settingsService fails in b2g child processes
+    // TODO bug 1205797, make this work in child processes.
+    try {
+      settingsService = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
+    } catch (e) {
+      return promise.reject(e);
+    }
+
     let req = settingsService.createLock().get(name, {
       handle: (name, value) => deferred.resolve(value),
       handleError: (error) => deferred.reject(error),
     });
   } else {
     deferred.reject(new Error("No settings service"));
   }
   return deferred.promise;