Merge autoland to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 02 Aug 2017 15:47:58 -0700
changeset 372386 4c7317211990d6cf156c103a73a5b3ec41f2dd4d
parent 372247 52285ea5e54c73d3ed824544cef2ee3f195f05e6 (current diff)
parent 372385 5b6833ad8ed993eca8bb4fcbb4668c2a2f3c2d95 (diff)
child 372387 22ad2f44b6f841593a6235262bfda0df17373b1d
push id32273
push userkwierso@gmail.com
push dateWed, 02 Aug 2017 22:48:18 +0000
treeherdermozilla-central@4c7317211990 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone57.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 autoland to central, a=merge MozReview-Commit-ID: 38bhUR3fKcF
browser/themes/linux/linuxShared.inc
browser/themes/linux/tabbrowser/tab-active-middle.png
browser/themes/linux/tabbrowser/tab-background-end.png
browser/themes/linux/tabbrowser/tab-background-middle.png
browser/themes/linux/tabbrowser/tab-background-start.png
browser/themes/linux/tabbrowser/tab-stroke-end.png
browser/themes/linux/tabbrowser/tab-stroke-start.png
browser/themes/osx/tabbrowser/alltabs-box-bkgnd-icon.png
browser/themes/osx/tabbrowser/alltabs-box-bkgnd-icon@2x.png
browser/themes/osx/tabbrowser/tab-active-middle.png
browser/themes/osx/tabbrowser/tab-active-middle@2x.png
browser/themes/osx/tabbrowser/tab-background-end.png
browser/themes/osx/tabbrowser/tab-background-middle.png
browser/themes/osx/tabbrowser/tab-background-start.png
browser/themes/osx/tabbrowser/tab-selected-end-yosemite-inactive.svg
browser/themes/osx/tabbrowser/tab-selected-start-yosemite-inactive.svg
browser/themes/osx/tabbrowser/tab-stroke-end.png
browser/themes/osx/tabbrowser/tab-stroke-end@2x.png
browser/themes/osx/tabbrowser/tab-stroke-start.png
browser/themes/osx/tabbrowser/tab-stroke-start@2x.png
browser/themes/preprocess-tab-svgs.py
browser/themes/shared/tab-selected.svg
browser/themes/tab-svgs.mozbuild
browser/themes/windows/tabbrowser/tab-active-middle.png
browser/themes/windows/tabbrowser/tab-active-middle@2x.png
browser/themes/windows/tabbrowser/tab-background-end-preWin10.png
browser/themes/windows/tabbrowser/tab-background-end-preWin10@2x.png
browser/themes/windows/tabbrowser/tab-background-end.png
browser/themes/windows/tabbrowser/tab-background-end@2x.png
browser/themes/windows/tabbrowser/tab-background-middle-preWin10.png
browser/themes/windows/tabbrowser/tab-background-middle-preWin10@2x.png
browser/themes/windows/tabbrowser/tab-background-middle.png
browser/themes/windows/tabbrowser/tab-background-middle@2x.png
browser/themes/windows/tabbrowser/tab-background-start-preWin10.png
browser/themes/windows/tabbrowser/tab-background-start-preWin10@2x.png
browser/themes/windows/tabbrowser/tab-background-start.png
browser/themes/windows/tabbrowser/tab-background-start@2x.png
browser/themes/windows/tabbrowser/tab-stroke-end.png
browser/themes/windows/tabbrowser/tab-stroke-end@2x.png
browser/themes/windows/tabbrowser/tab-stroke-start.png
browser/themes/windows/tabbrowser/tab-stroke-start@2x.png
browser/themes/windows/windowsShared.inc
mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProviderRemoteTabs.java
services/sync/modules/constants.js
third_party/rust/url/src/percent_encoding.rs
--- a/accessible/base/nsCoreUtils.cpp
+++ b/accessible/base/nsCoreUtils.cpp
@@ -439,17 +439,17 @@ nsCoreUtils::IsErrorPage(nsIDocument *aD
 {
   nsIURI *uri = aDocument->GetDocumentURI();
   bool isAboutScheme = false;
   uri->SchemeIs("about", &isAboutScheme);
   if (!isAboutScheme)
     return false;
 
   nsAutoCString path;
-  uri->GetPath(path);
+  uri->GetPathQueryRef(path);
 
   NS_NAMED_LITERAL_CSTRING(neterror, "neterror");
   NS_NAMED_LITERAL_CSTRING(certerror, "certerror");
 
   return StringBeginsWith(path, neterror) || StringBeginsWith(path, certerror);
 }
 
 bool
--- a/accessible/tests/browser/.eslintrc.js
+++ b/accessible/tests/browser/.eslintrc.js
@@ -13,17 +13,17 @@ module.exports = {
     "block-scoped-var": "error",
     "camelcase": "error",
     "comma-dangle": ["error", "never"],
     "complexity": ["error", 20],
     "consistent-this": "off",
     "curly": ["error", "multi-line"],
     "default-case": "off",
     "dot-location": ["error", "property"],
-    "dot-notation": "error",
+
     "eqeqeq": "off",
     "func-names": "off",
     "func-style": "off",
     "generator-star-spacing": "off",
     "handle-callback-err": ["error", "er"],
     "indent": ["error", 2, {"SwitchCase": 1}],
     "max-nested-callbacks": ["error", 4],
     "max-params": "off",
@@ -82,17 +82,16 @@ module.exports = {
     "space-in-parens": ["error", "never"],
     "space-unary-word-ops": "off",
     "strict": ["error", "global"],
     "valid-jsdoc": "off",
     "vars-on-top": "off",
     "wrap-iife": "off",
     "wrap-regex": "off",
     "yoda": "error",
-
     "guard-for-in": "off",
     "newline-after-var": "off",
     "no-alert": "off",
     "no-eq-null": "off",
     "no-func-assign": "off",
     "no-implied-eval": "off",
     "no-inner-declarations": "off",
     "no-invalid-regexp": "off",
--- a/accessible/tests/mochitest/common.js
+++ b/accessible/tests/mochitest/common.js
@@ -441,18 +441,18 @@ function testAccessibleTree(aAccOrElmOrI
     case "role":
       isRole(acc, accTree[prop], msg);
       break;
 
     case "states":
     case "extraStates":
     case "absentStates":
     case "absentExtraStates": {
-      testStates(acc, accTree["states"], accTree["extraStates"],
-                 accTree["absentStates"], accTree["absentExtraStates"]);
+      testStates(acc, accTree.states, accTree.extraStates,
+                 accTree.absentStates, accTree.absentExtraStates);
       break;
     }
 
     case "tagName":
       is(accTree[prop], acc.DOMNode.tagName, msg);
       break;
 
     case "textAttrs": {
@@ -478,17 +478,17 @@ function testAccessibleTree(aAccOrElmOrI
       if (prop.indexOf("todo_") == 0)
         todo(false, msg);
       else if (prop != "children")
         is(acc[prop], accTree[prop], msg);
     }
   }
 
   // Test children.
-  if ("children" in accTree && accTree["children"] instanceof Array) {
+  if ("children" in accTree && accTree.children instanceof Array) {
     var children = acc.children;
     var childCount = children.length;
 
     if (accTree.children.length != childCount) {
       for (var i = 0; i < Math.max(accTree.children.length, childCount); i++) {
         var accChild = null, testChild = null;
         try {
           testChild = accTree.children[i];
--- a/accessible/tests/mochitest/textattrs/test_general.html
+++ b/accessible/tests/mochitest/textattrs/test_general.html
@@ -200,17 +200,17 @@
 
       attrs = { "text-position": "sub" };
       testTextAttrs(ID, 128, attrs, defAttrs, 128, 142);
 
       //////////////////////////////////////////////////////////////////////////
       // area7
       ID = "area7";
       defAttrs = buildDefaultTextAttrs(ID, "12pt");
-      defAttrs["language"] = "en";
+      defAttrs.language = "en";
       testDefaultTextAttrs(ID, defAttrs);
 
       attrs = {"language": "ru"};
       testTextAttrs(ID, 0, attrs, defAttrs, 0, 6);
 
       attrs = {};
       testTextAttrs(ID, 6, attrs, defAttrs, 6, 7);
 
--- a/accessible/tests/mochitest/treeupdate/test_ariaowns.html
+++ b/accessible/tests/mochitest/treeupdate/test_ariaowns.html
@@ -462,17 +462,17 @@
       }
 
       this.finalCheck = function rearrangeARIAOwns_finalCheck()
       {
         var tree = { SECTION: [ ] };
         for (var role of aRoleList) {
           var ch = {};
           ch[role] = [];
-          tree["SECTION"].push(ch);
+          tree.SECTION.push(ch);
         }
         testAccessibleTree(aContainer, tree);
       }
 
       this.getID = function rearrangeARIAOwns_getID()
       {
         return `Rearrange @aria-owns attribute to '${aAttr}'`;
       }
--- a/addon-sdk/source/lib/sdk/url.js
+++ b/addon-sdk/source/lib/sdk/url.js
@@ -130,34 +130,34 @@ function URL(url, base) {
   try {
     fileName = uri.QueryInterface(Ci.nsIURL).fileName;
   } catch (e) {
     if (e.result != Cr.NS_NOINTERFACE) {
       throw e;
     }
   }
 
-  let uriData = [uri.path, uri.path.length, {}, {}, {}, {}, {}, {}];
+  let uriData = [uri.pathQueryRef, uri.pathQueryRef.length, {}, {}, {}, {}, {}, {}];
   URLParser.parsePath.apply(URLParser, uriData);
   let [{ value: filepathPos }, { value: filepathLen },
     { value: queryPos }, { value: queryLen },
     { value: refPos }, { value: refLen }] = uriData.slice(2);
 
   let hash = uri.ref ? "#" + uri.ref : "";
-  let pathname = uri.path.substr(filepathPos, filepathLen);
-  let search = uri.path.substr(queryPos, queryLen);
+  let pathname = uri.pathQueryRef.substr(filepathPos, filepathLen);
+  let search = uri.pathQueryRef.substr(queryPos, queryLen);
   search = search ? "?" + search : "";
 
   this.__defineGetter__("fileName", () => fileName);
   this.__defineGetter__("scheme", () => uri.scheme);
   this.__defineGetter__("userPass", () => userPass);
   this.__defineGetter__("host", () => host);
   this.__defineGetter__("hostname", () => host);
   this.__defineGetter__("port", () => port);
-  this.__defineGetter__("path", () => uri.path);
+  this.__defineGetter__("path", () => uri.pathQueryRef);
   this.__defineGetter__("pathname", () => pathname);
   this.__defineGetter__("hash", () => hash);
   this.__defineGetter__("href", () => uri.spec);
   this.__defineGetter__("origin", () => uri.prePath);
   this.__defineGetter__("protocol", () => uri.scheme + ":");
   this.__defineGetter__("search", () => search);
 
   Object.defineProperties(this, {
--- a/addon-sdk/source/test/addons/content-permissions/httpd.js
+++ b/addon-sdk/source/test/addons/content-permissions/httpd.js
@@ -1641,17 +1641,17 @@ RequestReader.prototype =
       if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
         throw HTTP_400;
 
       try
       {
         var uri = Cc["@mozilla.org/network/io-service;1"]
                     .getService(Ci.nsIIOService)
                     .newURI(fullPath);
-        fullPath = uri.path;
+        fullPath = uri.pathQueryRef;
         scheme = uri.scheme;
         host = metadata._host = uri.asciiHost;
         port = uri.port;
         if (port === -1)
         {
           if (scheme === "http")
             port = 80;
           else if (scheme === "https")
--- a/addon-sdk/source/test/addons/content-script-messages-latency/httpd.js
+++ b/addon-sdk/source/test/addons/content-script-messages-latency/httpd.js
@@ -1641,17 +1641,17 @@ RequestReader.prototype =
       if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
         throw HTTP_400;
 
       try
       {
         var uri = Cc["@mozilla.org/network/io-service;1"]
                     .getService(Ci.nsIIOService)
                     .newURI(fullPath);
-        fullPath = uri.path;
+        fullPath = uri.pathQueryRef;
         scheme = uri.scheme;
         host = metadata._host = uri.asciiHost;
         port = uri.port;
         if (port === -1)
         {
           if (scheme === "http")
             port = 80;
           else if (scheme === "https")
--- a/addon-sdk/source/test/addons/e10s-content/lib/httpd.js
+++ b/addon-sdk/source/test/addons/e10s-content/lib/httpd.js
@@ -1642,17 +1642,17 @@ RequestReader.prototype =
       if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
         throw HTTP_400;
 
       try
       {
         var uri = Cc["@mozilla.org/network/io-service;1"]
                     .getService(Ci.nsIIOService)
                     .newURI(fullPath);
-        fullPath = uri.path;
+        fullPath = uri.pathQueryRef;
         scheme = uri.scheme;
         host = metadata._host = uri.asciiHost;
         port = uri.port;
         if (port === -1)
         {
           if (scheme === "http")
             port = 80;
           else if (scheme === "https")
--- a/addon-sdk/source/test/addons/places/lib/httpd.js
+++ b/addon-sdk/source/test/addons/places/lib/httpd.js
@@ -1641,17 +1641,17 @@ RequestReader.prototype =
       if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
         throw HTTP_400;
 
       try
       {
         var uri = Cc["@mozilla.org/network/io-service;1"]
                     .getService(Ci.nsIIOService)
                     .newURI(fullPath);
-        fullPath = uri.path;
+        fullPath = uri.pathQueryRef;
         scheme = uri.scheme;
         host = metadata._host = uri.asciiHost;
         port = uri.port;
         if (port === -1)
         {
           if (scheme === "http")
             port = 80;
           else if (scheme === "https")
--- a/addon-sdk/source/test/lib/httpd.js
+++ b/addon-sdk/source/test/lib/httpd.js
@@ -1642,17 +1642,17 @@ RequestReader.prototype =
       if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
         throw HTTP_400;
 
       try
       {
         var uri = Cc["@mozilla.org/network/io-service;1"]
                     .getService(Ci.nsIIOService)
                     .newURI(fullPath);
-        fullPath = uri.path;
+        fullPath = uri.pathQueryRef;
         scheme = uri.scheme;
         host = metadata._host = uri.asciiHost;
         port = uri.port;
         if (port === -1)
         {
           if (scheme === "http")
             port = 80;
           else if (scheme === "https")
--- a/browser/base/content/browser-gestureSupport.js
+++ b/browser/base/content/browser-gestureSupport.js
@@ -137,22 +137,22 @@ var gGestureSupport = {
     let isLatched = false;
 
     // Create the update function here to capture closure state
     this._doUpdate = function GS__doUpdate(updateEvent) {
       // Update the offset with new event data
       offset += updateEvent.delta;
 
       // Check if the cumulative deltas exceed the threshold
-      if (Math.abs(offset) > aPref["threshold"]) {
+      if (Math.abs(offset) > aPref.threshold) {
         // Trigger the action if we don't care about latching; otherwise, make
         // sure either we're not latched and going the same direction of the
         // initial motion; or we're latched and going the opposite way
         let sameDir = (latchDir ^ offset) >= 0;
-        if (!aPref["latched"] || (isLatched ^ sameDir)) {
+        if (!aPref.latched || (isLatched ^ sameDir)) {
           this._doAction(updateEvent, [aGesture, offset > 0 ? aInc : aDec]);
 
           // We must be getting latched or leaving it, so just toggle
           isLatched = !isLatched;
         }
 
         // Reset motion counter to prepare for more of the same gesture
         offset = 0;
--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -42,16 +42,19 @@ var BrowserPageActions = {
     delete this.mainViewBodyNode;
     return this.mainViewBodyNode = this.mainViewNode.querySelector(".panel-subview-body");
   },
 
   /**
    * Inits.  Call to init.
    */
   init() {
+    if (!AppConstants.MOZ_PHOTON_THEME) {
+      return;
+    }
     for (let action of PageActions.actions) {
       this.placeAction(action, PageActions.insertBeforeActionIDInUrlbar(action));
     }
   },
 
   /**
    * Adds or removes as necessary DOM nodes for the given action.
    *
@@ -294,16 +297,20 @@ var BrowserPageActions = {
     let buttonNode = document.createElement("image");
     buttonNode.classList.add("urlbar-icon");
     if (action.tooltip) {
       buttonNode.setAttribute("tooltiptext", action.tooltip);
     }
     if (action.iconURL) {
       buttonNode.style.listStyleImage = `url('${action.iconURL}')`;
     }
+    buttonNode.setAttribute("context", "pageActionPanelContextMenu");
+    buttonNode.addEventListener("contextmenu", event => {
+      BrowserPageActions.onContextMenu(event);
+    });
     if (action.nodeAttributes) {
       for (let name in action.nodeAttributes) {
         buttonNode.setAttribute(name, action.nodeAttributes[name]);
       }
     }
     buttonNode.addEventListener("click", event => {
       if (event.button != 0) {
         return;
@@ -415,17 +422,32 @@ var BrowserPageActions = {
    *         or the urlbar.
    * @return (PageAction.Action) The node's related action, or null if none.
    */
   actionForNode(node) {
     if (!node) {
       return null;
     }
     let actionID = this._actionIDForNodeID(node.id);
-    return PageActions.actionForID(actionID);
+    let action = PageActions.actionForID(actionID);
+    if (!action) {
+      // The given node may be an ancestor of a node corresponding to an action,
+      // like how #star-button is contained in #star-button-box, the latter
+      // being the bookmark action's node.  Look up the ancestor chain.
+      for (let n = node.parentNode; n && !action; n = n.parentNode) {
+        if (n.id == "urlbar-icons" || n.localName == "panelview") {
+          // We reached the urlbar icons container or the panelview container.
+          // Stop looking; no acton was found.
+          break;
+        }
+        actionID = this._actionIDForNodeID(n.id);
+        action = PageActions.actionForID(actionID);
+      }
+    }
+    return action;
   },
 
   // The ID of the given action's top-level button in the panel.
   _panelButtonNodeIDForActionID(actionID) {
     return `pageAction-panel-${actionID}`;
   },
 
   // The ID of the given action's button in the urlbar.
@@ -451,17 +473,26 @@ var BrowserPageActions = {
 
   // The ID of the action corresponding to the given top-level button in the
   // panel or button in the urlbar.
   _actionIDForNodeID(nodeID) {
     if (!nodeID) {
       return null;
     }
     let match = nodeID.match(/^pageAction-(?:panel|urlbar)-(.+)$/);
-    return match ? match[1] : null;
+    if (match) {
+      return match[1];
+    }
+    // Check all the urlbar ID overrides.
+    for (let action of PageActions.actions) {
+      if (action.urlbarIDOverride && action.urlbarIDOverride == nodeID) {
+        return action.id;
+      }
+    }
+    return null;
   },
 
   /**
    * Call this when the main page action button in the urlbar is activated.
    *
    * @param  event (DOM event, required)
    *         The click or whatever event.
    */
@@ -587,16 +618,23 @@ BrowserPageActions.bookmark = {
     // Update the button label via the bookmark observer.
     BookmarkingUI.updateBookmarkPageMenuItem();
   },
 
   onCommand(event, buttonNode) {
     BrowserPageActions.panelNode.hidePopup();
     BookmarkingUI.onStarCommand(event);
   },
+
+  onUrlbarNodeClicked(event) {
+    if (event.type == "click" && event.button != 0) {
+      return;
+    }
+    BookmarkingUI.onStarCommand(event);
+  },
 };
 
 // copy URL
 BrowserPageActions.copyURL = {
   onPlacedInPanel(buttonNode) {
     BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title");
   },
 
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -5,26 +5,28 @@
 // This file is loaded into the browser window scope.
 /* eslint-env mozilla/browser-window */
 
 XPCOMUtils.defineLazyScriptGetter(this, ["PlacesToolbar", "PlacesMenu",
                                          "PlacesPanelview", "PlacesPanelMenuView"],
                                   "chrome://browser/content/places/browserPlacesViews.js");
 
 var StarUI = {
-  _itemId: -1,
-  uri: null,
+  _itemGuids: null,
+  // TODO (bug 1131491): _itemIdsMap is only used for the old transactions manager.
+  _itemIdsMap: null,
   _batching: false,
   _isNewBookmark: false,
   _isComposing: false,
   _autoCloseTimer: 0,
   // The autoclose timer is diasbled if the user interacts with the
   // popup, such as making a change through typing or clicking on
   // the popup.
   _autoCloseTimerEnabled: true,
+  _removeBookmarksOnPopupHidden: false,
 
   _element(aID) {
     return document.getElementById(aID);
   },
 
   // Edit-bookmark panel
   get panel() {
     delete this.panel;
@@ -98,42 +100,50 @@ var StarUI = {
           if (!this._element("editBookmarkPanelContent").hidden)
             this.quitEditMode();
 
           if (this._anchorToolbarButton) {
             this._anchorToolbarButton.removeAttribute("open");
             this._anchorToolbarButton = null;
           }
           this._restoreCommandsState();
-          this._itemId = -1;
-          if (this._batching)
+          let removeBookmarksOnPopupHidden = this._removeBookmarksOnPopupHidden;
+          this._removeBookmarksOnPopupHidden = false;
+          let idsForRemoval = this._itemIdsMap;
+          let guidsForRemoval = this._itemGuids;
+          this._itemGuids = null;
+          this._itemIdsMap = null;
+
+          if (this._batching) {
             this.endBatch();
+          }
 
           let libraryButton;
-          if (this._uriForRemoval) {
+          if (removeBookmarksOnPopupHidden && guidsForRemoval) {
             if (this._isNewBookmark) {
               if (!PlacesUIUtils.useAsyncTransactions) {
                 PlacesUtils.transactionManager.undoTransaction();
                 break;
               }
               PlacesTransactions.undo().catch(Cu.reportError);
               break;
             }
             // Remove all bookmarks for the bookmark's url, this also removes
             // the tags for the url.
             if (!PlacesUIUtils.useAsyncTransactions) {
-              let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval);
-              for (let itemId of itemIds) {
-                let txn = new PlacesRemoveItemTransaction(itemId);
-                PlacesUtils.transactionManager.doTransaction(txn);
+              if (idsForRemoval) {
+                for (let itemId of idsForRemoval.values()) {
+                  let txn = new PlacesRemoveItemTransaction(itemId);
+                  PlacesUtils.transactionManager.doTransaction(txn);
+                }
               }
               break;
             }
 
-            PlacesTransactions.RemoveBookmarksForUrls([this._uriForRemoval])
+            PlacesTransactions.Remove(guidsForRemoval)
                               .transact().catch(Cu.reportError);
           } else if (this._isNewBookmark &&
                      Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled") &&
                      AppConstants.MOZ_PHOTON_ANIMATIONS &&
                      (libraryButton = document.getElementById("library-button")) &&
                      libraryButton.getAttribute("cui-areatype") != "menu-panel" &&
                      libraryButton.getAttribute("overflowedItem") != "true" &&
                      libraryButton.closest("#nav-bar")) {
@@ -236,32 +246,33 @@ var StarUI = {
     // Slow double-clicks (not true double-clicks) shouldn't
     // cause the panel to flicker.
     if (this.panel.state == "showing" ||
         this.panel.state == "open") {
       return;
     }
 
     this._isNewBookmark = aIsNewBookmark;
-    this._uriForRemoval = "";
+    this._itemIdsMap = null;
+    this._itemGuids = null;
     // TODO (bug 1131491): Deprecate this once async transactions are enabled
     // and the legacy transactions code is gone.
     if (typeof(aNode) == "number") {
       let itemId = aNode;
       let guid = await PlacesUtils.promiseItemGuid(itemId);
       aNode = await PlacesUIUtils.fetchNodeLike(guid);
     }
 
     // Performance: load the overlay the first time the panel is opened
     // (see bug 392443).
     if (this._overlayLoading)
       return;
 
     if (this._overlayLoaded) {
-      this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
+      await this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
       return;
     }
 
     this._overlayLoading = true;
     document.loadOverlay(
       "chrome://browser/content/places/editBookmarkOverlay.xul",
       (aSubject, aTopic, aData) => {
         // Move the header (star, title, button) into the grid,
@@ -273,17 +284,17 @@ var StarUI = {
 
         this._overlayLoading = false;
         this._overlayLoaded = true;
         this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
       }
     );
   },
 
-  _doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition) {
+  async _doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition) {
     if (this.panel.state != "closed")
       return;
 
     this._blockCommands(); // un-done in the popuphidden handler
 
     this._element("editBookmarkPanelTitle").value =
       this._isNewBookmark ?
         gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
@@ -291,25 +302,33 @@ var StarUI = {
 
     // No description; show the Done, Remove;
     this._element("editBookmarkPanelDescription").textContent = "";
     this._element("editBookmarkPanelBottomButtons").hidden = false;
     this._element("editBookmarkPanelContent").hidden = false;
 
     // The label of the remove button differs if the URI is bookmarked
     // multiple times.
-    let bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
+    this._itemGuids = [];
+
+    await PlacesUtils.bookmarks.fetch({url: gBrowser.currentURI},
+      bookmark => this._itemGuids.push(bookmark.guid));
+
+    if (!PlacesUIUtils.useAsyncTransactions) {
+      this._itemIdsMap = await PlacesUtils.promiseManyItemIds(this._itemGuids);
+    }
     let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
-    let label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length);
+    let bookmarksCount = this._itemGuids.length;
+    let label = PluralForm.get(bookmarksCount, forms)
+                          .replace("#1", bookmarksCount);
     this._element("editBookmarkPanelRemoveButton").label = label;
 
     // unset the unstarred state, if set
     this._element("editBookmarkPanelStarIcon").removeAttribute("unstarred");
 
-    this._itemId = aNode.itemId;
     this.beginBatch();
 
     if (aAnchorElement) {
       // Set the open=true attribute if the anchor is a
       // descendent of a toolbarbutton.
       let parent = aAnchorElement.parentNode;
       while (parent) {
         if (parent.localName == "toolbarbutton") {
@@ -356,17 +375,17 @@ var StarUI = {
 
   quitEditMode: function SU_quitEditMode() {
     this._element("editBookmarkPanelContent").hidden = true;
     this._element("editBookmarkPanelBottomButtons").hidden = true;
     gEditItemOverlay.uninitPanel(true);
   },
 
   removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
-    this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
+    this._removeBookmarksOnPopupHidden = true;
     this.panel.hidePopup();
   },
 
   // Matching the way it is used in the Library, editBookmarkOverlay implements
   // an instant-apply UI, having no batched-Undo/Redo support.
   // However, in this context (the Star UI) we have a Cancel button whose
   // expected behavior is to undo all the operations done in the panel.
   // Sometime in the future this needs to be reimplemented using a
@@ -592,17 +611,17 @@ var PlacesCommandHook = {
    */
   bookmarkCurrentPage: function PCH_bookmarkCurrentPage(aShowEditUI, aParent) {
     this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI)
         .catch(Components.utils.reportError);
   },
 
   /**
    * Adds a bookmark to the page targeted by a link.
-   * @param aParent
+   * @param aParentId
    *        The folder in which to create a new bookmark if aURL isn't
    *        bookmarked.
    * @param aURL (string)
    *        the address of the link target
    * @param aTitle
    *        The link text
    * @param [optional] aDescription
    *        The linked page description, if available
@@ -611,19 +630,23 @@ var PlacesCommandHook = {
     let node = await PlacesUIUtils.fetchNodeLike({ url: aURL });
     if (node) {
       PlacesUIUtils.showBookmarkDialog({ action: "edit",
                                          node
                                        }, window.top);
       return;
     }
 
+    let parentGuid = aParentId == PlacesUtils.bookmarksMenuFolderId ?
+                       PlacesUtils.bookmarks.menuGuid :
+                       await PlacesUtils.promiseItemGuid(aParentId);
     let ip = new InsertionPoint(aParentId,
                                 PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                Components.interfaces.nsITreeView.DROP_ON);
+                                Components.interfaces.nsITreeView.DROP_ON,
+                                null, null, parentGuid);
     PlacesUIUtils.showBookmarkDialog({ action: "add",
                                        type: "bookmark",
                                        uri: makeURI(aURL),
                                        title: aTitle,
                                        description: aDescription,
                                        defaultInsertionPoint: ip,
                                        hiddenRows: [ "description",
                                                      "location",
@@ -691,17 +714,18 @@ var PlacesCommandHook = {
    * @title     title
    *            The title of the feed. Optional.
    * @subtitle  subtitle
    *            A short description of the feed. Optional.
    */
   async addLiveBookmark(url, feedTitle, feedSubtitle) {
     let toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId,
                                        PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                       Components.interfaces.nsITreeView.DROP_ON);
+                                       Components.interfaces.nsITreeView.DROP_ON,
+                                       null, null, PlacesUtils.bookmarks.toolbarGuid);
 
     let feedURI = makeURI(url);
     let title = feedTitle || gBrowser.contentTitle;
     let description = feedSubtitle;
     if (!description) {
       description = (await this._getPageDetails(gBrowser.selectedBrowser)).description;
     }
 
@@ -1107,33 +1131,35 @@ var PlacesMenuDNDHandler = {
   /**
    * Called when the user drags over the <menu> element.
    * @param   event
    *          The DragOver event.
    */
   onDragOver: function PMDH_onDragOver(event) {
     let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
                                 PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                Components.interfaces.nsITreeView.DROP_ON);
+                                Components.interfaces.nsITreeView.DROP_ON,
+                                null, null, PlacesUtils.bookmarks.menuGuid);
     if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer))
       event.preventDefault();
 
     event.stopPropagation();
   },
 
   /**
    * Called when the user drops on the <menu> element.
    * @param   event
    *          The Drop event.
    */
   onDrop: function PMDH_onDrop(event) {
     // Put the item at the end of bookmark menu.
     let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
                                 PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                Components.interfaces.nsITreeView.DROP_ON);
+                                Components.interfaces.nsITreeView.DROP_ON,
+                                null, null, PlacesUtils.bookmarks.menuGuid);
     PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
     PlacesControllerDragHelper.currentDropTarget = null;
     event.stopPropagation();
   }
 };
 
 /**
  * This object handles the initialization and uninitialization of the bookmarks
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4092,18 +4092,20 @@ function FillHistoryMenu(aParent) {
       item.setAttribute("uri", uri);
       item.setAttribute("label", entry.title || uri);
       item.setAttribute("index", j);
 
       // Cache this so that gotoHistoryIndex doesn't need the original index
       item.setAttribute("historyindex", j - index);
 
       if (j != index) {
-        item.setAttribute("image",
-                          PlacesUtils.urlWithSizeRef(window, "page-icon:" + uri, 16));
+        // Use list-style-image rather than the image attribute in order to
+        // allow CSS to override this.
+        item.style.listStyleImage =
+          "url(" + PlacesUtils.urlWithSizeRef(window, "page-icon:" + uri, 16) + ")";
       }
 
       if (j < index) {
         item.className = "unified-nav-back menuitem-iconic menuitem-with-favicon";
         item.setAttribute("tooltiptext", tooltipBack);
       } else if (j == index) {
         item.setAttribute("type", "radio");
         item.setAttribute("checked", "true");
@@ -6570,17 +6572,17 @@ var IndexedDBPromptHelper = {
 
     var browser = requestor.getInterface(Ci.nsIDOMNode);
     if (browser.ownerGlobal != window) {
       // Only listen for notifications for browsers in our chrome window.
       return;
     }
 
     // Get the host name if available or the file path otherwise.
-    var host = browser.currentURI.asciiHost || browser.currentURI.path;
+    var host = browser.currentURI.asciiHost || browser.currentURI.pathQueryRef;
 
     var message;
     var responseTopic;
     if (topic == this._permissionsPrompt) {
       message = gNavigatorBundle.getFormattedString("offlineApps.available2",
                                                     [ host ]);
       responseTopic = this._permissionsResponse;
     }
@@ -7691,31 +7693,31 @@ var gIdentityHandler = {
     try {
       this._uri.host;
       this._uriHasHost = true;
     } catch (ex) {
       this._uriHasHost = false;
     }
 
     let whitelist = /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|searchreset|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
-    this._isSecureInternalUI = uri.schemeIs("about") && whitelist.test(uri.path);
+    this._isSecureInternalUI = uri.schemeIs("about") && whitelist.test(uri.pathQueryRef);
 
     this._isExtensionPage = uri.schemeIs("moz-extension");
 
     // Create a channel for the sole purpose of getting the resolved URI
     // of the request to determine if it's loaded from the file system.
     this._isURILoadedFromFile = false;
     let chanOptions = {uri: this._uri, loadUsingSystemPrincipal: true};
     let resolvedURI;
     try {
       resolvedURI = NetUtil.newChannel(chanOptions).URI;
       if (resolvedURI.schemeIs("jar")) {
         // Given a URI "jar:<jar-file-uri>!/<jar-entry>"
         // create a new URI using <jar-file-uri>!/<jar-entry>
-        resolvedURI = NetUtil.newURI(resolvedURI.path);
+        resolvedURI = NetUtil.newURI(resolvedURI.pathQueryRef);
       }
       // Check the URI again after resolving.
       this._isURILoadedFromFile = resolvedURI.schemeIs("file");
     } catch (ex) {
       // NetUtil's methods will throw for malformed URIs and the like
     }
   },
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -896,26 +896,27 @@
                        onclick="FullZoom.reset();"
                        tooltip="dynamic-shortcut-tooltip"
                        hidden="true"/>
 #ifdef MOZ_PHOTON_THEME
                 <image id="pageActionButton"
                        class="urlbar-icon"
                        tooltiptext="&pageActionButton.tooltip;"
                        onclick="BrowserPageActions.mainButtonClicked(event);"/>
-                <hbox id="star-button-box" hidden="true">
-                  <image id="star-button"
-                         class="urlbar-icon"
-                         onclick="BookmarkingUI.onStarCommand(event);">
+                <hbox id="star-button-box"
+                      hidden="true"
+                      context="pageActionPanelContextMenu"
+                      oncontextmenu="BrowserPageActions.onContextMenu(event);"
+                      onclick="BrowserPageActions.bookmark.onUrlbarNodeClicked(event);">
+                  <image id="star-button" class="urlbar-icon">
                     <observes element="bookmarkThisPageBroadcaster" attribute="starred"/>
                     <observes element="bookmarkThisPageBroadcaster" attribute="tooltiptext"/>
                   </image>
                   <hbox id="star-button-animatable-box">
-                    <image id="star-button-animatable-image"
-                           onclick="BookmarkingUI.onStarCommand(event);"/>
+                    <image id="star-button-animatable-image"/>
                   </hbox>
                 </hbox>
 #endif
               </hbox>
               <hbox id="userContext-icons" hidden="true">
                 <label id="userContext-label"/>
                 <image id="userContext-indicator"/>
               </hbox>
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -521,17 +521,17 @@
         <parameter name="aURI"/>
         <parameter name="aResolvedURI"/>
         <body><![CDATA[
           if (!aURI.schemeIs("about")) {
             return false;
           }
 
           // Specially handle about:blank as local
-          if (aURI.path === "blank") {
+          if (aURI.pathQueryRef === "blank") {
             return true;
           }
 
           try {
             // Use the passed in resolvedURI if we have one
             const resolvedURI = aResolvedURI || Services.io.newChannelFromURI2(
               aURI,
               null, // loadingNode
--- a/browser/base/content/test/general/browser_e10s_about_process.js
+++ b/browser/base/content/test/general/browser_e10s_about_process.js
@@ -28,17 +28,17 @@ function AboutModule() {
 
 AboutModule.prototype = {
   newChannel(aURI, aLoadInfo) {
     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   },
 
   getURIFlags(aURI) {
     for (let module of TEST_MODULES) {
-      if (aURI.path.startsWith(module.path)) {
+      if (aURI.pathQueryRef.startsWith(module.path)) {
         return module.flags;
       }
     }
 
     ok(false, "Called getURIFlags for an unknown page " + aURI.spec);
     return 0;
   },
 
--- a/browser/base/content/test/general/browser_tabfocus.js
+++ b/browser/base/content/test/general/browser_tabfocus.js
@@ -7,26 +7,26 @@
 var testPage1 = "<html id='html1'><body id='body1'><button id='button1'>Tab 1</button></body></html>";
 var testPage2 = "<html id='html2'><body id='body2'><button id='button2'>Tab 2</button></body></html>";
 var testPage3 = "<html id='html3'><body id='body3'><button id='button3'>Tab 3</button></body></html>";
 
 const fm = Services.focus;
 
 function EventStore() {
   this["main-window"] = [];
-  this["window1"] = [];
-  this["window2"] = [];
+  this.window1 = [];
+  this.window2 = [];
 }
 
 EventStore.prototype = {
   "push": function(event) {
     if (event.indexOf("1") > -1) {
-      this["window1"].push(event);
+      this.window1.push(event);
     } else if (event.indexOf("2") > -1) {
-      this["window2"].push(event);
+      this.window2.push(event);
     } else {
       this["main-window"].push(event);
     }
   }
 }
 
 var tab1 = null;
 var tab2 = null;
@@ -538,14 +538,14 @@ function expectFocusShift(callback, expe
     _lastfocuswindow = expectedWindow;
   }
 
   return new Promise((resolve, reject) => {
     currentPromiseResolver = resolve;
     callback();
 
     // No events are expected, so resolve the promise immediately.
-    if (expectedEvents["main-window"].length + expectedEvents["window1"].length + expectedEvents["window2"].length == 0) {
+    if (expectedEvents["main-window"].length + expectedEvents.window1.length + expectedEvents.window2.length == 0) {
       currentPromiseResolver();
       currentPromiseResolver = null;
     }
   });
 }
--- a/browser/base/content/test/performance/browser_windowopen_reflows.js
+++ b/browser/base/content/test/performance/browser_windowopen_reflows.js
@@ -22,17 +22,16 @@ const EXPECTED_REFLOWS = [
   },
 ];
 
 if (Services.appinfo.OS == "Linux") {
   if (gMultiProcessBrowser) {
     EXPECTED_REFLOWS.push({
       stack: [
         "handleEvent@chrome://browser/content/tabbrowser.xml",
-        "EventListener.handleEvent*tabbrowser-tabs_XBL_Constructor@chrome://browser/content/tabbrowser.xml",
       ],
     });
   } else {
     EXPECTED_REFLOWS.push({
       stack: [
         "handleEvent@chrome://browser/content/tabbrowser.xml",
         "inferFromText@chrome://browser/content/browser.js",
         "handleEvent@chrome://browser/content/browser.js",
@@ -69,17 +68,16 @@ if (Services.appinfo.OS == "WINNT") {
         "inferFromText@chrome://browser/content/browser.js",
         "handleEvent@chrome://browser/content/browser.js",
       ],
     },
 
     {
       stack: [
         "handleEvent@chrome://browser/content/tabbrowser.xml",
-        "EventListener.handleEvent*tabbrowser-tabs_XBL_Constructor@chrome://browser/content/tabbrowser.xml",
       ],
     }
   );
 }
 
 if (Services.appinfo.OS == "WINNT" || Services.appinfo.OS == "Darwin") {
   EXPECTED_REFLOWS.push(
     {
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -18,16 +18,19 @@ var gExceptionPaths = [
   "resource://app/defaults/preferences/",
   "resource://gre/modules/commonjs/",
   "resource://gre/defaults/pref/",
   "resource://shield-recipe-client/node_modules/jexl/lib/",
 
   // https://github.com/mozilla/normandy/issues/577
   "resource://shield-recipe-client/test/",
 
+  // https://github.com/mozilla/activity-stream/issues/3053
+  "resource://activity-stream/data/content/tippytop/images/",
+
   // browser/extensions/pdfjs/content/build/pdf.js#1999
   "resource://pdf.js/web/images/",
 ];
 
 // These are not part of the omni.ja file, so we find them only when running
 // the test on a non-packaged build.
 if (AppConstants.platform == "macosx")
   gExceptionPaths.push("resource://gre/res/cursors/");
@@ -517,34 +520,34 @@ add_task(async function checkAllTheFiles
   // our zipreader APIs are all sync)
   let uris = await generateURIsFromDirTree(appDir, [".css", ".manifest", ".json", ".jpg", ".png", ".gif", ".svg",  ".dtd", ".properties"].concat(kCodeExtensions));
 
   // Parse and remove all manifests from the list.
   // NOTE that this must be done before filtering out devtools paths
   // so that all chrome paths can be recorded.
   let manifestPromises = [];
   uris = uris.filter(uri => {
-    let path = uri.path;
+    let path = uri.pathQueryRef;
     if (path.endsWith(".manifest")) {
       manifestPromises.push(parseManifest(uri));
       return false;
     }
 
     return true;
   });
 
   // Wait for all manifest to be parsed
   await Promise.all(manifestPromises);
 
   // We build a list of promises that get resolved when their respective
   // files have loaded and produced no errors.
   let allPromises = [];
 
   for (let uri of uris) {
-    let path = uri.path;
+    let path = uri.pathQueryRef;
     if (path.endsWith(".css"))
       allPromises.push(parseCSSFile(uri));
     else if (kCodeExtensions.some(ext => path.endsWith(ext)))
       allPromises.push(parseCodeFile(uri));
   }
 
   // Wait for all the files to have actually loaded:
   await Promise.all(allPromises);
--- a/browser/base/content/test/static/browser_parsable_css.js
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -274,17 +274,17 @@ add_task(async function checkAllTheCSS()
   await iframeLoaded;
   let doc = iframe.contentWindow.document;
 
   // Parse and remove all manifests from the list.
   // NOTE that this must be done before filtering out devtools paths
   // so that all chrome paths can be recorded.
   let manifestPromises = [];
   uris = uris.filter(uri => {
-    if (uri.path.endsWith(".manifest")) {
+    if (uri.pathQueryRef.endsWith(".manifest")) {
       manifestPromises.push(parseManifest(uri));
       return false;
     }
     return true;
   });
   // Wait for all manifest to be parsed
   await Promise.all(manifestPromises);
 
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -77,17 +77,17 @@ add_task(async function bookmark() {
     await promisePageActionPanelOpen();
 
     // The bookmark button should read "Bookmark This Page" and not be starred.
     Assert.equal(bookmarkButton.label, "Bookmark This Page");
     Assert.ok(!bookmarkButton.hasAttribute("starred"));
 
     // Done.
     hiddenPromise = promisePageActionPanelHidden();
-    gPageActionPanel.hidePopup();
+    BrowserPageActions.panelNode.hidePopup();
     await hiddenPromise;
   });
 });
 
 add_task(async function emailLink() {
   // Open an actionable page so that the main page action button appears.  (It
   // does not appear on about:blank for example.)
   let url = "http://example.com/";
@@ -120,17 +120,17 @@ add_task(async function sendToDevice_non
   await BrowserTestUtils.withNewTab("about:home", async () => {
     await promiseSyncReady();
     // Open the panel.  Send to Device should be disabled.
     await promisePageActionPanelOpen();
     let sendToDeviceButton =
       document.getElementById("pageAction-panel-sendToDevice");
     Assert.ok(sendToDeviceButton.disabled);
     let hiddenPromise = promisePageActionPanelHidden();
-    gPageActionPanel.hidePopup();
+    BrowserPageActions.panelNode.hidePopup();
     await hiddenPromise;
   });
 });
 
 add_task(async function sendToDevice_syncNotReady_other_states() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
@@ -175,17 +175,17 @@ add_task(async function sendToDevice_syn
           label: "Verify Your Account...",
         },
       }
     ];
     checkSendToDeviceItems(expectedItems);
 
     // Done, hide the panel.
     let hiddenPromise = promisePageActionPanelHidden();
-    gPageActionPanel.hidePopup();
+    BrowserPageActions.panelNode.hidePopup();
     await hiddenPromise;
 
     cleanUp();
   });
 });
 
 add_task(async function sendToDevice_syncNotReady_configured() {
   // Open a tab that's sendable.
@@ -265,17 +265,17 @@ add_task(async function sendToDevice_syn
         checkSendToDeviceItems(expectedItems);
       } else {
         ok(false, "This should never happen");
       }
     }
 
     // Done, hide the panel.
     let hiddenPromise = promisePageActionPanelHidden();
-    gPageActionPanel.hidePopup();
+    BrowserPageActions.panelNode.hidePopup();
     await hiddenPromise;
     cleanUp();
   });
 });
 
 add_task(async function sendToDevice_notSignedIn() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
@@ -311,17 +311,17 @@ add_task(async function sendToDevice_not
           label: "Learn About Sending Tabs..."
         },
       }
     ];
     checkSendToDeviceItems(expectedItems);
 
     // Done, hide the panel.
     let hiddenPromise = promisePageActionPanelHidden();
-    gPageActionPanel.hidePopup();
+    BrowserPageActions.panelNode.hidePopup();
     await hiddenPromise;
   });
 });
 
 add_task(async function sendToDevice_noDevices() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
@@ -367,17 +367,17 @@ add_task(async function sendToDevice_noD
           label: "Learn About Sending Tabs..."
         }
       }
     ];
     checkSendToDeviceItems(expectedItems);
 
     // Done, hide the panel.
     let hiddenPromise = promisePageActionPanelHidden();
-    gPageActionPanel.hidePopup();
+    BrowserPageActions.panelNode.hidePopup();
     await hiddenPromise;
 
     cleanUp();
 
     await UIState.reset();
   });
 });
 
@@ -433,23 +433,128 @@ add_task(async function sendToDevice_dev
           label: "Send to All Devices"
         }
       }
     );
     checkSendToDeviceItems(expectedItems);
 
     // Done, hide the panel.
     let hiddenPromise = promisePageActionPanelHidden();
-    gPageActionPanel.hidePopup();
+    BrowserPageActions.panelNode.hidePopup();
     await hiddenPromise;
 
     cleanUp();
   });
 });
 
+add_task(async function contextMenu() {
+  // Open an actionable page so that the main page action button appears.
+  let url = "http://example.com/";
+  await BrowserTestUtils.withNewTab(url, async () => {
+    // Open the panel and then open the context menu on the bookmark button.
+    await promisePageActionPanelOpen();
+    let bookmarkButton = document.getElementById("pageAction-panel-bookmark");
+    let contextMenuPromise = promisePanelShown("pageActionPanelContextMenu");
+    EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
+      type: "contextmenu",
+      button: 2,
+    });
+    await contextMenuPromise;
+
+    // The context menu should show "Remove from Address Bar".  Click it.
+    let contextMenuNode = document.getElementById("pageActionPanelContextMenu");
+    Assert.equal(contextMenuNode.childNodes.length, 1,
+                 "Context menu has one child");
+    Assert.equal(contextMenuNode.childNodes[0].label, "Remove from Address Bar",
+                 "Context menu is in the 'remove' state");
+    contextMenuPromise = promisePanelHidden("pageActionPanelContextMenu");
+    EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
+    await contextMenuPromise;
+
+    // The action should be removed from the urlbar.  In this case, the bookmark
+    // star, the node in the urlbar should be hidden.
+    let starButtonBox = document.getElementById("star-button-box");
+    await BrowserTestUtils.waitForCondition(() => {
+      return starButtonBox.hidden;
+    }, "Waiting for star button to become hidden");
+
+    // Open the context menu again on the bookmark button.  (The page action
+    // panel remains open.)
+    contextMenuPromise = promisePanelShown("pageActionPanelContextMenu");
+    EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
+      type: "contextmenu",
+      button: 2,
+    });
+    await contextMenuPromise;
+
+    // The context menu should show "Add to Address Bar".  Click it.
+    Assert.equal(contextMenuNode.childNodes.length, 1,
+                 "Context menu has one child");
+    Assert.equal(contextMenuNode.childNodes[0].label, "Add to Address Bar",
+                 "Context menu is in the 'add' state");
+    contextMenuPromise = promisePanelHidden("pageActionPanelContextMenu");
+    EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
+    await contextMenuPromise;
+
+    // The action should be added to the urlbar.
+    await BrowserTestUtils.waitForCondition(() => {
+      return !starButtonBox.hidden;
+    }, "Waiting for star button to become unhidden");
+
+    // Open the context menu on the bookmark star in the urlbar.
+    contextMenuPromise = promisePanelShown("pageActionPanelContextMenu");
+    EventUtils.synthesizeMouseAtCenter(starButtonBox, {
+      type: "contextmenu",
+      button: 2,
+    });
+    await contextMenuPromise;
+
+    // The context menu should show "Remove from Address Bar".  Click it.
+    Assert.equal(contextMenuNode.childNodes.length, 1,
+                 "Context menu has one child");
+    Assert.equal(contextMenuNode.childNodes[0].label, "Remove from Address Bar",
+                 "Context menu is in the 'remove' state");
+    contextMenuPromise = promisePanelHidden("pageActionPanelContextMenu");
+    EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
+    await contextMenuPromise;
+
+    // The action should be removed from the urlbar.
+    await BrowserTestUtils.waitForCondition(() => {
+      return starButtonBox.hidden;
+    }, "Waiting for star button to become hidden");
+
+    // Finally, add the bookmark star back to the urlbar so that other tests
+    // that rely on it are OK.
+    await promisePageActionPanelOpen();
+    contextMenuPromise = promisePanelShown("pageActionPanelContextMenu");
+    EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
+      type: "contextmenu",
+      button: 2,
+    });
+    await contextMenuPromise;
+    Assert.equal(contextMenuNode.childNodes.length, 1,
+                 "Context menu has one child");
+    Assert.equal(contextMenuNode.childNodes[0].label, "Add to Address Bar",
+                 "Context menu is in the 'add' state");
+    contextMenuPromise = promisePanelHidden("pageActionPanelContextMenu");
+    EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
+    await contextMenuPromise;
+    await BrowserTestUtils.waitForCondition(() => {
+      return !starButtonBox.hidden;
+    }, "Waiting for star button to become unhidden");
+  });
+
+  // urlbar tests that run after this one can break if the mouse is left over
+  // the area where the urlbar popup appears, which seems to happen due to the
+  // above synthesized mouse events.  Move it over the urlbar.
+  EventUtils.synthesizeMouseAtCenter(gURLBar, { type: "mousemove" });
+  gURLBar.focus();
+});
+
+
 function promiseSyncReady() {
   let service = Cc["@mozilla.org/weave/service;1"]
                   .getService(Components.interfaces.nsISupports)
                   .wrappedJSObject;
   return service.whenLoaded().then(() => {
     UIState.isReady();
     return UIState.refresh();
   });
--- a/browser/base/content/test/urlbar/head.js
+++ b/browser/base/content/test/urlbar/head.js
@@ -195,44 +195,60 @@ function promiseNewSearchEngine(basename
       onError(errCode) {
         Assert.ok(false, "addEngine failed with error code " + errCode);
         reject();
       },
     });
   });
 }
 
-let gPageActionPanel = document.getElementById("pageActionPanel");
-
 function promisePageActionPanelOpen() {
-  let button = document.getElementById("pageActionButton");
+  if (BrowserPageActions.panelNode.state == "open") {
+    return Promise.resolve();
+  }
   let shownPromise = promisePageActionPanelShown();
-  EventUtils.synthesizeMouseAtCenter(button, {});
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
   return shownPromise;
 }
 
 function promisePageActionPanelShown() {
-  return promisePageActionPanelEvent("popupshown");
+  return promisePanelShown(BrowserPageActions.panelNode);
 }
 
 function promisePageActionPanelHidden() {
-  return promisePageActionPanelEvent("popuphidden");
+  return promisePanelHidden(BrowserPageActions.panelNode);
+}
+
+function promisePanelShown(panelIDOrNode) {
+  return promisePanelEvent(panelIDOrNode, "popupshown");
+}
+
+function promisePanelHidden(panelIDOrNode) {
+  return promisePanelEvent(panelIDOrNode, "popuphidden");
 }
 
-function promisePageActionPanelEvent(name) {
+function promisePanelEvent(panelIDOrNode, eventType) {
   return new Promise(resolve => {
-    gPageActionPanel.addEventListener(name, () => {
+    let panel = typeof(panelIDOrNode) != "string" ? panelIDOrNode :
+                document.getElementById(panelIDOrNode);
+    if (!panel ||
+        (eventType == "popupshown" && panel.state == "open") ||
+        (eventType == "popuphidden" && panel.state == "closed")) {
+      executeSoon(resolve);
+      return;
+    }
+    panel.addEventListener(eventType, () => {
       executeSoon(resolve);
     }, { once: true });
   });
 }
 
 function promisePageActionViewShown() {
   return new Promise(resolve => {
-    gPageActionPanel.addEventListener("ViewShown", (event) => {
+    BrowserPageActions.panelNode.addEventListener("ViewShown", (event) => {
       let target = event.originalTarget;
       window.setTimeout(() => {
         resolve(target);
       }, 5000);
     }, { once: true });
   });
 }
 
--- a/browser/base/content/test/webextensions/browser_permissions_addons_search.js
+++ b/browser/base/content/test/webextensions/browser_permissions_addons_search.js
@@ -19,17 +19,17 @@ async function installSearch(filename) {
   await searchResultsPromise;
   ok(win.gViewController.currentViewId.startsWith("addons://search"),
      "about:addons is displaying search results");
 
   let list = win.document.getElementById("search-list");
   let item = null;
   for (let child of list.childNodes) {
     if (child.nodeName == "richlistitem" &&
-        child.mAddon.install.sourceURI.path.endsWith(filename)) {
+        child.mAddon.install.sourceURI.pathQueryRef.endsWith(filename)) {
           item = child;
           break;
     }
   }
   ok(item, `Found ${filename} in search results`);
 
   // abracadabara XBL
   item.clientTop;
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -727,38 +727,38 @@ function openAboutDialog() {
 function openPreferences(paneID, extraArgs) {
   let histogram = Services.telemetry.getHistogramById("FX_PREFERENCES_OPENED_VIA");
   if (extraArgs && extraArgs.origin) {
     histogram.add(extraArgs.origin);
   } else {
     histogram.add("other");
   }
   function switchToAdvancedSubPane(doc) {
-    if (extraArgs && extraArgs["advancedTab"]) {
+    if (extraArgs && extraArgs.advancedTab) {
       // After the Preferences reorg works in Bug 1335907, no more advancedPrefs element.
       // The old Preference is pref-off behind `browser.preferences.useOldOrganization` on Nightly.
       // During the transition between the old and new Preferences, should do checking before proceeding.
       let advancedPaneTabs = doc.getElementById("advancedPrefs");
       if (advancedPaneTabs) {
-        advancedPaneTabs.selectedTab = doc.getElementById(extraArgs["advancedTab"]);
+        advancedPaneTabs.selectedTab = doc.getElementById(extraArgs.advancedTab);
       }
     }
   }
 
   // This function is duplicated from preferences.js.
   function internalPrefCategoryNameToFriendlyName(aName) {
     return (aName || "").replace(/^pane./, function(toReplace) { return toReplace[4].toLowerCase(); });
   }
 
   let win = Services.wm.getMostRecentWindow("navigator:browser");
   let friendlyCategoryName = internalPrefCategoryNameToFriendlyName(paneID);
   let params;
-  if (extraArgs && extraArgs["urlParams"]) {
+  if (extraArgs && extraArgs.urlParams) {
     params = new URLSearchParams();
-    let urlParams = extraArgs["urlParams"];
+    let urlParams = extraArgs.urlParams;
     for (let name in urlParams) {
       if (urlParams[name] !== undefined) {
         params.set(name, urlParams[name]);
       }
     }
   }
   let preferencesURL = "about:preferences" + (params ? "?" + params : "") +
                        (friendlyCategoryName ? "#" + friendlyCategoryName : "");
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -107,17 +107,17 @@ static const RedirEntry kRedirMap[] = {
     nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
 };
 
 static nsAutoCString
 GetAboutModuleName(nsIURI *aURI)
 {
   nsAutoCString path;
-  aURI->GetPath(path);
+  aURI->GetPathQueryRef(path);
 
   int32_t f = path.FindChar('#');
   if (f >= 0)
     path.SetLength(f);
 
   f = path.FindChar('?');
   if (f >= 0)
     path.SetLength(f);
--- a/browser/components/contextualidentity/test/browser/browser_forgetAPI_cookie_getCookiesWithOriginAttributes.js
+++ b/browser/components/contextualidentity/test/browser/browser_forgetAPI_cookie_getCookiesWithOriginAttributes.js
@@ -61,18 +61,18 @@ add_task(async function test_cookie_getC
   }
 
   // Check that cookies have been set properly.
   for (let userContextId of Object.keys(USER_CONTEXTS)) {
     let enumerator = getCookiesForOA(TEST_HOST, userContextId);
     ok(enumerator.hasMoreElements(), "Cookies available");
 
     let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
-    is(foundCookie["name"], cookieName, "Check cookie name");
-    is(foundCookie["value"], USER_CONTEXTS[userContextId], "Check cookie value");
+    is(foundCookie.name, cookieName, "Check cookie name");
+    is(foundCookie.value, USER_CONTEXTS[userContextId], "Check cookie value");
   }
 
   // Using getCookiesWithOriginAttributes() to get all cookies for a certain
   // domain by using the originAttributes pattern, and clear all these cookies.
   let enumerator = Services.cookies.getCookiesWithOriginAttributes(JSON.stringify({}), TEST_HOST);
   while (enumerator.hasMoreElements()) {
     let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
     Services.cookies.remove(cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
--- a/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js
+++ b/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js
@@ -140,18 +140,18 @@ async function test_cookie_cleared() {
     await BrowserTestUtils.removeTab(tabs[userContextId].tab);
   }
   // Check that cookies have been set properly.
   for (let userContextId of Object.keys(USER_CONTEXTS)) {
     let enumerator = getCookiesForOA(TEST_HOST, userContextId);
     ok(enumerator.hasMoreElements(), "Cookies available");
 
     let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
-    Assert.equal(foundCookie["name"], COOKIE_NAME, "Check cookie name");
-    Assert.equal(foundCookie["value"], USER_CONTEXTS[userContextId], "Check cookie value");
+    Assert.equal(foundCookie.name, COOKIE_NAME, "Check cookie name");
+    Assert.equal(foundCookie.value, USER_CONTEXTS[userContextId], "Check cookie value");
   }
 
   // Forget the site.
   await ForgetAboutSite.removeDataFromDomain(TEST_HOST);
 
   // Check that whether cookies has been cleared or not.
   for (let userContextId of Object.keys(USER_CONTEXTS)) {
     let enumerator = getCookiesForOA(TEST_HOST, userContextId);
--- a/browser/components/contextualidentity/test/browser/browser_restore_getCookiesWithOriginAttributes.js
+++ b/browser/components/contextualidentity/test/browser/browser_restore_getCookiesWithOriginAttributes.js
@@ -52,18 +52,18 @@ function checkCookies(ignoreContext = nu
   for (let userContextId of Object.keys(USER_CONTEXTS)) {
     if (ignoreContext && userContextId === String(ignoreContext)) {
       continue;
     }
     let enumerator = getCookiesForOA(TEST_HOST, userContextId);
     ok(enumerator.hasMoreElements(), "Cookies available");
 
     let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
-    is(foundCookie["name"], COOKIE_NAME, "Check cookie name");
-    is(foundCookie["value"], USER_CONTEXTS[userContextId], "Check cookie value");
+    is(foundCookie.name, COOKIE_NAME, "Check cookie name");
+    is(foundCookie.value, USER_CONTEXTS[userContextId], "Check cookie value");
   }
 }
 
 function deleteCookies(onlyContext = null) {
   // Using getCookiesWithOriginAttributes() to get all cookies for a certain
   // domain by using the originAttributes pattern, and clear all these cookies.
   let enumerator = Services.cookies.getCookiesWithOriginAttributes(JSON.stringify({}), TEST_HOST);
   while (enumerator.hasMoreElements()) {
@@ -90,18 +90,18 @@ add_task(async function test_cookie_getC
   }
 
   // Check that cookies have been set properly.
   for (let userContextId of Object.keys(USER_CONTEXTS)) {
     let enumerator = getCookiesForOA(TEST_HOST, userContextId);
     ok(enumerator.hasMoreElements(), "Cookies available");
 
     let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
-    is(foundCookie["name"], COOKIE_NAME, "Check cookie name");
-    is(foundCookie["value"], USER_CONTEXTS[userContextId], "Check cookie value");
+    is(foundCookie.name, COOKIE_NAME, "Check cookie name");
+    is(foundCookie.value, USER_CONTEXTS[userContextId], "Check cookie value");
   }
   checkCookies();
 
   deleteCookies(DELETE_CONTEXT);
 
   checkCookies(DELETE_CONTEXT);
 
   deleteCookies();
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -550,17 +550,17 @@ var CustomizableUIInternal = {
     }
     // Default to a toolbar:
     if (!props.has("type")) {
       props.set("type", CustomizableUI.TYPE_TOOLBAR);
     }
     if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
       // Check aProperties instead of props because this check is only interested
       // in the passed arguments, not the state of a potentially pre-existing area.
-      if (!aInternalCaller && aProperties["defaultCollapsed"]) {
+      if (!aInternalCaller && aProperties.defaultCollapsed) {
         throw new Error("defaultCollapsed is only allowed for default toolbars.")
       }
       if (!props.has("defaultCollapsed")) {
         props.set("defaultCollapsed", true);
       }
     } else if (props.has("defaultCollapsed")) {
       throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
     }
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -815,29 +815,27 @@ CustomizeMode.prototype = {
     CustomizableUI.addWidgetToArea(aNode.id, panel);
     if (!this._customizing) {
       CustomizableUI.dispatchToolboxEvent("customizationchange");
     }
 
     if (AppConstants.MOZ_PHOTON_ANIMATIONS &&
         Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled")) {
       let overflowButton = this.document.getElementById("nav-bar-overflow-button");
-      // If the overflow-button is not visible already, we need to force a layout
-      // flush before calculating the height of it (the button is only visible if
-      // either the "nonemptyoverflow" or "overflowing" attribute is present on the toolbar).
-      BrowserUtils.setToolbarButtonHeightProperty(overflowButton, {forceLayoutFlushIfNeeded: true});
-      overflowButton.setAttribute("animate", "true");
-      overflowButton.addEventListener("animationend", function onAnimationEnd(event) {
-        if (event.animationName.startsWith("overflow-animation")) {
-          this.setAttribute("fade", "true");
-        } else if (event.animationName == "overflow-fade") {
-          this.removeEventListener("animationend", onAnimationEnd);
-          this.removeAttribute("animate");
-          this.removeAttribute("fade");
-        }
+      BrowserUtils.setToolbarButtonHeightProperty(overflowButton).then(() => {
+        overflowButton.setAttribute("animate", "true");
+        overflowButton.addEventListener("animationend", function onAnimationEnd(event) {
+          if (event.animationName.startsWith("overflow-animation")) {
+            this.setAttribute("fade", "true");
+          } else if (event.animationName == "overflow-fade") {
+            this.removeEventListener("animationend", onAnimationEnd);
+            this.removeAttribute("animate");
+            this.removeAttribute("fade");
+          }
+        });
       });
     }
   },
 
   removeFromArea(aNode) {
     aNode = this._getCustomizableChildForNode(aNode);
     if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
       aNode = aNode.firstChild;
@@ -2547,17 +2545,17 @@ CustomizeMode.prototype = {
   },
 };
 
 function __dumpDragData(aEvent, caller) {
   if (!gDebug) {
     return;
   }
   let str = "Dumping drag data (" + (caller ? caller + " in " : "") + "CustomizeMode.jsm) {\n";
-  str += "  type: " + aEvent["type"] + "\n";
+  str += "  type: " + aEvent.type + "\n";
   for (let el of ["target", "currentTarget", "relatedTarget"]) {
     if (aEvent[el]) {
       str += "  " + el + ": " + aEvent[el] + "(localName=" + aEvent[el].localName + "; id=" + aEvent[el].id + ")\n";
     }
   }
   for (let prop in aEvent.dataTransfer) {
     if (typeof aEvent.dataTransfer[prop] != "function") {
       str += "  dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n";
--- a/browser/components/distribution.js
+++ b/browser/components/distribution.js
@@ -113,17 +113,17 @@ DistributionCustomizer.prototype = {
         } else if (keys.indexOf(key + "." + this._language) >= 0) {
           key += "." + this._language;
         }
 
         if (!items[itemIndex])
           items[itemIndex] = {};
         items[itemIndex][iprop] = this._ini.getString(section, key);
 
-        if (iprop == "type" && items[itemIndex]["type"] == "default")
+        if (iprop == "type" && items[itemIndex].type == "default")
           defaultIndex = itemIndex;
 
         if (maxIndex < itemIndex)
           maxIndex = itemIndex;
       } else {
         dump(`Key did not match: ${key}\n`);
       }
     }
@@ -260,58 +260,58 @@ DistributionCustomizer.prototype = {
   async _doApplyBookmarks() {
     if (!this._ini)
       return;
 
     let sections = enumToObject(this._ini.getSections());
 
     // The global section, and several of its fields, is required
     // (we also check here to be consistent with applyPrefDefaults below)
-    if (!sections["Global"])
+    if (!sections.Global)
       return;
 
     let globalPrefs = enumToObject(this._ini.getKeys("Global"));
-    if (!(globalPrefs["id"] && globalPrefs["version"] && globalPrefs["about"]))
+    if (!(globalPrefs.id && globalPrefs.version && globalPrefs.about))
       return;
 
     let bmProcessedPref;
     try {
       bmProcessedPref = this._ini.getString("Global",
                                             "bookmarks.initialized.pref");
     } catch (e) {
       bmProcessedPref = "distribution." +
         this._ini.getString("Global", "id") + ".bookmarksProcessed";
     }
 
     let bmProcessed = this._prefs.getBoolPref(bmProcessedPref, false);
 
     if (!bmProcessed) {
-      if (sections["BookmarksMenu"])
+      if (sections.BookmarksMenu)
         await this._parseBookmarksSection(PlacesUtils.bookmarks.menuGuid,
                                           "BookmarksMenu");
-      if (sections["BookmarksToolbar"])
+      if (sections.BookmarksToolbar)
         await this._parseBookmarksSection(PlacesUtils.bookmarks.toolbarGuid,
                                           "BookmarksToolbar");
       this._prefs.setBoolPref(bmProcessedPref, true);
     }
   },
 
   _prefDefaultsApplied: false,
   applyPrefDefaults: function DIST_applyPrefDefaults() {
     this._prefDefaultsApplied = true;
     if (!this._ini)
       return this._checkCustomizationComplete();
 
     let sections = enumToObject(this._ini.getSections());
 
     // The global section, and several of its fields, is required
-    if (!sections["Global"])
+    if (!sections.Global)
       return this._checkCustomizationComplete();
     let globalPrefs = enumToObject(this._ini.getKeys("Global"));
-    if (!(globalPrefs["id"] && globalPrefs["version"] && globalPrefs["about"]))
+    if (!(globalPrefs.id && globalPrefs.version && globalPrefs.about))
       return this._checkCustomizationComplete();
 
     let defaults = new Preferences({defaultBranch: true});
 
     // Global really contains info we set as prefs.  They're only
     // separate because they are "special" (read: required)
 
     defaults.set("distribution.id", this._ini.getString("Global", "id"));
@@ -356,17 +356,17 @@ DistributionCustomizer.prototype = {
           if (value) {
             defaults.set(key, parseValue(value));
           }
           usedPreferences.push(key);
         } catch (e) { /* ignore bad prefs and move on */ }
       }
     }
 
-    if (sections["Preferences"]) {
+    if (sections.Preferences) {
       for (let key of enumerate(this._ini.getKeys("Preferences"))) {
         if (usedPreferences.indexOf(key) > -1) {
           continue;
         }
         try {
           let value = this._ini.getString("Preferences", key);
           if (value) {
             value = value.replace(/%LOCALE%/g, this._locale);
@@ -408,17 +408,17 @@ DistributionCustomizer.prototype = {
             localizedStr.data = "data:text/plain," + key + "=" + value;
             defaults._prefBranch.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedStr);
           }
           usedLocalizablePreferences.push(key);
         } catch (e) { /* ignore bad prefs and move on */ }
       }
     }
 
-    if (sections["LocalizablePreferences"]) {
+    if (sections.LocalizablePreferences) {
       for (let key of enumerate(this._ini.getKeys("LocalizablePreferences"))) {
         if (usedLocalizablePreferences.indexOf(key) > -1) {
           continue;
         }
         try {
           let value = this._ini.getString("LocalizablePreferences", key);
           if (value) {
             value = parseValue(value);
--- a/browser/components/feeds/WebContentConverter.js
+++ b/browser/components/feeds/WebContentConverter.js
@@ -8,17 +8,17 @@ Components.utils.import("resource://gre/
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 function LOG(str) {
-  dump("*** " + str + "\n");
+  // dump("*** " + str + "\n");
 }
 
 const WCCR_CONTRACTID = "@mozilla.org/embeddor.implemented/web-content-handler-registrar;1";
 const WCCR_CLASSID = Components.ID("{792a7e82-06a0-437c-af63-b2d12e808acc}");
 
 const WCC_CLASSID = Components.ID("{db7ebf28-cc40-415f-8a51-1b111851df1e}");
 const WCC_CLASSNAME = "Web Service Handler";
 
--- a/browser/components/migration/.eslintrc.js
+++ b/browser/components/migration/.eslintrc.js
@@ -1,16 +1,15 @@
 "use strict";
 
 module.exports = {
   "rules": {
     "block-scoped-var": "error",
     "comma-dangle": "off",
     "complexity": ["error", {"max": 21}],
-    "dot-notation": "error",
     "indent": ["error", 2, {"SwitchCase": 1, "ArrayExpression": "first", "ObjectExpression": "first"}],
     "max-nested-callbacks": ["error", 3],
     "new-parens": "error",
     "no-extend-native": "error",
     "no-fallthrough": ["error", { "commentPattern": ".*[Ii]ntentional(?:ly)?\\s+fall(?:ing)?[\\s-]*through.*" }],
     "no-multi-str": "error",
     "no-return-assign": "error",
     "no-sequences": "error",
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1891,17 +1891,17 @@ BrowserGlue.prototype = {
         if (defaultBehavior > 0) {
           prefValue = !!(defaultBehavior & Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]);
         }
         Services.prefs.setBoolPref("browser.urlbar.suggest." + type, prefValue);
       }
 
       // Typed behavior will be used only for results from history.
       if (defaultBehavior != -1 &&
-          !!(defaultBehavior & Ci.mozIPlacesAutoComplete["BEHAVIOR_TYPED"])) {
+          !!(defaultBehavior & Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED)) {
         Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true);
       }
     }
 
     if (currentUIVersion < 27) {
       // Fix up document color use:
       const kOldColorPref = "browser.display.use_document_colors";
       if (Services.prefs.prefHasUserValue(kOldColorPref) &&
--- a/browser/components/originattributes/test/browser/file_windowOpenerRestrictionTarget.html
+++ b/browser/components/originattributes/test/browser/file_windowOpenerRestrictionTarget.html
@@ -11,17 +11,17 @@
     let openerRestriction = true;
     let cookieValue;
     if (window.location.search.length > 0) {
       cookieValue = window.location.search.substr(1);
       openerRestriction = false;
     }
 
     try {
-      let openerFrame = window.opener.frames["child"];
+      let openerFrame = window.opener.frames.child;
       let result = openerFrame.document.cookie === cookieValue;
       if (result && !openerRestriction) {
         document.title = "pass";
       }
     } catch (e) {
       if (openerRestriction) {
         document.title = "pass";
       }
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -1084,21 +1084,21 @@ this.PlacesUIUtils = {
       // if node title is empty, try to set the label using host and filename
       // PlacesUtils._uri() will throw if aNode.uri is not a valid URI
       try {
         var uri = PlacesUtils._uri(aNode.uri);
         var host = uri.host;
         var fileName = uri.QueryInterface(Ci.nsIURL).fileName;
         // if fileName is empty, use path to distinguish labels
         if (aDoNotCutTitle) {
-          title = host + uri.path;
+          title = host + uri.pathQueryRef;
         } else {
           title = host + (fileName ?
                            (host ? "/" + this.ellipsis + "/" : "") + fileName :
-                           uri.path);
+                           uri.pathQueryRef);
         }
       } catch (e) {
         // Use (no title) for non-standard URIs (data:, javascript:, ...)
         title = "";
       }
     } else
       title = aNode.title;
 
@@ -1355,17 +1355,17 @@ this.PlacesUIUtils = {
 
   /**
    * Get the folder id for the organizer left-pane folder.
    */
   get allBookmarksFolderId() {
     // ensure the left-pane root is initialized;
     this.leftPaneFolderId;
     delete this.allBookmarksFolderId;
-    return this.allBookmarksFolderId = this.leftPaneQueries["AllBookmarks"];
+    return this.allBookmarksFolderId = this.leftPaneQueries.AllBookmarks;
   },
 
   /**
    * If an item is a left-pane query, returns the name of the query
    * or an empty string if not.
    *
    * @param aItemId id of a container
    * @return the name of the query, or empty string if not a left-pane query
--- a/browser/components/places/content/bookmarkProperties.js
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -161,17 +161,18 @@ var BookmarkPropertiesPanel = {
         this._title = dialogInfo.title;
 
       if ("defaultInsertionPoint" in dialogInfo) {
         this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint;
       } else {
         this._defaultInsertionPoint =
           new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
                              PlacesUtils.bookmarks.DEFAULT_INDEX,
-                             Ci.nsITreeView.DROP_ON);
+                             Ci.nsITreeView.DROP_ON, null, null,
+                             PlacesUtils.bookmarks.menuGuid);
       }
 
       switch (dialogInfo.type) {
         case "bookmark":
           this._itemType = BOOKMARK_ITEM;
           if ("uri" in dialogInfo) {
             NS_ASSERT(dialogInfo.uri instanceof Ci.nsIURI,
                       "uri property should be a uri object");
@@ -488,21 +489,22 @@ var BookmarkPropertiesPanel = {
 
   /**
    * [New Item Mode] Get the insertion point details for the new item, given
    * dialog state and opening arguments.
    *
    * The container-identifier and insertion-index are returned separately in
    * the form of [containerIdentifier, insertionIndex]
    */
-  _getInsertionPointDetails: function BPP__getInsertionPointDetails() {
-    var containerId = this._defaultInsertionPoint.itemId;
-    var indexInContainer = this._defaultInsertionPoint.index;
-
-    return [containerId, indexInContainer];
+  async _getInsertionPointDetails() {
+    return [
+      this._defaultInsertionPoint.itemId,
+      await this._defaultInsertionPoint.getIndex(),
+      await this._defaultInsertionPoint.promiseGuid(),
+    ]
   },
 
   /**
    * Returns a transaction for creating a new bookmark item representing the
    * various fields and opening arguments of the dialog.
    */
   _getCreateNewBookmarkTransaction:
   function BPP__getCreateNewBookmarkTransaction(aContainer, aIndex) {
@@ -580,17 +582,17 @@ var BookmarkPropertiesPanel = {
       annotations.push(this._getDescriptionAnnotation(this._description));
 
     return new PlacesCreateFolderTransaction(this._title, aContainer,
                                              aIndex, annotations,
                                              childItemsTransactions);
   },
 
   async _createNewItem() {
-    let [container, index] = this._getInsertionPointDetails();
+    let [container, index] = await this._getInsertionPointDetails();
     let txn;
     switch (this._itemType) {
       case BOOKMARK_FOLDER:
         txn = this._getCreateNewFolderTransaction(container, index);
         break;
       case LIVEMARK_CONTAINER:
         txn = new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI,
                                                   this._title, container, index);
@@ -627,18 +629,17 @@ var BookmarkPropertiesPanel = {
       }
     });
   },
 
   async _promiseNewItem() {
     if (!PlacesUIUtils.useAsyncTransactions)
       return this._createNewItem();
 
-    let [containerId, index] = this._getInsertionPointDetails();
-    let parentGuid = await PlacesUtils.promiseItemGuid(containerId);
+    let [containerId, index, parentGuid] = await this._getInsertionPointDetails();
     let annotations = [];
     if (this._description) {
       annotations.push({ name: PlacesUIUtils.DESCRIPTION_ANNO,
                          value: this._description });
     }
     if (this._loadInSidebar) {
       annotations.push({ name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
                          value: true });
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -214,17 +214,18 @@ PlacesViewBase.prototype = {
         }
       }
     }
 
     if (PlacesControllerDragHelper.disallowInsertion(container))
       return null;
 
     return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
-                              index, orientation, tagName);
+                              index, orientation, tagName, null,
+                              PlacesUtils.getConcreteItemGuid(container));
   },
 
   buildContextMenu: function PVB_buildContextMenu(aPopup) {
     this._contextMenuShown = aPopup;
     window.updateCommands("places");
     return this.controller.buildContextMenu(aPopup);
   },
 
@@ -1418,68 +1419,73 @@ PlacesToolbar.prototype = {
         // If we are in the middle of it, drop inside it.
         // Otherwise, drop before it, with regards to RTL mode.
         let threshold = eltRect.width * 0.25;
         if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold)
                        : (aEvent.clientX < eltRect.left + threshold)) {
           // Drop before this folder.
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-                               eltIndex, Ci.nsITreeView.DROP_BEFORE);
+                               eltIndex, Ci.nsITreeView.DROP_BEFORE, null, null,
+                               PlacesUtils.getConcreteItemGuid(this._resultNode));
           dropPoint.beforeIndex = eltIndex;
         } else if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
                             : (aEvent.clientX < eltRect.right - threshold)) {
           // Drop inside this folder.
           let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
                         elt._placesNode.title : null;
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(elt._placesNode),
-                               -1, Ci.nsITreeView.DROP_ON,
-                               tagName);
+                               -1, Ci.nsITreeView.DROP_ON, tagName, null,
+                               PlacesUtils.getConcreteItemGuid(elt._placesNode));
           dropPoint.beforeIndex = eltIndex;
           dropPoint.folderElt = elt;
         } else {
           // Drop after this folder.
           let beforeIndex =
             (eltIndex == this._rootElt.childNodes.length - 1) ?
             -1 : eltIndex + 1;
 
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-                               beforeIndex, Ci.nsITreeView.DROP_BEFORE);
+                               beforeIndex, Ci.nsITreeView.DROP_BEFORE, null, null,
+                               PlacesUtils.getConcreteItemGuid(this._resultNode));
           dropPoint.beforeIndex = beforeIndex;
         }
       } else {
         // This is a non-folder node or a read-only folder.
         // Drop before it with regards to RTL mode.
         let threshold = eltRect.width * 0.5;
         if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
                        : (aEvent.clientX < eltRect.left + threshold)) {
           // Drop before this bookmark.
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-                               eltIndex, Ci.nsITreeView.DROP_BEFORE);
+                               eltIndex, Ci.nsITreeView.DROP_BEFORE, null, null,
+                               PlacesUtils.getConcreteItemGuid(this._resultNode));
           dropPoint.beforeIndex = eltIndex;
         } else {
           // Drop after this bookmark.
           let beforeIndex =
             eltIndex == this._rootElt.childNodes.length - 1 ?
             -1 : eltIndex + 1;
           dropPoint.ip =
             new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-                               beforeIndex, Ci.nsITreeView.DROP_BEFORE);
+                               beforeIndex, Ci.nsITreeView.DROP_BEFORE, null, null,
+                               PlacesUtils.getConcreteItemGuid(this._resultNode));
           dropPoint.beforeIndex = beforeIndex;
         }
       }
     } else {
       // We are most likely dragging on the empty area of the
       // toolbar, we should drop after the last node.
       dropPoint.ip =
         new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
-                           -1, Ci.nsITreeView.DROP_BEFORE);
+                           -1, Ci.nsITreeView.DROP_BEFORE, null, null,
+                           PlacesUtils.getConcreteItemGuid(this._resultNode));
       dropPoint.beforeIndex = -1;
     }
 
     return dropPoint;
   },
 
   _setTimer: function PT_setTimer(aTime) {
     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -32,43 +32,65 @@ const RELOAD_ACTION_MOVE = 3;
  *          The index within the container where we should insert
  * @param   aOrientation
  *          The orientation of the insertion. NOTE: the adjustments to the
  *          insertion point to accommodate the orientation should be done by
  *          the person who constructs the IP, not the user. The orientation
  *          is provided for informational purposes only!
  * @param   [optional] aTag
  *          The tag name if this IP is set to a tag, null otherwise.
- * @param   [optional] aDropNearItemId
- *          When defined we will calculate index based on this itemId
+ * @param   [optional] aDropNearNode
+ *          When defined we will calculate index based on this node
+ * @param   [optional] aGuid
+ *          The guid of the parent container
  * @constructor
  */
 function InsertionPoint(aItemId, aIndex, aOrientation, aTagName = null,
-                        aDropNearItemId = false) {
+                        aDropNearNode = null, aGuid = null) {
+
   this.itemId = aItemId;
+  this.guid = aGuid;
   this._index = aIndex;
   this.orientation = aOrientation;
   this.tagName = aTagName;
-  this.dropNearItemId = aDropNearItemId;
+  this.dropNearNode = aDropNearNode;
 }
 
 InsertionPoint.prototype = {
   set index(val) {
     return this._index = val;
   },
 
+ // TODO (Bug 1382991): Remove this backwards compatibility shim.
   promiseGuid() {
-    return PlacesUtils.promiseItemGuid(this.itemId);
+    return this.guid || PlacesUtils.promiseItemGuid(this.itemId);
   },
 
+  // TODO (Bug 1382991): Remove this backwards compatibility shim.
   get index() {
-    if (this.dropNearItemId > 0) {
+    if (this.dropNearNode && typeof this.dropNearNode != "number")
+      throw new Error("dropNearNode is not a number, use getIndex() instead?");
+    if (this.dropNearNode > 0) {
       // If dropNearItemId is set up we must calculate the real index of
       // the item near which we will drop.
-      var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId);
+      var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearNode);
+      return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
+    }
+    return this._index;
+  },
+
+  async getIndex() {
+    // TODO (Bug 1382991): Remove this backwards compatibility check.
+    if (typeof this.dropNearNode == "number")
+      return this.index;
+
+    if (this.dropNearNode) {
+      // If dropNearNode is set up we must calculate the index of the item near
+      // which we will drop.
+      let index = (await PlacesUtils.bookmarks.fetch(this.dropNearNode.bookmarkGuid)).index;
       return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
     }
     return this._index;
   },
 
   get isTag() {
     return typeof(this.tagName) == "string";
   }
@@ -263,20 +285,20 @@ PlacesController.prototype = {
       break;
     case "placesCmd_open:privatewindow":
       PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view, true);
       break;
     case "placesCmd_open:tab":
       PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
       break;
     case "placesCmd_new:folder":
-      this.newItem("folder");
+      this.newItem("folder").catch(Components.utils.reportError);
       break;
     case "placesCmd_new:bookmark":
-      this.newItem("bookmark");
+      this.newItem("bookmark").catch(Components.utils.reportError);
       break;
     case "placesCmd_new:separator":
       this.newSeparator().catch(Components.utils.reportError);
       break;
     case "placesCmd_show:info":
       this.showBookmarkPropertiesForSelection();
       break;
     case "placesCmd_moveBookmarks":
@@ -422,47 +444,47 @@ PlacesController.prototype = {
       var node = nodes[i];
       var nodeType = node.type;
       var uri = null;
 
       // We don't use the nodeIs* methods here to avoid going through the type
       // property way too often
       switch (nodeType) {
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
-          nodeData["query"] = true;
+          nodeData.query = true;
           if (node.parent) {
             switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
               case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
-                nodeData["host"] = true;
+                nodeData.host = true;
                 break;
               case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
               case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
-                nodeData["day"] = true;
+                nodeData.day = true;
                 break;
             }
           }
           break;
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
-          nodeData["folder"] = true;
+          nodeData.folder = true;
           break;
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
-          nodeData["separator"] = true;
+          nodeData.separator = true;
           break;
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
-          nodeData["link"] = true;
+          nodeData.link = true;
           uri = NetUtil.newURI(node.uri);
           if (PlacesUtils.nodeIsBookmark(node)) {
-            nodeData["bookmark"] = true;
+            nodeData.bookmark = true;
             var parentNode = node.parent;
             if (parentNode) {
               if (PlacesUtils.nodeIsTagQuery(parentNode))
-                nodeData["tagChild"] = true;
+                nodeData.tagChild = true;
               else if (this.hasCachedLivemarkInfo(parentNode))
-                nodeData["livemarkChild"] = true;
+                nodeData.livemarkChild = true;
             }
           }
           break;
       }
 
       // annotations
       if (uri) {
         let names = PlacesUtils.annotations.getPageAnnotationNames(uri);
@@ -723,55 +745,56 @@ PlacesController.prototype = {
   },
 
   /**
    * Shows the Add Bookmark UI for the current insertion point.
    *
    * @param aType
    *        the type of the new item (bookmark/livemark/folder)
    */
-  newItem: function PC_newItem(aType) {
+  async newItem(aType) {
     let ip = this._view.insertionPoint;
     if (!ip)
       throw Cr.NS_ERROR_NOT_AVAILABLE;
 
     let performed =
       PlacesUIUtils.showBookmarkDialog({ action: "add",
                                          type: aType,
                                          defaultInsertionPoint: ip,
                                          hiddenRows: [ "folderPicker" ]
                                        }, window.top);
     if (performed) {
       // Select the new item.
       let insertedNodeId = PlacesUtils.bookmarks
-                                      .getIdForItemAt(ip.itemId, ip.index);
+                                      .getIdForItemAt(ip.itemId, await ip.getIndex());
       this._view.selectItems([insertedNodeId], false);
     }
   },
 
   /**
    * Create a new Bookmark separator somewhere.
    */
   async newSeparator() {
     var ip = this._view.insertionPoint;
     if (!ip)
       throw Cr.NS_ERROR_NOT_AVAILABLE;
 
+    let index = await ip.getIndex();
     if (!PlacesUIUtils.useAsyncTransactions) {
-      let txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
+      let txn = new PlacesCreateSeparatorTransaction(ip.itemId, index);
       PlacesUtils.transactionManager.doTransaction(txn);
       // Select the new item.
       let insertedNodeId = PlacesUtils.bookmarks
-                                      .getIdForItemAt(ip.itemId, ip.index);
+                                      .getIdForItemAt(ip.itemId, index);
       this._view.selectItems([insertedNodeId], false);
       return;
     }
 
     let txn = PlacesTransactions.NewSeparator({ parentGuid: await ip.promiseGuid(),
-                                                index: ip.index });
+                                                index });
     let guid = await txn.transact();
     let itemId = await PlacesUtils.promiseItemId(guid);
     // Select the new item.
     this._view.selectItems([itemId], false);
   },
 
   /**
    * Opens a dialog for moving the selected nodes.
@@ -837,35 +860,37 @@ PlacesController.prototype = {
    * A range is an array of adjacent nodes in a view.
    * @param   [in] range
    *          An array of nodes to remove. Should all be adjacent.
    * @param   [out] transactions
    *          An array of transactions.
    * @param   [optional] removedFolders
    *          An array of folder nodes that have already been removed.
    */
-  _removeRange: function PC__removeRange(range, transactions, removedFolders) {
+  async _removeRange(range, transactions, removedFolders) {
     NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
     if (!removedFolders)
       removedFolders = [];
 
     for (var i = 0; i < range.length; ++i) {
       var node = range[i];
       if (this._shouldSkipNode(node, removedFolders))
         continue;
 
       if (PlacesUtils.nodeIsTagQuery(node.parent)) {
         // This is a uri node inside a tag container.  It needs a special
         // untag transaction.
         var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
         var uri = NetUtil.newURI(node.uri);
         if (PlacesUIUtils.useAsyncTransactions) {
           let tag = node.parent.title;
-          if (!tag)
-            tag = PlacesUtils.bookmarks.getItemTitle(tagItemId);
+          if (!tag) {
+            let tagGuid = PlacesUtils.getConcreteItemGuid(node.parent);
+            tag = (await PlacesUtils.bookmarks.fetch(tagGuid)).title;
+          }
           transactions.push(PlacesTransactions.Untag({ uri, tag }));
         } else {
           let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
           transactions.push(txn);
         }
       } else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
                PlacesUtils.nodeIsQuery(node.parent) &&
                PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
@@ -923,18 +948,19 @@ PlacesController.prototype = {
    * @param   txnName
    *          See |remove|.
    */
   async _removeRowsFromBookmarks(txnName) {
     var ranges = this._view.removableSelectionRanges;
     var transactions = [];
     var removedFolders = [];
 
-    for (var i = 0; i < ranges.length; i++)
-      this._removeRange(ranges[i], transactions, removedFolders);
+    for (let range of ranges) {
+      await this._removeRange(range, transactions, removedFolders);
+    }
 
     if (transactions.length > 0) {
       if (PlacesUIUtils.useAsyncTransactions) {
         await PlacesTransactions.batch(transactions);
       } else {
         var txn = new PlacesAggregatedTransaction(txnName, transactions);
         PlacesUtils.transactionManager.doTransaction(txn);
       }
@@ -1267,17 +1293,17 @@ PlacesController.prototype = {
 
     let itemsToSelect = [];
     if (PlacesUIUtils.useAsyncTransactions) {
       if (ip.isTag) {
         let urls = items.filter(item => "uri" in item).map(item => Services.io.newURI(item.uri));
         await PlacesTransactions.Tag({ urls, tag: ip.tagName }).transact();
       } else {
         await PlacesTransactions.batch(async function() {
-          let insertionIndex = ip.index;
+          let insertionIndex = await ip.getIndex();
           let parent = await ip.promiseGuid();
 
           for (let item of items) {
             let doCopy = action == "copy";
 
             // If this is not a copy, check for safety that we can move the
             // source, otherwise report an error and fallback to a copy.
             if (!doCopy &&
@@ -1294,51 +1320,51 @@ PlacesController.prototype = {
             // position.  If index is DEFAULT_INDEX, items are just appended.
             if (insertionIndex != PlacesUtils.bookmarks.DEFAULT_INDEX)
               insertionIndex++;
           }
         });
       }
     } else {
       let transactions = [];
-      let insertionIndex = ip.index;
-      for (let i = 0; i < items.length; ++i) {
+      let insertionIndex = await ip.getIndex();
+      for (let index = insertionIndex, i = 0; i < items.length; ++i) {
         if (ip.isTag) {
           // Pasting into a tag container means tagging the item, regardless of
           // the requested action.
           let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri),
                                                    [ip.itemId]);
           transactions.push(tagTxn);
           continue;
         }
 
         // Adjust index to make sure items are pasted in the correct position.
         // If index is DEFAULT_INDEX, items are just appended.
-        if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX)
-          insertionIndex = ip.index + i;
+        if (index != PlacesUtils.bookmarks.DEFAULT_INDEX)
+          index += i;
 
         // If this is not a copy, check for safety that we can move the source,
         // otherwise report an error and fallback to a copy.
         if (action != "copy" && !PlacesControllerDragHelper.canMoveUnwrappedNode(items[i])) {
           Components.utils.reportError("Tried to move an unmovable Places " +
                                        "node, reverting to a copy operation.");
           action = "copy";
         }
         transactions.push(
           PlacesUIUtils.makeTransaction(items[i], type, ip.itemId,
-                                        insertionIndex, action == "copy")
+                                        index, action == "copy")
         );
       }
 
       let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
       PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
 
       for (let i = 0; i < transactions.length; ++i) {
         itemsToSelect.push(
-          PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i)
+          PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, insertionIndex + i)
         );
       }
     }
 
     // Cut/past operations are not repeatable, so clear the clipboard.
     if (action == "cut") {
       this._clearClipboard();
     }
@@ -1588,23 +1614,23 @@ var PlacesControllerDragHelper = {
         let spec = uri ? uri.spec : "about:blank";
         nodes = [{ uri: spec,
                    title: data.label,
                    type: PlacesUtils.TYPE_X_MOZ_URL}];
       } else
         throw new Error("bogus data was passed as a tab");
 
       for (let unwrapped of nodes) {
-        let index = insertionPoint.index;
+        let index = await insertionPoint.getIndex();
 
         // Adjust insertion index to prevent reversal of dragged items. When you
         // drag multiple elts upward: need to increment index or each successive
         // elt will be inserted at the same index, each above the previous.
         let dragginUp = insertionPoint.itemId == unwrapped.parent &&
-                        index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
+                        index < (await PlacesUtils.bookmarks.fetch(unwrapped.itemGuid)).index;
         if (index != -1 && dragginUp)
           index += movedCount++;
 
         // If dragging over a tag container we should tag the item.
         if (insertionPoint.isTag) {
           let uri = NetUtil.newURI(unwrapped.uri);
           let tagItemId = insertionPoint.itemId;
           if (PlacesUIUtils.useAsyncTransactions)
--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -1006,27 +1006,28 @@ var gEditItemOverlay = {
 
   async newFolder() {
     let ip = this._folderTree.insertionPoint;
 
     // default to the bookmarks menu folder
     if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) {
       ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
                               PlacesUtils.bookmarks.DEFAULT_INDEX,
-                              Ci.nsITreeView.DROP_ON);
+                              Ci.nsITreeView.DROP_ON, null, null,
+                              PlacesUtils.bookmarks.menuGuid);
     }
 
     // XXXmano: add a separate "New Folder" string at some point...
     let title = this._element("newFolderButton").label;
     if (PlacesUIUtils.useAsyncTransactions) {
       let parentGuid = await ip.promiseGuid();
-      await PlacesTransactions.NewFolder({ parentGuid, title, index: ip.index })
+      await PlacesTransactions.NewFolder({ parentGuid, title, index: await ip.getIndex() })
                               .transact().catch(Components.utils.reportError);
     } else {
-      let txn = new PlacesCreateFolderTransaction(title, ip.itemId, ip.index);
+      let txn = new PlacesCreateFolderTransaction(title, ip.itemId, await ip.getIndex());
       PlacesUtils.transactionManager.doTransaction(txn);
     }
 
     this._folderTree.focus();
     this._folderTree.selectItems([ip.itemId]);
     PlacesUtils.asContainer(this._folderTree.selectedNode).containerOpen = true;
     this._folderTree.selectItems([this._lastNewItem]);
     this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
--- a/browser/components/places/content/menu.xml
+++ b/browser/components/places/content/menu.xml
@@ -88,17 +88,19 @@
             let eltY = elt.boxObject.y - scrollboxOffset;
             let eltHeight = elt.boxObject.height;
 
             if (!elt._placesNode) {
               // If we are dragging over a non places node drop at the end.
               dropPoint.ip = new InsertionPoint(
                                   PlacesUtils.getConcreteItemId(resultNode),
                                   -1,
-                                  Ci.nsITreeView.DROP_ON);
+                                  Ci.nsITreeView.DROP_ON,
+                                  null,
+                                  PlacesUtils.getConcreteItemGuid(resultNode));
               // We can set folderElt if we are dropping over a static menu that
               // has an internal placespopup.
               let isMenu = elt.localName == "menu" ||
                  (elt.localName == "toolbarbutton" &&
                   elt.getAttribute("type") == "menu");
               if (isMenu && elt.lastChild &&
                   elt.lastChild.hasAttribute("placespopup"))
                 dropPoint.folderElt = elt;
@@ -113,47 +115,52 @@
               // This is a folder or a tag container.
               if (eventY - eltY < eltHeight * 0.20) {
                 // If mouse is in the top part of the element, drop above folder.
                 dropPoint.ip = new InsertionPoint(
                                     PlacesUtils.getConcreteItemId(resultNode),
                                     -1,
                                     Ci.nsITreeView.DROP_BEFORE,
                                     tagName,
-                                    elt._placesNode.itemId);
+                                    elt._placesNode,
+                                    PlacesUtils.getConcreteItemGuid(resultNode));
                 return dropPoint;
               } else if (eventY - eltY < eltHeight * 0.80) {
                 // If mouse is in the middle of the element, drop inside folder.
                 dropPoint.ip = new InsertionPoint(
                                     PlacesUtils.getConcreteItemId(elt._placesNode),
                                     -1,
                                     Ci.nsITreeView.DROP_ON,
-                                    tagName);
+                                    tagName,
+                                    null,
+                                    PlacesUtils.getConcreteItemGuid(elt._placesNode));
                 dropPoint.folderElt = elt;
                 return dropPoint;
               }
             } else if (eventY - eltY <= eltHeight / 2) {
               // This is a non-folder node or a readonly folder.
               // If the mouse is above the middle, drop above this item.
               dropPoint.ip = new InsertionPoint(
                                   PlacesUtils.getConcreteItemId(resultNode),
                                   -1,
                                   Ci.nsITreeView.DROP_BEFORE,
                                   tagName,
-                                  elt._placesNode.itemId);
+                                  elt._placesNode,
+                                  PlacesUtils.getConcreteItemGuid(resultNode));
               return dropPoint;
             }
 
             // Drop below the item.
             dropPoint.ip = new InsertionPoint(
                                 PlacesUtils.getConcreteItemId(resultNode),
                                 -1,
                                 Ci.nsITreeView.DROP_AFTER,
                                 tagName,
-                                elt._placesNode.itemId);
+                                elt._placesNode,
+                                PlacesUtils.getConcreteItemGuid(resultNode));
             return dropPoint;
         ]]></body>
       </method>
 
       <!-- Sub-menus should be opened when the mouse drags over them, and closed
            when the mouse drags off.  The overFolder object manages opening and
            closing of folders when the mouse hovers. -->
       <field name="_overFolder"><![CDATA[({
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -278,19 +278,19 @@ var PlacesOrganizer = {
    * Sets the search scope based on aNode's properties.
    * @param   aNode
    *          the node to set up scope from
    */
   _setSearchScopeForNode: function PO__setScopeForNode(aNode) {
     let itemId = aNode.itemId;
 
     if (PlacesUtils.nodeIsHistoryContainer(aNode) ||
-        itemId == PlacesUIUtils.leftPaneQueries["History"]) {
+        itemId == PlacesUIUtils.leftPaneQueries.History) {
       PlacesQueryBuilder.setScope("history");
-    } else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) {
+    } else if (itemId == PlacesUIUtils.leftPaneQueries.Downloads) {
       PlacesQueryBuilder.setScope("downloads");
     } else {
       // Default to All Bookmarks for all other nodes, per bug 469437.
       PlacesQueryBuilder.setScope("bookmarks");
     }
   },
 
   /**
--- a/browser/components/places/content/tree.xml
+++ b/browser/components/places/content/tree.xml
@@ -466,17 +466,17 @@
 
       <method name="_getInsertionPoint">
         <parameter name="index"/>
         <parameter name="orientation"/>
         <body><![CDATA[
           var result = this.result;
           var resultview = this.view;
           var container = result.root;
-          var dropNearItemId = -1;
+          var dropNearNode = null;
           NS_ASSERT(container, "null container");
           // When there's no selection, assume the container is the container
           // the view is populated from (i.e. the result's itemId).
           if (index != -1) {
             var lastSelected = resultview.nodeForTreeIndex(index);
             if (resultview.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
               // If the last selected item is an open container, append _into_
               // it, rather than insert adjacent to it.
@@ -510,17 +510,17 @@
                 index = -1;
               } else if (queryOptions.excludeItems ||
                          queryOptions.excludeQueries ||
                          queryOptions.excludeReadOnlyFolders) {
                 // Some item may be invisible, insert near last selected one.
                 // We don't replace index here to avoid requests to the db,
                 // instead it will be calculated later by the controller.
                 index = -1;
-                dropNearItemId = lastSelected.itemId;
+                dropNearNode = lastSelected;
               } else {
                 var lsi = container.getChildIndex(lastSelected);
                 index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
               }
             }
           }
 
           if (PlacesControllerDragHelper.disallowInsertion(container))
@@ -532,17 +532,18 @@
             tagName = container.title;
             if (!tagName)
               return null;
           }
 
           return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
                                     index, orientation,
                                     tagName,
-                                    dropNearItemId);
+                                    dropNearNode,
+                                    PlacesUtils.getConcreteItemGuid(container));
         ]]></body>
       </method>
 
       <!-- nsIPlacesView -->
       <method name="selectAll">
         <body><![CDATA[
           this.view.selection.selectAll();
         ]]></body>
--- a/browser/components/places/content/treeView.js
+++ b/browser/components/places/content/treeView.js
@@ -1368,17 +1368,17 @@ PlacesTreeView.prototype = {
       return false;
 
     let ip = this._getInsertionPoint(aRow, aOrientation);
     return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer);
   },
 
   _getInsertionPoint: function PTV__getInsertionPoint(index, orientation) {
     let container = this._result.root;
-    let dropNearItemId = -1;
+    let dropNearNode = null;
     // When there's no selection, assume the container is the container
     // the view is populated from (i.e. the result's itemId).
     if (index != -1) {
       let lastSelected = this.nodeForTreeIndex(index);
       if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
         // If the last selected item is an open container, append _into_
         // it, rather than insert adjacent to it.
         container = lastSelected;
@@ -1416,17 +1416,17 @@ PlacesTreeView.prototype = {
           index = -1;
         } else if (queryOptions.excludeItems ||
                  queryOptions.excludeQueries ||
                  queryOptions.excludeReadOnlyFolders) {
           // Some item may be invisible, insert near last selected one.
           // We don't replace index here to avoid requests to the db,
           // instead it will be calculated later by the controller.
           index = -1;
-          dropNearItemId = lastSelected.itemId;
+          dropNearNode = lastSelected;
         } else {
           let lsi = container.getChildIndex(lastSelected);
           index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
         }
       }
     }
 
     if (PlacesControllerDragHelper.disallowInsertion(container))
@@ -1438,17 +1438,18 @@ PlacesTreeView.prototype = {
       tagName = container.title;
       if (!tagName)
         return null;
     }
 
     return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
                               index, orientation,
                               tagName,
-                              dropNearItemId);
+                              dropNearNode,
+                              PlacesUtils.getConcreteItemGuid(container));
   },
 
   drop: function PTV_drop(aRow, aOrientation, aDataTransfer) {
     // We are responsible for translating the |index| and |orientation|
     // parameters into a container id and index within the container,
     // since this information is specific to the tree view.
     let ip = this._getInsertionPoint(aRow, aOrientation);
     if (ip) {
--- a/browser/components/places/tests/browser/browser_0_library_left_pane_migration.js
+++ b/browser/components/places/tests/browser/browser_0_library_left_pane_migration.js
@@ -78,13 +78,13 @@ add_task(async function() {
   version = PlacesUtils.annotations.getItemAnnotation(leftPaneRoot,
                                                       PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
   is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION,
      "Left pane version has been correctly upgraded");
 
   // Check left pane is populated.
   organizer.PlacesOrganizer.selectLeftPaneQuery("History");
   is(organizer.PlacesOrganizer._places.selectedNode.itemId,
-     PlacesUIUtils.leftPaneQueries["History"],
+     PlacesUIUtils.leftPaneQueries.History,
      "Library left pane is populated and working");
 
   await promiseLibraryClosed(organizer);
 });
--- a/browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js
@@ -1,15 +1,15 @@
 "use strict"
 
 add_task(async function() {
   info("Add a live bookmark editing its data");
 
   await withSidebarTree("bookmarks", async function(tree) {
-    let itemId = PlacesUIUtils.leftPaneQueries["UnfiledBookmarks"];
+    let itemId = PlacesUIUtils.leftPaneQueries.UnfiledBookmarks;
     tree.selectItems([itemId]);
 
     await withBookmarksDialog(
       true,
       function openDialog() {
         PlacesCommandHook.addLiveBookmark("http://livemark.com/",
                                           "livemark", "description");
       },
--- a/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
@@ -1,15 +1,15 @@
 "use strict"
 
 add_task(async function() {
   info("Bug 479348 - Properties on a root should be read-only.");
 
   await withSidebarTree("bookmarks", async function(tree) {
-    let itemId = PlacesUIUtils.leftPaneQueries["UnfiledBookmarks"];
+    let itemId = PlacesUIUtils.leftPaneQueries.UnfiledBookmarks;
     tree.selectItems([itemId]);
     ok(tree.controller.isCommandEnabled("placesCmd_show:info"),
        "'placesCmd_show:info' on current selected node is enabled");
 
     await withBookmarksDialog(
       true,
       function openDialog() {
         tree.controller.doCommand("placesCmd_show:info");
--- a/browser/components/places/tests/browser/browser_library_search.js
+++ b/browser/components/places/tests/browser/browser_library_search.js
@@ -32,38 +32,38 @@ var gLibrary;
 
 var testCases = [
   function allBookmarksScope() {
     let defScope = getDefaultScope(PlacesUIUtils.allBookmarksFolderId);
     search(PlacesUIUtils.allBookmarksFolderId, "dummy", defScope);
   },
 
   function historyScope() {
-    let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries["History"]);
-    search(PlacesUIUtils.leftPaneQueries["History"], "dummy", defScope);
+    let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries.History);
+    search(PlacesUIUtils.leftPaneQueries.History, "dummy", defScope);
   },
 
   function downloadsScope() {
-    let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries["Downloads"]);
-    search(PlacesUIUtils.leftPaneQueries["Downloads"], "dummy", defScope);
+    let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries.Downloads);
+    search(PlacesUIUtils.leftPaneQueries.Downloads, "dummy", defScope);
   },
 ];
 
 /**
  * Returns the default search scope for a given folder.
  *
  * @param  aFolderId
  *         the item ID of a node in the left pane's tree
  * @return the default scope when the folder is newly selected
  */
 function getDefaultScope(aFolderId) {
   switch (aFolderId) {
-    case PlacesUIUtils.leftPaneQueries["History"]:
+    case PlacesUIUtils.leftPaneQueries.History:
       return "scopeBarHistory"
-    case PlacesUIUtils.leftPaneQueries["Downloads"]:
+    case PlacesUIUtils.leftPaneQueries.Downloads:
       return "scopeBarDownloads";
     default:
       return "scopeBarAll";
   }
 }
 
 /**
  * Returns the single nsINavHistoryQuery represented by a given place URI.
@@ -111,18 +111,18 @@ function search(aFolderId, aSearchStr, a
   // content tree properly.
   if (aFolderId) {
     folderTree.selectItems([aFolderId]);
     isnot(folderTree.selectedNode, null,
        "Sanity check: left pane tree should have selection after selecting!");
 
     // getFolders() on a History query returns an empty array, so no use
     // comparing against aFolderId in that case.
-    if (aFolderId !== PlacesUIUtils.leftPaneQueries["History"] &&
-        aFolderId !== PlacesUIUtils.leftPaneQueries["Downloads"]) {
+    if (aFolderId !== PlacesUIUtils.leftPaneQueries.History &&
+        aFolderId !== PlacesUIUtils.leftPaneQueries.Downloads) {
       // contentTree.place should be equal to contentTree.result.root.uri,
       // but it's not until bug 476952 is fixed.
       let query = queryStringToQuery(contentTree.result.root.uri);
       is(query.getFolders()[0], aFolderId,
          "Content tree's folder should be what was selected in the left pane");
     }
   }
 
--- a/browser/components/preferences/cookies.js
+++ b/browser/components/preferences/cookies.js
@@ -209,18 +209,18 @@ var gCookiesWindow = {
         return this._filterSet[aIndex];
 
       var start = 0;
       var count = 0, hostIndex = 0;
 
       var cacheIndex = Math.min(this._cacheValid, aIndex);
       if (cacheIndex > 0) {
         var cacheItem = this._cacheItems[cacheIndex];
-        start = cacheItem["start"];
-        count = hostIndex = cacheItem["count"];
+        start = cacheItem.start;
+        count = hostIndex = cacheItem.count;
       }
 
       for (let i = start; i < gCookiesWindow._hostOrder.length; ++i) { // var host in gCookiesWindow._hosts) {
         let currHost = gCookiesWindow._hosts[gCookiesWindow._hostOrder[i]];// gCookiesWindow._hosts[host];
         if (!currHost) continue;
         if (count == aIndex)
           return currHost;
         hostIndex = count;
--- a/browser/components/preferences/in-content-new/tests/browser_cookies_dialog.js
+++ b/browser/components/preferences/in-content-new/tests/browser_cookies_dialog.js
@@ -20,17 +20,17 @@ add_task(async function openCookiesSubDi
 
   cookiesDialog = await dialogOpened;
 });
 
 add_task(async function testDeleteCookie() {
   let doc = cookiesDialog.document;
 
   // Add a cookie.
-  Services.cookies.add(URI.host, URI.path, "", "", false, false, true, Date.now());
+  Services.cookies.add(URI.host, URI.pathQueryRef, "", "", false, false, true, Date.now());
 
   let tree = doc.getElementById("cookiesList");
   Assert.equal(tree.view.rowCount, 1, "Row count should initially be 1");
   tree.focus();
   tree.view.selection.select(0);
 
   if (AppConstants.platform == "macosx") {
     EventUtils.synthesizeKey("VK_BACK_SPACE", {});
--- a/browser/components/preferences/in-content/tests/browser_cookies_dialog.js
+++ b/browser/components/preferences/in-content/tests/browser_cookies_dialog.js
@@ -20,17 +20,17 @@ add_task(async function openCookiesSubDi
 
   cookiesDialog = await dialogOpened;
 });
 
 add_task(async function testDeleteCookie() {
   let doc = cookiesDialog.document;
 
   // Add a cookie.
-  Services.cookies.add(URI.host, URI.path, "", "", false, false, true, Date.now());
+  Services.cookies.add(URI.host, URI.pathQueryRef, "", "", false, false, true, Date.now());
 
   let tree = doc.getElementById("cookiesList");
   Assert.equal(tree.view.rowCount, 1, "Row count should initially be 1");
   tree.focus();
   tree.view.selection.select(0);
 
   if (AppConstants.platform == "macosx") {
     EventUtils.synthesizeKey("VK_BACK_SPACE", {});
--- a/browser/components/resistfingerprinting/test/browser/file_navigator.html
+++ b/browser/components/resistfingerprinting/test/browser/file_navigator.html
@@ -3,30 +3,30 @@
 <title>Test page for navigator object</title>
 <meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
 <script>
   // This page will collect information from the navigator object and store
   // the result at a paragraph in the page.
   function collect() {
     let result = {};
 
-    result["appCodeName"] = navigator.appCodeName;
-    result["appName"] = navigator.appName;
-    result["appVersion"] = navigator.appVersion;
-    result["platform"] = navigator.platform;
-    result["userAgent"] = navigator.userAgent;
-    result["product"] = navigator.product;
-    result["productSub"] = navigator.productSub;
-    result["vendor"] = navigator.vendor;
-    result["vendorSub"] = navigator.vendorSub;
-    result["mimeTypesLength"] = navigator.mimeTypes.length;
-    result["pluginsLength"] = navigator.plugins.length;
-    result["oscpu"] = navigator.oscpu;
-    result["buildID"] = navigator.buildID;
-    result["hardwareConcurrency"] = navigator.hardwareConcurrency;
+    result.appCodeName = navigator.appCodeName;
+    result.appName = navigator.appName;
+    result.appVersion = navigator.appVersion;
+    result.platform = navigator.platform;
+    result.userAgent = navigator.userAgent;
+    result.product = navigator.product;
+    result.productSub = navigator.productSub;
+    result.vendor = navigator.vendor;
+    result.vendorSub = navigator.vendorSub;
+    result.mimeTypesLength = navigator.mimeTypes.length;
+    result.pluginsLength = navigator.plugins.length;
+    result.oscpu = navigator.oscpu;
+    result.buildID = navigator.buildID;
+    result.hardwareConcurrency = navigator.hardwareConcurrency;
 
     // eslint-disable-next-line no-unsanitized/property
     document.getElementById("result").innerHTML = JSON.stringify(result);
   }
 </script>
 </head>
 <body onload="collect();">
 <p id="result"></p>
--- a/browser/components/resistfingerprinting/test/browser/file_navigatorWorker.js
+++ b/browser/components/resistfingerprinting/test/browser/file_navigatorWorker.js
@@ -1,19 +1,19 @@
 /* eslint-env worker */
 
 onconnect = function(e) {
   let port = e.ports[0];
 
   let navigatorObj = self.navigator;
   let result = {};
 
-  result["appCodeName"] = navigatorObj.appCodeName;
-  result["appName"] = navigatorObj.appName;
-  result["appVersion"] = navigatorObj.appVersion;
-  result["platform"] = navigatorObj.platform;
-  result["userAgent"] = navigatorObj.userAgent;
-  result["product"] = navigatorObj.product;
-  result["hardwareConcurrency"] = navigatorObj.hardwareConcurrency;
+  result.appCodeName = navigatorObj.appCodeName;
+  result.appName = navigatorObj.appName;
+  result.appVersion = navigatorObj.appVersion;
+  result.platform = navigatorObj.platform;
+  result.userAgent = navigatorObj.userAgent;
+  result.product = navigatorObj.product;
+  result.hardwareConcurrency = navigatorObj.hardwareConcurrency;
 
   port.postMessage(JSON.stringify(result));
   port.start();
 };
--- a/browser/components/sessionstore/test/browser_339445_sample.html
+++ b/browser/components/sessionstore/test/browser_339445_sample.html
@@ -8,11 +8,11 @@ storageTestItem = <span id="storageTestI
   storageTestItem's textContent will be one of the following:
   * FAIL    : sessionStorage wasn't available
   * PENDING : the test value has been initialized on first load
   * SUCCESS : the test value was correctly retrieved
 -->
 
 <script type="application/javascript">
   document.getElementById("storageTestItem").textContent =
-    sessionStorage["storageTestItem"] || "PENDING";
-  sessionStorage["storageTestItem"] = "SUCCESS";
+    sessionStorage.storageTestItem || "PENDING";
+  sessionStorage.storageTestItem = "SUCCESS";
 </script>
--- a/browser/components/sessionstore/test/browser_467409-backslashplosion.js
+++ b/browser/components/sessionstore/test/browser_467409-backslashplosion.js
@@ -52,23 +52,23 @@ add_task(async function test_nested_abou
   gBrowser.removeTab(tab);
 });
 
 async function checkState(prefix, tab) {
   // Flush and query tab state.
   await TabStateFlusher.flush(tab.linkedBrowser);
   let {formdata} = JSON.parse(ss.getTabState(tab));
 
-  ok(formdata.id["sessionData"], prefix + ": we have form data for about:sessionrestore");
+  ok(formdata.id.sessionData, prefix + ": we have form data for about:sessionrestore");
 
-  let sessionData_raw = JSON.stringify(formdata.id["sessionData"]);
+  let sessionData_raw = JSON.stringify(formdata.id.sessionData);
   ok(!/\\/.test(sessionData_raw), prefix + ": #sessionData contains no backslashes");
   info(sessionData_raw);
 
   let gotError = false;
   try {
-    JSON.parse(formdata.id["sessionData"]);
+    JSON.parse(formdata.id.sessionData);
   } catch (e) {
     info(prefix + ": got error: " + e);
     gotError = true;
   }
   ok(gotError, prefix + ": attempting to JSON.parse form data threw error");
 }
--- a/browser/components/sessionstore/test/browser_485563.js
+++ b/browser/components/sessionstore/test/browser_485563.js
@@ -8,17 +8,17 @@ function test() {
   waitForExplicitFinish();
 
   let uniqueValue = Math.random() + "\u2028Second line\u2029Second paragraph\u2027";
 
   let tab = BrowserTestUtils.addTab(gBrowser);
   promiseBrowserLoaded(tab.linkedBrowser).then(() => {
     ss.setTabValue(tab, "bug485563", uniqueValue);
     let tabState = JSON.parse(ss.getTabState(tab));
-    is(tabState.extData["bug485563"], uniqueValue,
+    is(tabState.extData.bug485563, uniqueValue,
        "unicode line separator wasn't over-encoded");
     ss.deleteTabValue(tab, "bug485563");
     ss.setTabState(tab, JSON.stringify(tabState));
     is(ss.getTabValue(tab, "bug485563"), uniqueValue,
        "unicode line separator was correctly preserved");
 
     gBrowser.removeTab(tab);
     finish();
--- a/browser/components/sessionstore/test/browser_590268.js
+++ b/browser/components/sessionstore/test/browser_590268.js
@@ -62,17 +62,17 @@ function test() {
 
   // This does the actual testing. SSTabRestoring should be firing on tabs from
   // left to right, so we're going to start with the rightmost tab.
   function onFirstSSTabRestoring() {
     info("onFirstSSTabRestoring...");
     for (let i = gBrowser.tabs.length - 1; i >= 0; i--) {
       let tab = gBrowser.tabs[i];
       let actualUniq = ss.getTabValue(tab, "uniq");
-      let expectedUniq = state.windows[0].tabs[i].extData["uniq"];
+      let expectedUniq = state.windows[0].tabs[i].extData.uniq;
 
       if (wasLoaded[actualUniq]) {
         info("tab " + i + ": already restored");
         continue;
       }
       is(actualUniq, expectedUniq, "tab " + i + ": extData was correct");
 
       // Now we're going to set a piece of data back on the tab so it can be read
--- a/browser/components/sessionstore/test/browser_607016.js
+++ b/browser/components/sessionstore/test/browser_607016.js
@@ -22,17 +22,17 @@ add_task(async function() {
   ], selected: 1 }] };
 
   async function progressCallback() {
     let curState = JSON.parse(ss.getBrowserState());
     for (let i = 0; i < curState.windows[0].tabs.length; i++) {
       let tabState = state.windows[0].tabs[i];
       let tabCurState = curState.windows[0].tabs[i];
       if (tabState.extData) {
-        is(tabCurState.extData["uniq"], tabState.extData["uniq"],
+        is(tabCurState.extData.uniq, tabState.extData.uniq,
            "sanity check that tab has correct extData");
       } else {
         // We aren't expecting there to be any data on extData, but panorama
         // may be setting something, so we need to make sure that if we do have
         // data, we just don't have anything for "uniq".
         ok(!("extData" in tabCurState) || !("uniq" in tabCurState.extData),
            "sanity check that tab doesn't have extData or extData doesn't have 'uniq'");
       }
--- a/browser/components/sessionstore/test/browser_739805.js
+++ b/browser/components/sessionstore/test/browser_739805.js
@@ -21,17 +21,17 @@ function test() {
 
   promiseBrowserLoaded(browser).then(() => {
     isnot(gBrowser.selectedTab, tab, "newly created tab is not selected");
 
     ss.setTabState(tab, JSON.stringify(tabState));
     is(browser.__SS_restoreState, TAB_STATE_NEEDS_RESTORE, "tab needs restoring");
 
     let {formdata} = JSON.parse(ss.getTabState(tab));
-    is(formdata && formdata.id["foo"], "bar", "tab state's formdata is valid");
+    is(formdata && formdata.id.foo, "bar", "tab state's formdata is valid");
 
     promiseTabRestored(tab).then(() => {
       ContentTask.spawn(browser, null, function() {
         let input = content.document.getElementById("foo");
         is(input.value, "bar", "formdata has been restored correctly");
       }).then(() => { finish(); });
     });
 
--- a/browser/components/sessionstore/test/browser_sessionStorage_size.js
+++ b/browser/components/sessionstore/test/browser_sessionStorage_size.js
@@ -8,19 +8,19 @@ const URL = "http://mochi.test:8888/brow
             "browser/components/sessionstore/test/browser_sessionStorage.html" +
             "?" + RAND;
 
 const OUTER_VALUE = "outer-value-" + RAND;
 
 function getEstimateChars() {
   let snap;
   if (gMultiProcessBrowser) {
-    snap = Services.telemetry.histogramSnapshots.content["FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS"];
+    snap = Services.telemetry.histogramSnapshots.content.FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS;
   } else {
-    snap = Services.telemetry.histogramSnapshots.parent["FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS"];
+    snap = Services.telemetry.histogramSnapshots.parent.FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS;
   }
   if (!snap) {
     return 0;
   }
   return snap.counts[4];
 }
 
 // Test that we record the size of messages.
--- a/browser/components/shell/content/setDesktopBackground.js
+++ b/browser/components/shell/content/setDesktopBackground.js
@@ -149,55 +149,55 @@ var gSetBackground = {
         ctx.drawImage(this._image, x, y, width, height);
         break;
       }
     }
   }
 };
 
 if (AppConstants.platform != "macosx") {
-  gSetBackground["_initColor"] = function() {
+  gSetBackground._initColor = function() {
     var color = this._shell.desktopBackgroundColor;
 
     const rMask = 4294901760;
     const gMask = 65280;
     const bMask = 255;
     var r = (color & rMask) >> 16;
     var g = (color & gMask) >> 8;
     var b = (color & bMask);
     this.updateColor(this._rgbToHex(r, g, b));
 
     var colorpicker = document.getElementById("desktopColor");
     colorpicker.color = this._backgroundColor;
   };
 
-  gSetBackground["updateColor"] = function(aColor) {
+  gSetBackground.updateColor = function(aColor) {
     this._backgroundColor = aColor;
     this._canvas.style.backgroundColor = aColor;
   };
 
   // Converts a color string in the format "#RRGGBB" to an integer.
-  gSetBackground["_hexStringToLong"] = function(aString) {
+  gSetBackground._hexStringToLong = function(aString) {
     return parseInt(aString.substring(1, 3), 16) << 16 |
            parseInt(aString.substring(3, 5), 16) << 8 |
            parseInt(aString.substring(5, 7), 16);
   };
 
-  gSetBackground["_rgbToHex"] = function(aR, aG, aB) {
+  gSetBackground._rgbToHex = function(aR, aG, aB) {
     return "#" + [aR, aG, aB].map(aInt => aInt.toString(16).replace(/^(.)$/, "0$1"))
                              .join("").toUpperCase();
   };
 } else {
-  gSetBackground["observe"] = function(aSubject, aTopic, aData) {
+  gSetBackground.observe = function(aSubject, aTopic, aData) {
     if (aTopic == "shell:desktop-background-changed") {
       document.getElementById("setDesktopBackground").hidden = true;
       document.getElementById("showDesktopPreferences").hidden = false;
 
       Components.classes["@mozilla.org/observer-service;1"]
                 .getService(Ci.nsIObserverService)
                 .removeObserver(this, "shell:desktop-background-changed");
     }
   };
 
-  gSetBackground["showDesktopPrefs"] = function() {
+  gSetBackground.showDesktopPrefs = function() {
     this._shell.openApplication(Ci.nsIMacShellService.APPLICATION_DESKTOP);
   };
 }
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -1578,55 +1578,55 @@ this.UITour = {
       let appinfo = {};
       props.forEach(property => appinfo[property] = Services.appinfo[property]);
 
       // Identifier of the partner repack, as stored in preference "distribution.id"
       // and included in Firefox and other update pings. Note this is not the same as
       // Services.appinfo.distributionID (value of MOZ_DISTRIBUTION_ID is set at build time).
       let distribution =
           Services.prefs.getDefaultBranch("distribution.").getCharPref("id", "default");
-      appinfo["distribution"] = distribution;
+      appinfo.distribution = distribution;
 
       let isDefaultBrowser = null;
       try {
         let shell = aWindow.getShellService();
         if (shell) {
           isDefaultBrowser = shell.isDefaultBrowser(false);
         }
       } catch (e) {}
-      appinfo["defaultBrowser"] = isDefaultBrowser;
+      appinfo.defaultBrowser = isDefaultBrowser;
 
       let canSetDefaultBrowserInBackground = true;
       if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2") ||
           AppConstants.isPlatformAndVersionAtLeast("macosx", "10.10")) {
         canSetDefaultBrowserInBackground = false;
       } else if (AppConstants.platform == "linux") {
         // The ShellService may not exist on some versions of Linux.
         try {
           aWindow.getShellService();
         } catch (e) {
           canSetDefaultBrowserInBackground = null;
         }
       }
 
-      appinfo["canSetDefaultBrowserInBackground"] =
+      appinfo.canSetDefaultBrowserInBackground =
         canSetDefaultBrowserInBackground;
 
       // Expose Profile creation and last reset dates in weeks.
       const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
       let profileAge = new ProfileAge(null, null);
       let createdDate = await profileAge.created;
       let resetDate = await profileAge.reset;
       let createdWeeksAgo = Math.floor((Date.now() - createdDate) / ONE_WEEK);
       let resetWeeksAgo = null;
       if (resetDate) {
         resetWeeksAgo = Math.floor((Date.now() - resetDate) / ONE_WEEK);
       }
-      appinfo["profileCreatedWeeksAgo"] = createdWeeksAgo;
-      appinfo["profileResetWeeksAgo"] = resetWeeksAgo;
+      appinfo.profileCreatedWeeksAgo = createdWeeksAgo;
+      appinfo.profileResetWeeksAgo = resetWeeksAgo;
 
       this.sendPageCallback(aMessageManager, aCallbackID, appinfo);
     })().catch(err => {
       log.error(err);
       this.sendPageCallback(aMessageManager, aCallbackID, {});
     })
   },
 
--- a/browser/experiments/test/xpcshell/test_cache.js
+++ b/browser/experiments/test/xpcshell/test_cache.js
@@ -383,17 +383,17 @@ add_task(async function test_expiration(
   // The first experiment should be expired and not in the cache, it ended more than
   // 180 days ago. We should see the one still running in the cache.
   history = await experiments.getExperiments();
   Assert.equal(history.length, 1, "Expired experiments must not be saved to cache.");
   checkExperimentListsEqual(experimentListData.slice(0, 1), history);
 
   // Test that experiments that are cached locally but never ran are removed from cache
   // when they are removed from the manifest (this is cached data, not really history).
-  gManifestObject["experiments"] = gManifestObject["experiments"].slice(1, 1);
+  gManifestObject.experiments = gManifestObject.experiments.slice(1, 1);
   await experiments.updateManifest();
   validateCache([...experiments._experiments.keys()], [EXPERIMENT2_ID]);
 
   // Cleanup.
   await experiments._toggleExperimentsEnabled(false);
   await promiseRestartManager();
   await removeCacheFile();
 });
--- a/browser/extensions/activity-stream/bootstrap.js
+++ b/browser/extensions/activity-stream/bootstrap.js
@@ -70,18 +70,20 @@ function init(reason) {
 /**
  * uninit - Uninitializes the activityStream instance, if it exsits.This could be
  *          called by the shutdown() function exposed by bootstrap.js, or it could
  *          be called when ACTIVITY_STREAM_ENABLED_PREF is changed from true to false.
  *
  * @param  {type} reason Reason for uninitialization. Could be uninstall, upgrade, or PREF_OFF
  */
 function uninit(reason) {
+  // Make sure to only uninit once in case both pref change and shutdown happen
   if (activityStream) {
     activityStream.uninit(reason);
+    activityStream = null;
   }
 }
 
 /**
  * onPrefChanged - handler for changes to ACTIVITY_STREAM_ENABLED_PREF
  *
  */
 function onPrefChanged() {
@@ -94,17 +96,17 @@ function onPrefChanged() {
 
 /**
  * onBrowserReady - Continues startup of the add-on after browser is ready.
  */
 function onBrowserReady() {
   waitingForBrowserReady = false;
 
   // Listen for changes to the pref that enables Activity Stream
-  Services.prefs.addObserver(ACTIVITY_STREAM_ENABLED_PREF, observe);
+  Services.prefs.addObserver(ACTIVITY_STREAM_ENABLED_PREF, observe); // eslint-disable-line no-use-before-define
 
   // Only initialize if the pref is true
   if (Services.prefs.getBoolPref(ACTIVITY_STREAM_ENABLED_PREF, false)) {
     init(startupReason);
   }
 }
 
 /**
@@ -113,17 +115,17 @@ function onBrowserReady() {
 function observe(subject, topic, data) {
   switch (topic) {
     case BROWSER_READY_NOTIFICATION:
       Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION);
       // Avoid running synchronously during this event that's used for timing
       Services.tm.dispatchToMainThread(() => onBrowserReady());
       break;
     case PREF_CHANGED_TOPIC:
-      if (data == ACTIVITY_STREAM_ENABLED_PREF) {
+      if (data === ACTIVITY_STREAM_ENABLED_PREF) {
         onPrefChanged();
       }
       break;
   }
 }
 
 // The functions below are required by bootstrap.js
 
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/common/Dedupe.jsm
@@ -0,0 +1,40 @@
+this.Dedupe = class Dedupe {
+  constructor(createKey, compare) {
+    this.createKey = createKey || this.defaultCreateKey;
+    this.compare = compare || this.defaultCompare;
+  }
+
+  defaultCreateKey(item) {
+    return item;
+  }
+
+  defaultCompare() {
+    return false;
+  }
+
+  /**
+   * Dedupe an array containing groups of elements.
+   * Duplicate removal favors earlier groups.
+   *
+   * @param {Array} groups Contains an arbitrary number of arrays of elements.
+   * @returns {Array}
+   */
+  group(groups) {
+    const globalKeys = new Set();
+    const result = [];
+    for (const values of groups) {
+      const valueMap = new Map();
+      for (const value of values) {
+        const key = this.createKey(value);
+        if (!globalKeys.has(key) && (!valueMap.has(key) || this.compare(valueMap.get(key), value))) {
+          valueMap.set(key, value);
+        }
+      }
+      result.push(valueMap);
+      valueMap.forEach((value, key) => globalKeys.add(key));
+    }
+    return result.map(m => Array.from(m.values()));
+  }
+};
+
+this.EXPORTED_SYMBOLS = ["Dedupe"];
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -32,17 +32,17 @@ const INITIAL_STATE = {
     data: {}
   },
   Sections: []
 };
 
 function App(prevState = INITIAL_STATE.App, action) {
   switch (action.type) {
     case at.INIT:
-      return Object.assign({}, action.data || {}, {initialized: true});
+      return Object.assign({}, prevState, action.data || {}, {initialized: true});
     case at.LOCALE_UPDATED: {
       if (!action.data) {
         return prevState;
       }
       let {locale, strings} = action.data;
       return Object.assign({}, prevState, {
         locale,
         strings
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/common/ShortURL.jsm
@@ -0,0 +1,32 @@
+Components.utils.importGlobalProperties(["URL"]);
+
+/**
+ * shortURL - Creates a short version of a link's url, used for display purposes
+ *            e.g. {url: http://www.foosite.com, eTLD: "com"}  =>  "foosite"
+ *
+ * @param  {obj} link A link object
+ *         {str} link.url (required)- The url of the link
+ *         {str} link.eTLD (required) - The tld of the link
+ *               e.g. for https://foo.org, the tld would be "org"
+ *               Note that this property is added in various queries for ActivityStream
+ *               via Services.eTLD.getPublicSuffix
+ *         {str} link.hostname (optional) - The hostname of the url
+ *               e.g. for http://www.hello.com/foo/bar, the hostname would be "www.hello.com"
+ *         {str} link.title (optional) - The title of the link
+ * @return {str}   A short url
+ */
+this.shortURL = function shortURL(link) {
+  if (!link.url && !link.hostname) {
+    return "";
+  }
+  const {eTLD} = link;
+  const hostname = (link.hostname || new URL(link.url).hostname).replace(/^www\./i, "");
+
+  // Remove the eTLD (e.g., com, net) and the preceding period from the hostname
+  const eTLDLength = (eTLD || "").length || (hostname.match(/\.com$/) && 3);
+  const eTLDExtra = eTLDLength > 0 ? -(eTLDLength + 1) : Infinity;
+  // If URL and hostname are not present fallback to page title.
+  return hostname.slice(0, eTLDExtra).toLowerCase() || hostname || link.title || link.url;
+};
+
+this.EXPORTED_SYMBOLS = ["shortURL"];
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -58,17 +58,17 @@
 /******/
 /******/ 	// Object.prototype.hasOwnProperty.call
 /******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
 /******/
 /******/ 	// __webpack_public_path__
 /******/ 	__webpack_require__.p = "";
 /******/
 /******/ 	// Load entry module and return exports
-/******/ 	return __webpack_require__(__webpack_require__.s = 26);
+/******/ 	return __webpack_require__(__webpack_require__.s = 25);
 /******/ })
 /************************************************************************/
 /******/ ([
 /* 0 */
 /***/ (function(module, exports) {
 
 module.exports = React;
 
@@ -295,66 +295,29 @@ module.exports = ReactRedux;
 
 /***/ }),
 /* 4 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-/**
- * shortURL - Creates a short version of a link's url, used for display purposes
- *            e.g. {url: http://www.foosite.com, eTLD: "com"}  =>  "foosite"
- *
- * @param  {obj} link A link object
- *         {str} link.url (required)- The url of the link
- *         {str} link.eTLD (required) - The tld of the link
- *               e.g. for https://foo.org, the tld would be "org"
- *               Note that this property is added in various queries for ActivityStream
- *               via Services.eTLD.getPublicSuffix
- *         {str} link.hostname (optional) - The hostname of the url
- *               e.g. for http://www.hello.com/foo/bar, the hostname would be "www.hello.com"
- *         {str} link.title (optional) - The title of the link
- * @return {str}   A short url
- */
-module.exports = function shortURL(link) {
-  if (!link.url && !link.hostname) {
-    return "";
-  }
-  const eTLD = link.eTLD;
-
-  const hostname = (link.hostname || new URL(link.url).hostname).replace(/^www\./i, "");
-
-  // Remove the eTLD (e.g., com, net) and the preceding period from the hostname
-  const eTLDLength = (eTLD || "").length || hostname.match(/\.com$/) && 3;
-  const eTLDExtra = eTLDLength > 0 ? -(eTLDLength + 1) : Infinity;
-  // If URL and hostname are not present fallback to page title.
-  return hostname.slice(0, eTLDExtra).toLowerCase() || hostname || link.title || link.url;
-};
-
-/***/ }),
-/* 5 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(2);
 
 const injectIntl = _require.injectIntl;
 
-const ContextMenu = __webpack_require__(16);
+const ContextMenu = __webpack_require__(15);
 
 var _require2 = __webpack_require__(1);
 
 const ac = _require2.actionCreators;
 
-const linkMenuOptions = __webpack_require__(23);
+const linkMenuOptions = __webpack_require__(22);
 const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow"];
 
 class LinkMenu extends React.Component {
   getOptions() {
     const props = this.props;
     const site = props.site,
           index = props.index,
           source = props.source;
@@ -399,17 +362,17 @@ class LinkMenu extends React.Component {
       options: this.getOptions() });
   }
 }
 
 module.exports = injectIntl(LinkMenu);
 module.exports._unconnected = LinkMenu;
 
 /***/ }),
-/* 6 */
+/* 5 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* globals Services */
 
 
 let usablePerfObj;
 
@@ -522,39 +485,39 @@ var _PerfService = function _PerfService
 
 var perfService = new _PerfService();
 module.exports = {
   _PerfService,
   perfService
 };
 
 /***/ }),
-/* 7 */
+/* 6 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(3);
 
 const connect = _require.connect;
 
 var _require2 = __webpack_require__(2);
 
 const addLocaleData = _require2.addLocaleData,
       IntlProvider = _require2.IntlProvider;
 
-const TopSites = __webpack_require__(21);
-const Search = __webpack_require__(19);
-const ConfirmDialog = __webpack_require__(15);
-const ManualMigration = __webpack_require__(17);
-const PreferencesPane = __webpack_require__(18);
-const Sections = __webpack_require__(20);
+const TopSites = __webpack_require__(20);
+const Search = __webpack_require__(18);
+const ConfirmDialog = __webpack_require__(14);
+const ManualMigration = __webpack_require__(16);
+const PreferencesPane = __webpack_require__(17);
+const Sections = __webpack_require__(19);
 
 // Locales that should be displayed RTL
 const RTL_LIST = ["ar", "he", "fa", "ur"];
 
 // Add the locale data for pluralization and relative-time formatting for now,
 // this just uses english locale data. We can make this more sophisticated if
 // more features are needed.
 function addLocaleDataForReactIntl(_ref) {
@@ -568,17 +531,18 @@ function addLocaleDataForReactIntl(_ref)
 class Base extends React.Component {
   componentDidMount() {
     // Also wait for the preloaded page to show, so the tab's title updates
     addEventListener("visibilitychange", () => this.updateTitle(this.props.App), { once: true });
   }
   componentWillUpdate(_ref2) {
     let App = _ref2.App;
 
-    if (App.locale !== this.props.App.locale) {
+    // Early loads might not have locale yet, so wait until we do
+    if (App.locale && App.locale !== this.props.App.locale) {
       addLocaleDataForReactIntl(App);
       this.updateTitle(App);
     }
   }
 
   updateTitle(_ref3) {
     let strings = _ref3.strings;
 
@@ -616,27 +580,27 @@ class Base extends React.Component {
       )
     );
   }
 }
 
 module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
 
 /***/ }),
-/* 8 */
+/* 7 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _require = __webpack_require__(1);
 
 const at = _require.actionTypes;
 
-var _require2 = __webpack_require__(6);
+var _require2 = __webpack_require__(5);
 
 const perfSvc = _require2.perfService;
 
 
 const VISIBLE = "visible";
 const VISIBILITY_CHANGE_EVENT = "visibilitychange";
 
 module.exports = class DetectUserSessionStart {
@@ -696,25 +660,25 @@ module.exports = class DetectUserSession
     if (this.document.visibilityState === VISIBLE) {
       this._sendEvent();
       this.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
     }
   }
 };
 
 /***/ }),
-/* 9 */
+/* 8 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* eslint-env mozilla/frame-script */
 
-var _require = __webpack_require__(25);
+var _require = __webpack_require__(24);
 
 const createStore = _require.createStore,
       combineReducers = _require.combineReducers,
       applyMiddleware = _require.applyMiddleware;
 
 var _require2 = __webpack_require__(1);
 
 const au = _require2.actionUtils;
@@ -781,17 +745,17 @@ module.exports = function initStore(redu
   return store;
 };
 
 module.exports.MERGE_STORE_ACTION = MERGE_STORE_ACTION;
 module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME;
 module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
 
 /***/ }),
-/* 10 */
+/* 9 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* WEBPACK VAR INJECTION */(function(global) {
 
 const DATABASE_NAME = "snippets_db";
 const DATABASE_VERSION = 1;
 const SNIPPETS_OBJECTSTORE_NAME = "snippets";
@@ -1092,20 +1056,20 @@ function addSnippetsSubscriber(store) {
 }
 
 module.exports = {
   addSnippetsSubscriber,
   SnippetsMap,
   SnippetsProvider,
   SNIPPETS_UPDATE_INTERVAL_MS
 };
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(24)))
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(23)))
 
 /***/ }),
-/* 11 */
+/* 10 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* 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/. */
 
 
@@ -1144,17 +1108,17 @@ const INITIAL_STATE = {
 };
 
 function App() {
   let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.App;
   let action = arguments[1];
 
   switch (action.type) {
     case at.INIT:
-      return Object.assign({}, action.data || {}, { initialized: true });
+      return Object.assign({}, prevState, action.data || {}, { initialized: true });
     case at.LOCALE_UPDATED:
       {
         if (!action.data) {
           return prevState;
         }
         var _action$data = action.data;
         let locale = _action$data.locale,
             strings = _action$data.strings;
@@ -1396,37 +1360,36 @@ function Snippets() {
 var reducers = { TopSites, App, Snippets, Prefs, Dialog, Sections };
 module.exports = {
   reducers,
   INITIAL_STATE,
   insertPinned
 };
 
 /***/ }),
-/* 12 */
+/* 11 */
 /***/ (function(module, exports) {
 
 module.exports = ReactDOM;
 
 /***/ }),
-/* 13 */
+/* 12 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const React = __webpack_require__(0);
-const LinkMenu = __webpack_require__(5);
-const shortURL = __webpack_require__(4);
+const LinkMenu = __webpack_require__(4);
 
 var _require = __webpack_require__(2);
 
 const FormattedMessage = _require.FormattedMessage;
 
-const cardContextTypes = __webpack_require__(14);
+const cardContextTypes = __webpack_require__(13);
 
 var _require2 = __webpack_require__(1);
 
 const ac = _require2.actionCreators,
       at = _require2.actionTypes;
 
 /**
  * Card component.
@@ -1450,17 +1413,26 @@ class Card extends React.Component {
     event.preventDefault();
     this.setState({
       activeCard: this.props.index,
       showContextMenu: true
     });
   }
   onLinkClick(event) {
     event.preventDefault();
-    this.props.dispatch(ac.SendToMain({ type: at.OPEN_LINK, data: this.props.link }));
+    const altKey = event.altKey,
+          button = event.button,
+          ctrlKey = event.ctrlKey,
+          metaKey = event.metaKey,
+          shiftKey = event.shiftKey;
+
+    this.props.dispatch(ac.SendToMain({
+      type: at.OPEN_LINK,
+      data: Object.assign(this.props.link, { event: { altKey, button, ctrlKey, metaKey, shiftKey } })
+    }));
     this.props.dispatch(ac.UserEvent({
       event: "CLICK",
       source: this.props.eventSource,
       action_position: this.props.index
     }));
   }
   onMenuUpdate(showContextMenu) {
     this.setState({ showContextMenu });
@@ -1469,17 +1441,16 @@ class Card extends React.Component {
     var _props = this.props;
     const index = _props.index,
           link = _props.link,
           dispatch = _props.dispatch,
           contextMenuOptions = _props.contextMenuOptions,
           eventSource = _props.eventSource;
 
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
-    const hostname = shortURL(link);
     var _cardContextTypes$lin = cardContextTypes[link.type];
     const icon = _cardContextTypes$lin.icon,
           intlID = _cardContextTypes$lin.intlID;
 
 
     return React.createElement(
       "li",
       { className: `card-outer${isContextMenuOpen ? " active" : ""}` },
@@ -1492,17 +1463,17 @@ class Card extends React.Component {
           link.image && React.createElement("div", { className: "card-preview-image", style: { backgroundImage: `url(${link.image})` } }),
           React.createElement(
             "div",
             { className: "card-details" },
             React.createElement(
               "div",
               { className: "card-host-name" },
               " ",
-              hostname,
+              link.hostname,
               " "
             ),
             React.createElement(
               "div",
               { className: `card-text${link.image ? "" : " full-height"}` },
               React.createElement(
                 "h4",
                 { className: "card-title", dir: "auto" },
@@ -1550,17 +1521,17 @@ class Card extends React.Component {
         site: link,
         visible: isContextMenuOpen })
     );
   }
 }
 module.exports = Card;
 
 /***/ }),
-/* 14 */
+/* 13 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 module.exports = {
   history: {
     intlID: "type_label_visited",
@@ -1576,17 +1547,17 @@ module.exports = {
   },
   now: {
     intlID: "type_label_now",
     icon: "now"
   }
 };
 
 /***/ }),
-/* 15 */
+/* 14 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(3);
@@ -1695,17 +1666,17 @@ const ConfirmDialog = React.createClass(
   }
 });
 
 module.exports = connect(state => state.Dialog)(ConfirmDialog);
 module.exports._unconnected = ConfirmDialog;
 module.exports.Dialog = ConfirmDialog;
 
 /***/ }),
-/* 16 */
+/* 15 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const React = __webpack_require__(0);
 
 class ContextMenu extends React.Component {
@@ -1789,17 +1760,17 @@ class ContextMenuItem extends React.Comp
   }
 }
 
 module.exports = ContextMenu;
 module.exports.ContextMenu = ContextMenu;
 module.exports.ContextMenuItem = ContextMenuItem;
 
 /***/ }),
-/* 17 */
+/* 16 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(3);
@@ -1867,17 +1838,17 @@ class ManualMigration extends React.Comp
     );
   }
 }
 
 module.exports = connect()(ManualMigration);
 module.exports._unconnected = ManualMigration;
 
 /***/ }),
-/* 18 */
+/* 17 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(3);
@@ -1914,20 +1885,23 @@ class PreferencesPane extends React.Comp
   constructor(props) {
     super(props);
     this.state = { visible: false };
     this.handleClickOutside = this.handleClickOutside.bind(this);
     this.handleChange = this.handleChange.bind(this);
     this.togglePane = this.togglePane.bind(this);
 
     // TODO This is temporary until sections register their PreferenceInput component automatically
-    try {
-      this.topStoriesOptions = JSON.parse(props.Prefs.values["feeds.section.topstories.options"]);
-    } catch (e) {
-      console.error("Problem parsing feeds.section.topstories.options", e); // eslint-disable-line no-console
+    const optionJSON = props.Prefs.values["feeds.section.topstories.options"];
+    if (optionJSON) {
+      try {
+        this.topStoriesOptions = JSON.parse(optionJSON);
+      } catch (e) {
+        console.error("Problem parsing feeds.section.topstories.options", e); // eslint-disable-line no-console
+      }
     }
   }
   componentDidMount() {
     document.addEventListener("click", this.handleClickOutside);
   }
   componentWillUnmount() {
     document.removeEventListener("click", this.handleClickOutside);
   }
@@ -2004,17 +1978,17 @@ class PreferencesPane extends React.Comp
   }
 }
 
 module.exports = connect(state => ({ Prefs: state.Prefs }))(injectIntl(PreferencesPane));
 module.exports.PreferencesPane = PreferencesPane;
 module.exports.PreferencesInput = PreferencesInput;
 
 /***/ }),
-/* 19 */
+/* 18 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* globals ContentSearchUIController */
 
 
 const React = __webpack_require__(0);
 
@@ -2103,82 +2077,114 @@ class Search extends React.Component {
     );
   }
 }
 
 module.exports = connect()(injectIntl(Search));
 module.exports._unconnected = Search;
 
 /***/ }),
-/* 20 */
+/* 19 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(3);
 
 const connect = _require.connect;
 
 var _require2 = __webpack_require__(2);
 
-const FormattedMessage = _require2.FormattedMessage;
-
-const Card = __webpack_require__(13);
-const Topics = __webpack_require__(22);
+const injectIntl = _require2.injectIntl,
+      FormattedMessage = _require2.FormattedMessage;
+
+const Card = __webpack_require__(12);
+const Topics = __webpack_require__(21);
 
 class Section extends React.Component {
+  constructor(props) {
+    super(props);
+    this.onInfoEnter = this.onInfoEnter.bind(this);
+    this.onInfoLeave = this.onInfoLeave.bind(this);
+    this.state = { infoActive: false };
+  }
+
+  onInfoEnter() {
+    this.setState({ infoActive: true });
+  }
+
+  onInfoLeave(event) {
+    // If we have a related target, check to see if it is within the current
+    // target (section-info-option) to keep infoActive true. False otherwise.
+    this.setState({
+      infoActive: event && event.relatedTarget && event.relatedTarget.compareDocumentPosition(event.currentTarget) & Node.DOCUMENT_POSITION_CONTAINS
+    });
+  }
+
   render() {
     var _props = this.props;
     const id = _props.id,
           eventSource = _props.eventSource,
           title = _props.title,
           icon = _props.icon,
           rows = _props.rows,
           infoOption = _props.infoOption,
           emptyState = _props.emptyState,
           dispatch = _props.dispatch,
           maxCards = _props.maxCards,
-          contextMenuOptions = _props.contextMenuOptions;
+          contextMenuOptions = _props.contextMenuOptions,
+          intl = _props.intl;
 
     const initialized = rows && rows.length > 0;
     const shouldShowTopics = id === "TopStories" && this.props.topics && this.props.read_more_endpoint;
+
+    const infoOptionIconA11yAttrs = {
+      "aria-haspopup": "true",
+      "aria-controls": "info-option",
+      "aria-expanded": this.state.infoActive ? "true" : "false",
+      "role": "note",
+      "tabIndex": 0
+    };
+
+    const sectionInfoTitle = intl.formatMessage({ id: "section_info_option" });
+
     // <Section> <-- React component
     // <section> <-- HTML5 element
     return React.createElement(
       "section",
       null,
       React.createElement(
         "div",
         { className: "section-top-bar" },
         React.createElement(
           "h3",
           { className: "section-title" },
           React.createElement("span", { className: `icon icon-small-spacer icon-${icon}` }),
           React.createElement(FormattedMessage, title)
         ),
         infoOption && React.createElement(
           "span",
-          { className: "section-info-option" },
-          React.createElement(
-            "span",
-            { className: "sr-only" },
-            React.createElement(FormattedMessage, { id: "section_info_option" })
-          ),
-          React.createElement("img", { className: "info-option-icon" }),
+          { className: "section-info-option",
+            onBlur: this.onInfoLeave,
+            onFocus: this.onInfoEnter,
+            onMouseOut: this.onInfoLeave,
+            onMouseOver: this.onInfoEnter },
+          React.createElement("img", _extends({ className: "info-option-icon", title: sectionInfoTitle
+          }, infoOptionIconA11yAttrs)),
           React.createElement(
             "div",
             { className: "info-option" },
             infoOption.header && React.createElement(
               "div",
-              { className: "info-option-header" },
+              { className: "info-option-header", role: "heading" },
               React.createElement(FormattedMessage, infoOption.header)
             ),
             infoOption.body && React.createElement(
               "p",
               { className: "info-option-body" },
               React.createElement(FormattedMessage, infoOption.body)
             ),
             infoOption.link && React.createElement(
@@ -2208,57 +2214,59 @@ class Section extends React.Component {
           )
         )
       ),
       shouldShowTopics && React.createElement(Topics, { topics: this.props.topics, read_more_endpoint: this.props.read_more_endpoint })
     );
   }
 }
 
+const SectionIntl = injectIntl(Section);
+
 class Sections extends React.Component {
   render() {
     const sections = this.props.Sections;
     return React.createElement(
       "div",
       { className: "sections-list" },
-      sections.map(section => React.createElement(Section, _extends({ key: section.id }, section, { dispatch: this.props.dispatch })))
+      sections.map(section => React.createElement(SectionIntl, _extends({ key: section.id }, section, { dispatch: this.props.dispatch })))
     );
   }
 }
 
 module.exports = connect(state => ({ Sections: state.Sections }))(Sections);
 module.exports._unconnected = Sections;
-module.exports.Section = Section;
+module.exports.SectionIntl = SectionIntl;
+module.exports._unconnectedSection = Section;
 
 /***/ }),
-/* 21 */
+/* 20 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(3);
 
 const connect = _require.connect;
 
 var _require2 = __webpack_require__(2);
 
 const FormattedMessage = _require2.FormattedMessage;
 
-const shortURL = __webpack_require__(4);
-const LinkMenu = __webpack_require__(5);
+const LinkMenu = __webpack_require__(4);
 
 var _require3 = __webpack_require__(1);
 
 const ac = _require3.actionCreators,
       at = _require3.actionTypes;
 
-var _require4 = __webpack_require__(6);
+var _require4 = __webpack_require__(5);
 
 const perfSvc = _require4.perfService;
 
 const TOP_SITES_SOURCE = "TOP_SITES";
 const TOP_SITES_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"];
 
 class TopSite extends React.Component {
   constructor(props) {
@@ -2290,35 +2298,47 @@ class TopSite extends React.Component {
   }
   render() {
     var _props = this.props;
     const link = _props.link,
           index = _props.index,
           dispatch = _props.dispatch;
 
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index;
-    const title = link.pinTitle || shortURL(link);
-    const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`;
+    const title = link.pinTitle || link.hostname;
     const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`;
-    const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
+    const tippyTopIcon = link.tippyTopIcon;
+
+    let imageClassName;
+    let imageStyle;
+    if (tippyTopIcon) {
+      imageClassName = "tippy-top-icon";
+      imageStyle = {
+        backgroundColor: link.backgroundColor,
+        backgroundImage: `url(${tippyTopIcon})`
+      };
+    } else {
+      imageClassName = `screenshot${link.screenshot ? " active" : ""}`;
+      imageStyle = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
+    }
     return React.createElement(
       "li",
       { className: topSiteOuterClassName, key: link.guid || link.url },
       React.createElement(
         "a",
         { href: link.url, onClick: this.onLinkClick },
         React.createElement(
           "div",
           { className: "tile", "aria-hidden": true },
           React.createElement(
             "span",
             { className: "letter-fallback" },
             title[0]
           ),
-          React.createElement("div", { className: screenshotClassName, style: style })
+          React.createElement("div", { className: imageClassName, style: imageStyle })
         ),
         React.createElement(
           "div",
           { className: `title ${link.isPinned ? "pinned" : ""}` },
           link.isPinned && React.createElement("div", { className: "icon icon-pin-small" }),
           React.createElement(
             "span",
             { dir: "auto" },
@@ -2459,17 +2479,17 @@ const TopSites = props => React.createEl
 );
 
 module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSitesPerfTimer);
 module.exports._unconnected = TopSitesPerfTimer;
 module.exports.TopSite = TopSite;
 module.exports.TopSites = TopSites;
 
 /***/ }),
-/* 22 */
+/* 21 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(2);
@@ -2524,34 +2544,33 @@ class Topics extends React.Component {
   }
 }
 
 module.exports = Topics;
 module.exports._unconnected = Topics;
 module.exports.Topic = Topic;
 
 /***/ }),
-/* 23 */
+/* 22 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 var _require = __webpack_require__(1);
 
 const at = _require.actionTypes,
       ac = _require.actionCreators;
 
-const shortURL = __webpack_require__(4);
-
 /**
  * List of functions that return items that can be included as menu options in a
  * LinkMenu. All functions take the site as the first parameter, and optionally
  * the index of the site.
  */
+
 module.exports = {
   Separator: () => ({ type: "separator" }),
   RemoveBookmark: site => ({
     id: "menu_action_remove_bookmark",
     icon: "bookmark-remove",
     action: ac.SendToMain({
       type: at.DELETE_BOOKMARK_BY_ID,
       data: site.bookmarkGuid
@@ -2607,17 +2626,17 @@ module.exports = {
     },
     userEvent: "DIALOG_OPEN"
   }),
   PinTopSite: (site, index) => ({
     id: "menu_action_pin",
     icon: "pin",
     action: ac.SendToMain({
       type: at.TOP_SITES_PIN,
-      data: { site: { url: site.url, title: shortURL(site) }, index }
+      data: { site: { url: site.url, title: site.hostname }, index }
     }),
     userEvent: "PIN"
   }),
   UnpinTopSite: site => ({
     id: "menu_action_unpin",
     icon: "unpin",
     action: ac.SendToMain({
       type: at.TOP_SITES_UNPIN,
@@ -2635,17 +2654,17 @@ module.exports = {
     userEvent: "SAVE_TO_POCKET"
   })
 };
 
 module.exports.CheckBookmark = site => site.bookmarkGuid ? module.exports.RemoveBookmark(site) : module.exports.AddBookmark(site);
 module.exports.CheckPinTopSite = (site, index) => site.isPinned ? module.exports.UnpinTopSite(site) : module.exports.PinTopSite(site, index);
 
 /***/ }),
-/* 24 */
+/* 23 */
 /***/ (function(module, exports) {
 
 var g;
 
 // This works in non-strict mode
 g = (function() {
 	return this;
 })();
@@ -2662,45 +2681,45 @@ try {
 // g can still be undefined, but nothing to do about it...
 // We return undefined, instead of nothing here, so it's
 // easier to handle this case. if(!global) { ...}
 
 module.exports = g;
 
 
 /***/ }),
-/* 25 */
+/* 24 */
 /***/ (function(module, exports) {
 
 module.exports = Redux;
 
 /***/ }),
-/* 26 */
+/* 25 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const React = __webpack_require__(0);
-const ReactDOM = __webpack_require__(12);
-const Base = __webpack_require__(7);
+const ReactDOM = __webpack_require__(11);
+const Base = __webpack_require__(6);
 
 var _require = __webpack_require__(3);
 
 const Provider = _require.Provider;
 
-const initStore = __webpack_require__(9);
-
-var _require2 = __webpack_require__(11);
+const initStore = __webpack_require__(8);
+
+var _require2 = __webpack_require__(10);
 
 const reducers = _require2.reducers;
 
-const DetectUserSessionStart = __webpack_require__(8);
-
-var _require3 = __webpack_require__(10);
+const DetectUserSessionStart = __webpack_require__(7);
+
+var _require3 = __webpack_require__(9);
 
 const addSnippetsSubscriber = _require3.addSnippetsSubscriber;
 
 
 new DetectUserSessionStart().sendEventOrAddListener();
 
 const store = initStore(reducers);
 
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -152,17 +152,18 @@ a {
 .outer-wrapper {
   display: flex;
   flex-grow: 1;
   padding: 62px 32px 32px;
   height: 100%; }
 
 main {
   margin: auto;
-  width: 224px; }
+  width: 224px;
+  padding-bottom: 120px; }
   @media (min-width: 416px) {
     main {
       width: 352px; } }
   @media (min-width: 544px) {
     main {
       width: 480px; } }
   @media (min-width: 800px) {
     main {
@@ -178,23 +179,59 @@ main {
   margin: 0 0 18px; }
   .section-title span {
     vertical-align: middle; }
 
 .top-sites-list {
   list-style: none;
   margin: 0;
   padding: 0;
-  margin-inline-end: -32px; }
-  @media (min-width: 544px) {
-    .top-sites-list {
-      width: 512px; } }
-  @media (min-width: 800px) {
-    .top-sites-list {
-      width: 768px; } }
+  margin-inline-end: -32px;
+  margin-bottom: -18px; }
+  @media (max-width: 416px) {
+    .top-sites-list :nth-child(2n+1) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: auto;
+      offset-inline-start: -32px;
+      offset-inline-end: auto; }
+    .top-sites-list :nth-child(2n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 416px) and (max-width: 544px) {
+    .top-sites-list :nth-child(3n+2) .context-menu, .top-sites-list :nth-child(3n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 544px) and (max-width: 800px) {
+    .top-sites-list :nth-child(4n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 544px) and (max-width: 768px) {
+    .top-sites-list :nth-child(4n+3) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 800px) and (max-width: 1248px) {
+    .top-sites-list :nth-child(6n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 800px) and (max-width: 1024px) {
+    .top-sites-list :nth-child(6n+5) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
   .top-sites-list li {
     display: inline-block;
     margin: 0 0 18px;
     margin-inline-end: 32px; }
   .top-sites-list .top-site-outer {
     position: relative; }
     .top-sites-list .top-site-outer > a {
       display: block;
@@ -255,16 +292,27 @@ main {
       border-radius: 6px;
       box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
       background-size: cover;
       background-position: top left;
       transition: opacity 1s;
       opacity: 0; }
       .top-sites-list .top-site-outer .screenshot.active {
         opacity: 1; }
+    .top-sites-list .top-site-outer .tippy-top-icon {
+      position: absolute;
+      top: 0;
+      left: 0;
+      height: 100%;
+      width: 100%;
+      border-radius: 6px;
+      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
+      background-position: center center;
+      background-size: 80px;
+      background-repeat: no-repeat; }
     .top-sites-list .top-site-outer .title {
       height: 30px;
       line-height: 30px;
       text-align: center;
       font-size: 11px;
       width: 96px;
       position: relative; }
       .top-sites-list .top-site-outer .title .icon {
@@ -290,22 +338,22 @@ main {
   .sections-list .section-top-bar .info-option-icon {
     background-image: url("assets/glyph-info-option-12.svg");
     background-size: 12px 12px;
     background-repeat: no-repeat;
     background-position: center;
     height: 16px;
     width: 16px;
     display: inline-block; }
-  .sections-list .section-top-bar .section-info-option div {
+  .sections-list .section-top-bar .section-info-option .info-option {
     visibility: hidden;
     opacity: 0;
     transition: visibility 0.2s, opacity 0.2s ease-out;
     transition-delay: 0.5s; }
-  .sections-list .section-top-bar .section-info-option:hover div {
+  .sections-list .section-top-bar .info-option-icon[aria-expanded="true"] + .info-option {
     visibility: visible;
     opacity: 1;
     transition: visibility 0.2s, opacity 0.2s ease-out; }
   .sections-list .section-top-bar .info-option {
     z-index: 9999;
     position: absolute;
     background: #FFF;
     border: solid 1px rgba(0, 0, 0, 0.1);
@@ -332,16 +380,34 @@ main {
     color: #0A84FF; }
 
 .sections-list .section-list {
   clear: both;
   margin: 0;
   display: grid;
   grid-template-columns: repeat(auto-fit, 224px);
   grid-gap: 32px; }
+  @media (max-width: 544px) {
+    .sections-list .section-list .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 544px) and (max-width: 800px) {
+    .sections-list .section-list :nth-child(2n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 800px) and (max-width: 1248px) {
+    .sections-list .section-list :nth-child(3n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
 
 .sections-list .section-empty-state {
   width: 100%;
   height: 266px;
   display: flex;
   border: solid 1px rgba(0, 0, 0, 0.1);
   border-radius: 3px;
   margin-bottom: 16px; }
@@ -361,44 +427,57 @@ main {
     .sections-list .section-empty-state .empty-state .empty-state-message {
       margin-bottom: 0;
       font-size: 13px;
       font-weight: 300;
       color: #A0A0A0;
       text-align: center; }
 
 .topic {
-  font-size: 13px;
+  font-size: 12px;
   color: #BFC0C7;
-  min-width: 780px;
-  line-height: 16px;
-  margin-top: 16px; }
+  margin-top: 16px;
+  line-height: 1.6; }
+  @media (min-width: 800px) {
+    .topic {
+      line-height: 16px;
+      min-width: 780px; } }
   .topic ul {
-    display: inline;
-    padding-left: 12px; }
+    margin: 0;
+    padding: 0; }
+    @media (min-width: 800px) {
+      .topic ul {
+        display: inline;
+        padding-left: 12px; } }
   .topic ul li {
-    display: inline; }
-  .topic ul li::after {
-    content: '•';
-    padding-left: 8px;
-    padding-right: 8px; }
-  .topic ul li:last-child::after {
-    content: none; }
+    display: inline-block; }
+    @media (min-width: 800px) {
+      .topic ul li {
+        display: inline; } }
+    .topic ul li::after {
+      content: '•';
+      padding-left: 8px;
+      padding-right: 8px; }
+    .topic ul li:last-child::after {
+      content: none; }
   .topic .topic-link {
     color: #008EA4; }
   .topic .topic-read-more {
-    float: right;
-    margin-right: 40px;
     color: #008EA4; }
+    @media (min-width: 800px) {
+      .topic .topic-read-more {
+        margin-right: 40px;
+        float: right; } }
   .topic .topic-read-more-logo {
     padding-right: 10px;
     margin-left: 5px;
     background-image: url("assets/topic-show-more-12.svg");
     background-repeat: no-repeat;
-    vertical-align: middle; }
+    vertical-align: middle;
+    background-position-y: 1px; }
 
 .search-wrapper {
   cursor: default;
   display: flex;
   position: relative;
   margin: 0 0 40px;
   width: 100%;
   height: 36px; }
@@ -689,27 +768,32 @@ main {
   .card-outer .card {
     height: 100%;
     border-radius: 3px; }
   .card-outer > a {
     display: block;
     color: inherit;
     height: 100%;
     outline: none;
-    position: absolute; }
+    position: absolute;
+    max-width: 224px; }
     .card-outer > a.active .card, .card-outer > a:focus .card {
       box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
       transition: box-shadow 150ms; }
+    .card-outer > a.active .card-title, .card-outer > a:focus .card-title {
+      color: #00AFF7; }
   .card-outer:hover, .card-outer:focus, .card-outer.active {
     outline: none;
     box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
     transition: box-shadow 150ms; }
     .card-outer:hover .context-menu-button, .card-outer:focus .context-menu-button, .card-outer.active .context-menu-button {
       transform: scale(1);
       opacity: 1; }
+    .card-outer:hover .card-title, .card-outer:focus .card-title, .card-outer.active .card-title {
+      color: #00AFF7; }
   .card-outer .card-preview-image {
     position: relative;
     background-size: cover;
     background-position: center;
     background-repeat: no-repeat;
     height: 122px;
     border-bottom-color: rgba(0, 0, 0, 0.1);
     border-bottom-style: solid;
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -1,12 +1,13 @@
 <!doctype html>
 <html lang="en-us" dir="ltr">
   <head>
     <meta charset="utf-8">
+    <meta http-equiv="Content-Security-Policy-Report-Only" content="script-src 'unsafe-inline'; img-src http: https: data: blob:; style-src 'unsafe-inline'; child-src 'none'; object-src 'none'; report-uri https://tiles.services.mozilla.com/v4/links/activity-stream/csp">
     <title></title>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"></div>
     <div id="snippets-container">
       <div id="snippets"></div>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f4f0cbf456f07dcf87d60475115552c8d76c01cf
GIT binary patch
literal 2571
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
b$OdYmycf%=Utj*ckAQ=ntL@!`!KwcMy($w|
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b5af67309d13c1b4eec7b9d36d419de35434d880
GIT binary patch
literal 3691
zc%1E)=QkS+8;29I_pXXnrB-X!t{tPLgv2T(`k=9QXsn=C6xG_)tWgP4N{yD#5}PVf
zd$hHy_EvA-bKd{reXeuv`*(hy@B71*2sJfiqUWUt002yPjUX0(v*^D=3;Mf1uw@AX
z02KRB6S)50digK^^8du$F2C!4|Nbn{!W0Go04YF}RMa%Ibo302Ow25-Z0sDIT-;Z9
zuJVHU_yq)ot_fck5xpTME+Hv(^Om%Xtem{UZAB$z6;(C$I~tl=+B&*=5Pbtfqr3Nv
zP41hTLCq~-a7!y|8`}qV_708+r$_%dySP3^y19FJdU^Z!`uPV02BCsO(4k@B5s{dv
z=$NOmaq$U>*raF4DXGuXUc5}t$jr*l$<51uRZv(|TvGbFth}-chp(=wCA_Vxf7j6X
zzNxvT^~1-uc49~8r_Wv8q@Lcs{(-@vkuRfP$HvJM-zKM~XJ+T-7Z#Vk|M<DQvbwgu
zvH5H3_x8^2-u}Vi(ecUY+4;qvE6{FZ0DxZVE<^_&GW9FpO&e~>Ng`cEJh#6=_0D7l
zHYU#xhx^J{%siOMf{$_f0nJBmR>M1A4cwF+d}j~ylSme}W4u4oX94Sw=io}SeEP&u
z*EJ+yDEw&XgYv+KBh7)6?u+@K%0A7%X99-xOa1buXO@-THxGJw*Sjd%Khj*3V{ix?
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..73ebdfbb52bb727a030d7479951976fb977c3c7a
GIT binary patch
literal 3496
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
Wtd;AcgjU4>0000<MNUMnLSTX@&EPEn
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..23587c5857843de336950650a2fc6e0a481874cf
GIT binary patch
literal 12722
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6d29e868ed6961fb4d36423c37ca9465b6d50016
GIT binary patch
literal 1705
zc$~GBdpy&77{|Z6klck<rqTsPoQT5MTsA6^9K<0+vam{aM#KD+Yej^v!={^~A~u#7
z>$+5K$^EjzVQMqEW|8BZzxwl>KhEp*JkR^{dOiQYKk06+PD=7x@&Ev(y?gNP%Vhk$
za<a?!LUX7Q0J5j{;_W<QV50X-CHM~2l-3hGRpJOQa4noa>(Z;ItDlA1QazkL8unwL
zb6mm~@FY$;wn#ZR9{LxMQ$?l$yZ;KNm|K`PGf>w3II8K6w5}^ld_mkkS6SKdM?};7
z8)dUsk39t_bVYnQSBiznyFqXb29Vim`%hf>@g+4sO0l3n_1T1=iq!kl`1R77_Lov<
zCp%njjHgCduv}borjPd&<lM%LafF*`^$~+}d?AJwtstwTpODeKRBhl{Pg`7F)OR-*
zlQHe`{%JNMCf-~;r0wi>ESzj2H7x@{_ie_^i3)zWoj2kOiQnm^{fBN9vXB$ksoIy!
zPV5r-CFUn)!((VR;vu@T`@PgC<VQ3yxb&^X3>&R8%Ean-7q3M@QhTH1hqy^4#%fB{
z?GrYeWGB#U$HU?eGry_*o}plCq7@{m*MN~xL7pFQdSKhmq-nU6W*x$@BQ5Qu0+(G2
z_v9Hhs{EI5M!#ZGebxOSXpMR!e%)%;++o@QH6zI5v`S`zd(rOJMxsOb#TCyuK`XMB
znzxY(znj+Pk}aDFbZAIykJvL{ciY!vG7Mig)zX_g)|rxTJpRNV>Zr&zlLzxlNv41L
zGb+^69VKWCL?0b6y;Nyvv%AYv4aCG;*KgGlE^0~Z(PNFb4IPyjZSz{WwV*hJh3PSP
zyHt~c%!+G1<0wpj9p!wku17%dJrec68@Av=ewMop5XVs_Padq3x1F56lMsqp#HPp6
zl}V4VtZWl2(9mW_esM7!d@BN{+MvxJH9sTHO<s~o8%N4a=pLk0Y5+Vct->i8v=q6H
z=&UZ2!T1y$g=qvF2~SfSJX?z(W*&0P^O-qB%Tk*lnMgB2?4ad$<f}$P=sVumamy7G
zz86-;(pw$i8Ib-3bdWlxKm$@7!(0o9(69+kj7NV?j0l1WVuoYeMI(Tx?%Xq%G!I%G
z4Km*>wSBgY*8`;npw`$Da90J?by0V<dq3jF`qY{q-R?~=wwMiJ!r0j=zPqp8vmt=v
z0x~6v`IHHUy;TlW*Civ}tc#4<YeLSK2aeb`IWHC{B0u8R3j{5L@Z!;y#XLAPn9Wg|
z5#^acT<ah8i48EK1Rd6~gRfV1%04*N8^-3B5Cz(^*`BbZ@au*KL(#kj!eJVFh%kki
zbtrq!jJ>&GANie^<t$>>_R?f5LL~`xY(cZOsF>e4h7fYTB2_aDtwveau&v}P)O~^X
zLQ}wcU1`jeJwhnb-YC+}29Q@-dvbn_DRRO*a`VOT4cn4sRpWI;+jHQMka}bkQiN$U
zydHNW2a6EjSpMc$U&wu9&=x|h&?BN}%_(Y>0*yMB%qrM~hK+)5+dS0YC@9gtwTpsf
zM1A$!M-5<uCm$Djf|9&(5^$Mwvzp|w-;$TR&>I)-WkJh1oMZJAoirW-eBAgqH-oG=
z?$`&eb?N!3NkxVW!RdYs;|z8uczGITuCLDZAIg+RU2VdcfnaaVP^or&1lE*n5#2Xk
zP!p*=xqzN3v1qMRDdyYw4fn^p9_J`i0~v(9+}24=S!AZe0S2xGlpBo4v1LhGH7^_i
zKyQgC%*AhY<w!p9zMQRLYKEnC9~jz%%@M{~u4o|ze!zJGasMxu^VOs~+$HYen9GQk
zYJ@TY;ZDD3`(2N1{=8hQ6kLX{`eK)JqRNwGd{m_F&ZqqP9JeRZz9cbpgKyV&^N#*<
z5jShvw5j|Qq6bqQRgs`ZNwD2GCF#!pWF@A&r&RZCaC>dMDWsE$b-LwraE?xLn^&?@
zc(`(Zju+^|(-U;bid5LlNpV(B&&J9lr)#co9i$di-q>+YgD~Ks5!Dw|&8DsI7|34*
z00b08f*W!c1L~cDw4b%<*I>lT^Sv8NQhpxx|H^$Muxj7(#?k-I@Nxx#y#!Z$seRzt
FzW`{r@=pK&
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1aa8262eebe5b5e980d9f6ab6734f40c2cbfbb49
GIT binary patch
literal 1327
zc%17D@N?(olHy`uVBq!ia0vp^3qY8I8A$FCoG#D6z<4^qC&bmgz=vTJ&=H8UvWc>>
zGjWM9aEUT=3yTRdNeVGY2r=*rGw}&CozpeiY3L{#x{e`guUO)$W79<M%w@VXkELme
zaK%#5r$!d%%ss^84>6=%lFZqAXSw*-^-QleuuR-8+O|X7(f4IB(1PlcAirP+X8nkU
z<tJ`E{qX<yy9X!L^EPOR<(|1xA!WAem&rC$$p!xwGN!#3-)i$Lwz#XdCe!cF$DfB<
zB8{c_n>9HuhjMM{|2|D&%0ziNrc<v1GB}@aevz}9h3(Pa_<J`Kj{T^sXFnfu;o8;c
zl;o(X-}t_+_+t0=XxO6c$?a{GpS@OVS}`y%&G2+_45?r|bFF!jiK76^1FQQ_Z*I%I
z4MY_`=Pz6rdOqjto4Ujlx4w3jmX@|wcNaI;;}7QF*zu#K<HLpv0$g03o+k?nwDl}C
zTbXoBb!FpHQ_=z-q&$CeX!)HvIb1z^YBEmgD?Aq!6LpR*^i;H5y=9Hev?<f4PHNWD
zN{mcpwOqDJ$}A*&qH|VJqIVvz<)Up8W}??8F~54yZe_Git}avIu++olZbqwCO4eoN
zDK!U0J&4PWu69okH~)M2?$w(YFJI&O9#wU6#=g28lcOySJoMPu+ln6g)z9D9c(@=r
zrKHD3LR!9X`V51u>o4EsoniA**J$dScV=gdH|JNe>7_ldS^6w3HrV*G8{4_Vu1?e6
zM}#kNem|?#wZOaC>fk}ug45h8`K)(d&5A41WsjNlXWv&|gW0cd-SnMzHzATSI$_)H
zuZQni+w9f0E9kGcwpzQy`FmuI{~u-cX9uGGH9QY)VVB$VPow+(>ctvi1sXOVW_+C=
zp=*)Ix_r~`O<fBuW^cP?P|N39IPJv&?iG4#!(%pfWnXMG54dwLa{Z3o?MWde2}~lE
zWma+vJSN;R|I2Z7>Ve!(7P=M%T&oN_CutsR^bfGPon6`X$g?0pLPY;tRNR6J)9<Ul
zrmcU<+f{zBab`fz?d+coN|)~(@UPa552$&)bW!ORhmXO3<F_n3`n0|-qC0Gwz>~V|
z+2ST5&VeE<^WUF8!?X5Il*Pl@X<-LBA{>C=z%525An@C?Y;)z6q@7(+`P(9Mr*E~F
zS?u@R`^wj{^I83Cp1+p;`t#qSf3fpfIdB0BD+N@L@bl!hKg-^@u(PnR=m;nzTxDQ{
Z0rm;@X&=t5y3Gg5jGnH3F6*2UngHA%oUi}@
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ac979f8aa49eb5ba3d3253ac772b21649f358c45
GIT binary patch
literal 1063
zc%17D@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&V1DQ6;uuoFm{g$u!SUe#|Nqlj
zw-oblSQL{e;+B28Ea3LG+}qpUnyrw%3TAvW%0Iuk_Ib=2{hJ&Ao#`r8yKT7J{<mKC
zS(804f4}*d^FHU#zrA1k^MAfsyheA@zO&clBo1)h=Zj2M+nTC-v#yqT&4;yfOw)s8
zx2C-g|E5)wobo<3Xm0K7-f+e{TG|u0xU+s(w#MZWv*>~In}j^=!x*GDDS0w#FlsQU
zGO#XSS->!bL8L*bfhm9~fWeD_%VDK9Q%Lf+qO#DktZmWT0=Ic>E7?|XUU%BsOV_WR
zF*{p!?%UbE6X#BxJHPI%+=;l8`%XwPgshL+w0!g7O{$v<S6xrZ-E->bm7BjA%I?~o
z_;z}kl*Obdfqk3_*I)0<`I>XZY9r%{{>{E#2bcq<r#!yS%j58O=Gt&3@rGY&v-h*;
zFnskiE9MMgcy&CdPiO(dmHL~HmO3!5xa*zB6!3jY6U%~sr_(f9IUVLkatSv4vYfp_
zM2Vp^Ow)m3mFDb~A_@$p(Vh+rS+|2K7($ZGGBs@&UR|72!Eoi{TZRieZtC2uxq0aD
z#(pb3hqZAGcT#TNyE*sf_1NFB8<wpSym#Z~$(#0a%feqym-^^C$9Dc#J_p-);eCOd
z**DcO1#G{<QqY~#z;NZLDPxVC1Miy4SqIE>uW>D4yC!JL5TmXw*1#agw!n+YM%-a4
zONC~G7Uu^khER0}23Caz1||&#Miv1FMh+mG%K^v^U|<5WnF>yTg_u^fy}3Sd4>N;I
zRKu>c2L7C>-RWLTECL7KGq7y96TdY`6R5^Qyn$gF%MLRzX$~SQ!0ZZb5cxw3OiBaE
zL*|*&fckh2urji2P&v>GCY>2TBsZ9>29xDr@p>RBvhU?47KMgX20fr#RT=ZZq#QGd
zjAI0mcDF1Us%F&1@J9HZvXeZS{X&MJw2GHO|5G{(#7hSrayVf3p#YFA0(3pc0tTQo
z?!ZW3r@(|a29RL9F@f`d(BF93PmyX2-d`e1Pt2XkpmTBVRmIzBOdDKohqd=i+&iE1
zfK|1<^y2G=t5eR-WLUHKS<cf}ef#)t8D^hhNr=8S<L!;tegFES6n|?7H{@P)pSp41
zdx;x~$*I<<{OR}8*Un^+nHRPBeO1&dpWdtgW$!YG+4^yxTclPGEC3ihUHx3vIVCg!
E0RPRCjQ{`u
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5018a37e0db23ac5bb8b2ed563908c6345f32aa1
GIT binary patch
literal 2745
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
Ij?k|C1BD+Ym;e9(
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1093f198b78b730e2efc7b2074040efca967dda4
GIT binary patch
literal 3054
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4f616096b744b9888952ef5e55729728c29efa67
GIT binary patch
literal 6920
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
O0000<MNUMnLSTZuN=1bL
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b49d4d4cf0a31d5b14aa15b26ef8731a8d4d3896
GIT binary patch
literal 5003
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bc68229cbae1f23ba24e9ac78df64cf49517a8db
GIT binary patch
literal 2419
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
W?Ek8G9RKr~0=ORYaAG+GCI1%|`hyYx
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0e857f559846ddfe65241817ab29b6aafe5a85cf
GIT binary patch
literal 2531
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..efb8a4f54685608d07415e681d5c11a6c2f3b063
GIT binary patch
literal 2920
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..17f68f4f20724b9561e98a869f977e64eb645a56
GIT binary patch
literal 3106
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
Q?SD&vgDnnIZxfjEKL^?gO#lD@
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3665e6d1ea1f36391b1df76e8f2c2da8928100a2
GIT binary patch
literal 1702
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
N0{nva*L=+s{2NvI3a|hG
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/tippytop/top_sites.json
@@ -0,0 +1,114 @@
+[
+  {
+    "title": "aliexpress",
+    "url": "https://www.aliexpress.com/",
+    "image_url": "aliexpress-com.png",
+    "background_color": "#f6050a",
+    "domain": "aliexpress.com"
+  },
+  {
+    "title": "allegro",
+    "url": "https://www.allegro.pl/",
+    "image_url": "allegro-pl.png",
+    "background_color": "#ff4900",
+    "domain": "allegro.pl"
+  },
+  {
+    "title": "amazon",
+    "urls": ["https://www.amazon.com/", "https://www.amazon.ca/", "https://www.amazon.de/", "https://www.amazon.co.uk/", "https://www.amazon.fr/"],
+    "image_url": "amazon-com.png",
+    "background_color": "#FFF",
+    "domain": "amazon.com"
+  },
+  {
+    "title": "avito",
+    "url": "https://www.avito.ru/",
+    "image_url": "avito-ru.png",
+    "background_color": "#FFFFFF",
+    "domain": "avito.ru"
+  },
+  {
+    "title": "bbc",
+    "urls": ["http://www.bbc.com/", "https://www.bbc.co.uk/"],
+    "image_url": "bbc-com.png",
+    "background_color": "#000000",
+    "domain": "bbc.com"
+  },
+  {
+    "title": "ebay",
+    "urls": ["https://www.ebay.com", "https://ebay.de", "https://www.ebay.co.uk/"],
+    "image_url": "ebay-com.png",
+    "background_color": "#ededed",
+    "domain": "ebay.com"
+  },
+  {
+    "title": "facebook",
+    "url": "https://www.facebook.com/",
+    "image_url": "facebook-com.png",
+    "background_color": "#3b5998",
+    "domain": "facebook.com"
+  },
+  {
+    "title": "leboncoin",
+    "url": "http://www.leboncoin.fr/",
+    "image_url": "leboncoin-fr.png",
+    "background_color": "#ff6000",
+    "domain": "leboncoin.fr"
+  },
+  {
+    "title": "ok",
+    "url": "https://www.ok.ru/",
+    "image_url": "ok-ru.png",
+    "background_color": "#fb7b00",
+    "domain": "ok.ru"
+  },
+  {
+    "title": "olx",
+    "url": "https://www.olx.pl/",
+    "image_url": "olx-pl.png",
+    "background_color": "#b7c200",
+    "domain": "olx.pl"
+  },
+  {
+    "title": "reddit",
+    "url": "https://www.reddit.com/",
+    "image_url": "reddit-com.png",
+    "background_color": "#cee3f8",
+    "domain": "reddit.com"
+  },
+  {
+    "title": "twitter",
+    "url": "https://twitter.com/",
+    "image_url": "twitter-com.png",
+    "background_color": "#049ff5",
+    "domain": "twitter.com"
+  },
+  {
+    "title": "vk",
+    "url": "https://vk.com/",
+    "image_url": "vk-com.png",
+    "background_color": "#4483be",
+    "domain": "vk.com"
+  },
+  {
+    "title": "youtube",
+    "url": "https://www.youtube.com/",
+    "image_url": "youtube-com.png",
+    "background_color": "#db4338",
+    "domain": "youtube.com"
+  },
+  {
+    "title": "wikipedia",
+    "url": "https://www.wikipedia.org/",
+    "image_url": "wikipedia-org.png",
+    "background_color": "#fff",
+    "domain": "wikipedia.org"
+  },
+  {
+    "title": "wykop",
+    "url": "https://www.wykop.pl/",
+    "image_url": "wykop-pl.png",
+    "background_color": "#157ead",
+    "domain": "wykop.pl"
+  }
+]
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -480,24 +480,26 @@
   "bs": {},
   "ca": {
     "newtab_page_title": "Pestanya nova",
     "default_label_loading": "S'està carregant…",
     "header_top_sites": "Llocs principals",
     "header_stories": "Articles populars",
     "header_visit_again": "Torneu a visitar",
     "header_bookmarks": "Adreces d'interès recents",
+    "header_recommended_by": "Recomanat per {provider}",
     "header_bookmarks_placeholder": "Encara no teniu cap adreça d'interès.",
     "header_stories_from": "de",
     "type_label_visited": "Visitats",
     "type_label_bookmarked": "A les adreces d'interès",
     "type_label_synced": "Sincronitzat des d'un altre dispositiu",
     "type_label_recommended": "Tendència",
     "type_label_open": "Obert",
     "type_label_topic": "Tema",
+    "type_label_now": "Ara",
     "menu_action_bookmark": "Afegeix a les adreces d'interès",
     "menu_action_remove_bookmark": "Elimina l'adreça d'interès",
     "menu_action_copy_address": "Copia l'adreça",
     "menu_action_email_link": "Envia l'enllaç per correu…",
     "menu_action_open_new_window": "Obre en una finestra nova",
     "menu_action_open_private_window": "Obre en una finestra privada nova",
     "menu_action_dismiss": "Descarta",
     "menu_action_delete": "Elimina de l'historial",
@@ -506,16 +508,17 @@
     "confirm_history_delete_p1": "Segur que voleu suprimir de l'historial totes les instàncies d'aquesta pàgina?",
     "confirm_history_delete_notice_p2": "Aquesta acció no es pot desfer.",
     "menu_action_save_to_pocket": "Desa al Pocket",
     "search_for_something_with": "Cerca {search_term} amb:",
     "search_button": "Cerca",
     "search_header": "Cerca de {search_engine_name}",
     "search_web_placeholder": "Cerca al web",
     "search_settings": "Canvia els paràmetres de cerca",
+    "section_info_option": "Informació",
     "welcome_title": "Us donem la benvinguda a la pestanya nova",
     "welcome_body": "El Firefox utilitzarà aquest espai per mostrar-vos les adreces d'interès, els articles i els vídeos més rellevants, així com les pàgines que heu visitat recentment, per tal que hi pugueu accedir fàcilment.",
     "welcome_label": "S'estan identificant els vostres llocs destacats",
     "time_label_less_than_minute": "<1 m",
     "time_label_minute": "{number} m",
     "time_label_hour": "{number} h",
     "time_label_day": "{number} d",
     "settings_pane_button_label": "Personalitzeu la pàgina de pestanya nova",
@@ -550,17 +553,21 @@
     "topsites_form_add_button": "Afegeix",
     "topsites_form_save_button": "Desa",
     "topsites_form_cancel_button": "Cancel·la",
     "topsites_form_url_validation": "Es necessita un URL vàlid",
     "pocket_read_more": "Temes populars:",
     "pocket_read_even_more": "Mostra més articles",
     "pocket_feedback_header": "El millor del web, seleccionat per més de 25 milions de persones.",
     "pocket_feedback_body": "El Pocket, membre de la família Mozilla, us permet accedir a contingut d'alta qualitat que d'altra manera potser no trobaríeu.",
-    "pocket_send_feedback": "Doneu la vostra opinió"
+    "pocket_send_feedback": "Doneu la vostra opinió",
+    "topstories_empty_state": "Ja esteu al dia. Torneu més tard per veure més articles populars de {provider}. No podeu esperar? Trieu un tema popular per descobrir els articles més interessants de tot el web.",
+    "manual_migration_explanation": "Proveu el Firefox amb els vostres llocs preferits i les adreces d'interès d'un altre navegador.",
+    "manual_migration_cancel_button": "No, gràcies",
+    "manual_migration_import_button": "Importa-ho ara"
   },
   "cak": {},
   "cs": {
     "newtab_page_title": "Nový panel",
     "default_label_loading": "Načítání…",
     "header_top_sites": "Top stránky",
     "header_stories": "Nejlepší příběhy",
     "header_visit_again": "Znovu navštívit",
@@ -1402,35 +1409,38 @@
     "topsites_form_save_button": "Guardar",
     "topsites_form_cancel_button": "Cancelar",
     "topsites_form_url_validation": "Se requiere URL válida",
     "pocket_read_more": "Tópicos populares:",
     "pocket_read_even_more": "Ver más historias",
     "pocket_feedback_header": "Lo mejor de la web, seleccionado por más de 25 millones de personas.",
     "pocket_feedback_body": "Pocket, parte de la familia Mozilla, ayudará a conectarte con contenido de alta calidad que no podrías haber encontrado de otra forma.",
     "pocket_send_feedback": "Enviar opinión",
+    "topstories_empty_state": "Ya te pusiste al día. Volvé más tarde para más historias de {provider}. ¿No podés esperar? Seleccioná un tema popular para encontrar más historias de todo el mundo.",
     "manual_migration_explanation": "Probá Firefox con tus sitios favoritos y marcadores de otro navegador.",
     "manual_migration_cancel_button": "No gracias",
     "manual_migration_import_button": "Importar ahora"
   },
   "es-CL": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Sitios frecuentes",
     "header_stories": "Historias populares",
     "header_visit_again": "Volver a visitar",
     "header_bookmarks": "Marcadores recientes",
+    "header_recommended_by": "Recomendado por {provider}",
     "header_bookmarks_placeholder": "Todavía no tienes marcadores.",
     "header_stories_from": "de",
     "type_label_visited": "Visitado",
     "type_label_bookmarked": "Marcado",
     "type_label_synced": "Sacado de otro dispositivo",
     "type_label_recommended": "Popular",
     "type_label_open": "Abrir",
     "type_label_topic": "Tema",
+    "type_label_now": "Ahora",
     "menu_action_bookmark": "Marcador",
     "menu_action_remove_bookmark": "Remover marcador",
     "menu_action_copy_address": "Copiar dirección",
     "menu_action_email_link": "Enviar enlace por correo",
     "menu_action_open_new_window": "Abrir en una nueva ventana",
     "menu_action_open_private_window": "Abrir en una nueva ventana privada",
     "menu_action_dismiss": "Descartar",
     "menu_action_delete": "Eliminar del historial",
@@ -1439,16 +1449,17 @@
     "confirm_history_delete_p1": "¿Estás seguro de que quieres eliminar cada instancia de esta página de tu historial?",
     "confirm_history_delete_notice_p2": "Esta acción no puede ser deshecha.",
     "menu_action_save_to_pocket": "Guardar en Pocket",
     "search_for_something_with": "Buscar {search_term} con:",
     "search_button": "Buscar",
     "search_header": "Búsqueda de {search_engine_name}",
     "search_web_placeholder": "Buscar en la Web",
     "search_settings": "Cambiar ajustes de búsqueda",
+    "section_info_option": "Info",
     "welcome_title": "Bienvenido a la nueva pestaña",
     "welcome_body": "Firefox usará este espacio para mostrarte los marcadores, artículos, videos y páginas visitadas recientemente más relevantes, para que puedas regresar a ellos de una.",
     "welcome_label": "Identificando tus destacados",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
     "time_label_day": "{number}d",
     "settings_pane_button_label": "Personaliza tu página de Nueva pestaña",
@@ -1483,17 +1494,21 @@
     "topsites_form_add_button": "Añadir",
     "topsites_form_save_button": "Guardar",
     "topsites_form_cancel_button": "Cancelar",
     "topsites_form_url_validation": "URL válida requerida",
     "pocket_read_more": "Temas populares:",
     "pocket_read_even_more": "Ver más historias",
     "pocket_feedback_header": "Lo mejor de la web, revisado por más de 25 millones de personas.",
     "pocket_feedback_body": "Pocket, una parte de la familia de Mozilla, te ayudará a conectarte con contenido de alta calidad que de otra forma no hubieras encontrado.",
-    "pocket_send_feedback": "Enviar comentario"
+    "pocket_send_feedback": "Enviar comentario",
+    "topstories_empty_state": "Te has puesto al día. Revisa más tarde para ver más historias de {provider}. ¿No puedes esperar? Selecciona un tema popular para encontrar más historias de todo el mundo.",
+    "manual_migration_explanation": "Prueba Firefox con tus sitios favoritos y marcadores de otro navegador.",
+    "manual_migration_cancel_button": "No, gracias",
+    "manual_migration_import_button": "Importar ahora"
   },
   "es-ES": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Sitios favoritos",
     "header_stories": "Historias populares",
     "header_visit_again": "Visitar de nuevo",
     "header_bookmarks": "Marcadores recientes",
@@ -1736,24 +1751,26 @@
   "eu": {},
   "fa": {
     "newtab_page_title": "زبانه جدید",
     "default_label_loading": "در حال بارگیری…",
     "header_top_sites": "سایت‌های برتر",
     "header_stories": "برترین داستان‌ها",
     "header_visit_again": "مشاهده دوباره",
     "header_bookmarks": "نشانک‌های اخیر",
+    "header_recommended_by": "پیشنهاد شده توسط {provider}",
     "header_bookmarks_placeholder": "هنوز هیچ نشانکی ندارید.",
     "header_stories_from": "از",
     "type_label_visited": "مشاهده شده",
     "type_label_bookmarked": "نشانک شده",
     "type_label_synced": "هم‌گام شده از دستگاهی دیگر",
     "type_label_recommended": "موضوعات داغ",
     "type_label_open": "باز کردن",
     "type_label_topic": "موضوع",
+    "type_label_now": "هم‌اکنون",
     "menu_action_bookmark": "نشانک",
     "menu_action_remove_bookmark": "حذف نشانک",
     "menu_action_copy_address": "رونوشت از آدرس",
     "menu_action_email_link": "ارسال پیوند…",
     "menu_action_open_new_window": "باز کردن در یک پنجره جدید",
     "menu_action_open_private_window": "بار کردن در یک پنجره ناشناس جدید",
     "menu_action_dismiss": "رد کردن",
     "menu_action_delete": "حذف از تاریخچه",
@@ -1762,16 +1779,17 @@
     "confirm_history_delete_p1": "آیا از پاک کردن همه نمونه‌های این صفحه از تاریخ‌چه خود اطمینان دارید؟",
     "confirm_history_delete_notice_p2": "این عمل قابل برگشت نیست.",
     "menu_action_save_to_pocket": "ذخیره‌سازی در Pocket",
     "search_for_something_with": "جست‌وجو برای {search_term} با:",
     "search_button": "جست‌وجو",
     "search_header": "جست‌وجو {search_engine_name}",
     "search_web_placeholder": "جست‌وجوی وب",
     "search_settings": "تغییر تنظیمات جست‌وجو",
+    "section_info_option": "اطلاعات",
     "welcome_title": "به زبانه جدید خوش‌آمدید",
     "welcome_body": "فایرفاکس از این فضا برای نمایش نشانک‌ها، مقالات، ویدئوها و صفحات مرتبطی که به‌تازگی مشاهده کرده‌اید استفاده می‌کند، تا شما به راحتی دوباره به آنها دسترسی داشته باشید.",
     "welcome_label": "شناسایی گزینه‌های برجسته شما",
     "time_label_less_than_minute": "> ۱ دقیقه",
     "time_label_minute": "{number} د",
     "time_label_hour": "{number} س",
     "time_label_day": "{number} ر",
     "settings_pane_button_label": "صفحهٔ زبانه جدید را سفارشی کنید",
@@ -1806,17 +1824,20 @@
     "topsites_form_add_button": "افزودن",
     "topsites_form_save_button": "ذخیره",
     "topsites_form_cancel_button": "انصراف",
     "topsites_form_url_validation": "URL معتبر الزامی است",
     "pocket_read_more": "موضوع‌های محبوب:",
     "pocket_read_even_more": "مشاهده داستان‌های بیشتر",
     "pocket_feedback_header": "بهترین‌های وب، گزینش شده توسط بیش از ۲۵ میلیون نفر.",
     "pocket_feedback_body": "Pocket، بخشی از خانواده موزیلا، کمک خواهد کرد تا به محتوایی با کیفیت بالا مرتبط شوید که در غیر این صورت ممکن بود پیدا نکنید.",
-    "pocket_send_feedback": "ارسال بازخورد"
+    "pocket_send_feedback": "ارسال بازخورد",
+    "manual_migration_explanation": "فایرفاکس را با سایت‌های مورد علاقه و نشانک‌های خود در سایر مرورگرها امتحان کنید.",
+    "manual_migration_cancel_button": "نه ممنون",
+    "manual_migration_import_button": "هم‌اکنون وارد شوند"
   },
   "ff": {},
   "fi": {
     "newtab_page_title": "Uusi välilehti",
     "default_label_loading": "Ladataan…",
     "header_top_sites": "Ykkössivustot",
     "header_stories": "Ykkösjutut",
     "header_visit_again": "Käy toistekin",
@@ -1967,17 +1988,17 @@
     "pocket_read_more": "Sujets populaires :",
     "pocket_read_even_more": "Afficher plus d’articles",
     "pocket_feedback_header": "Le meilleur du Web, sélectionné par plus de 25 millions de personnes.",
     "pocket_feedback_body": "Pocket, un membre de la famille Mozilla, vous aide à découvrir du contenu de grande qualité que vous auriez pu manquer dans le cas contraire.",
     "pocket_send_feedback": "Donner mon avis",
     "topstories_empty_state": "Il n’y en a pas d’autres. Revenez plus tard pour plus d’articles de {provider}. Vous ne voulez pas attendre ? Choisissez un sujet parmi les plus populaires pour découvrir d’autres articles intéressants sur le Web.",
     "manual_migration_explanation": "Essayez Firefox avec vos sites et marque-pages préférés, importés depuis un autre navigateur.",
     "manual_migration_cancel_button": "Non merci",
-    "manual_migration_import_button": "Importer maintenant"
+    "manual_migration_import_button": "Importer"
   },
   "fy-NL": {
     "newtab_page_title": "Nij ljepblêd",
     "default_label_loading": "Lade…",
     "header_top_sites": "Topwebsites",
     "header_stories": "Topferhalen",
     "header_visit_again": "Nochris besykje",
     "header_bookmarks": "Resinte blêdwizers",
@@ -2656,16 +2677,17 @@
     "menu_action_copy_address": "Salin Alamat",
     "menu_action_email_link": "Emailkan Tautan…",
     "menu_action_open_new_window": "Buka di Jendela Baru",
     "menu_action_open_private_window": "Buka di Jendela Penjelajahan Pribadi Baru",
     "menu_action_dismiss": "Tutup",
     "menu_action_delete": "Hapus dari Riwayat",
     "menu_action_pin": "Semat",
     "menu_action_unpin": "Lepas",
+    "confirm_history_delete_p1": "Yakin ingin menghapus setiap bagian dari laman ini dari riwayat Anda?",
     "confirm_history_delete_notice_p2": "Tindakan ini tidak bisa diurungkan.",
     "menu_action_save_to_pocket": "Simpan ke Pocket",
     "search_for_something_with": "Cari {search_term} lewat:",
     "search_button": "Cari",
     "search_header": "Pencarian {search_engine_name}",
     "search_web_placeholder": "Cari di Web",
     "search_settings": "Ubah Pengaturan Pencarian",
     "section_info_option": "Info",
@@ -2679,16 +2701,20 @@
     "settings_pane_button_label": "Ubahsuai laman Tab Baru Anda",
     "settings_pane_header": "Preferensi Tab Baru",
     "settings_pane_body": "Pilih apa yang Anda lihat ketika Anda membuka tab baru.",
     "settings_pane_search_header": "Pencarian",
     "settings_pane_search_body": "Cari Web dari tab baru Anda.",
     "settings_pane_topsites_header": "Situs Teratas",
     "settings_pane_topsites_body": "Mengakses situs web yang paling sering Anda kunjungi.",
     "settings_pane_topsites_options_showmore": "Tampilkan dua baris",
+    "settings_pane_bookmarks_header": "Markah Terbaru",
+    "settings_pane_bookmarks_body": "Markah Anda dibuat di lokasi yang praktis.",
+    "settings_pane_visit_again_header": "Kunjungi Lagi",
+    "settings_pane_visit_again_body": "Firefox akan menunjukkan bagian dari riwayat penjelajahan yang mungkin ingin Anda ingat atau kunjungi lagi.",
     "settings_pane_pocketstories_header": "Cerita Utama",
     "settings_pane_pocketstories_body": "Pocket, bagian dari keluarga Mozilla, akan membantu hubungkan Anda dengan konten berkualitas tinggi yang tak dapat Anda temukan di tempat lain.",
     "settings_pane_done_button": "Selesai",
     "edit_topsites_button_text": "Sunting",
     "edit_topsites_button_label": "Ubahsuai bagian Situs Teratas Anda",
     "edit_topsites_showmore_button": "Tampilkan lainnya",
     "edit_topsites_showless_button": "Tampilkan lebih sedikit",
     "edit_topsites_done_button": "Selesai",
@@ -2704,17 +2730,21 @@
     "topsites_form_add_button": "Tambah",
     "topsites_form_save_button": "Simpan",
     "topsites_form_cancel_button": "Batalkan",
     "topsites_form_url_validation": "URL valid diperlukan",
     "pocket_read_more": "Topik Populer:",
     "pocket_read_even_more": "Lihat Cerita Lainnya",
     "pocket_feedback_header": "Yang terbaik dari Web, dikurasi lebih dari 25 juta orang.",
     "pocket_feedback_body": "Pocket, bagian dari keluarga Mozilla, akan membantu hubungkan Anda dengan konten berkualitas tinggi yang tak dapat Anda temukan di tempat lain.",
-    "pocket_send_feedback": "Kirim Umpanbalik"
+    "pocket_send_feedback": "Kirim Umpanbalik",
+    "topstories_empty_state": "Maaf Anda tercegat. Periksa lagi nanti untuk lebih banyak cerita terbaik dari {provider}. Tidak mau menunggu? Pilih topik populer untuk menemukan lebih banyak cerita hebat dari seluruh web.",
+    "manual_migration_explanation": "Cobalah Firefox dengan situs dan markah kesukaan Anda dari peramban yang lain.",
+    "manual_migration_cancel_button": "Tidak, Terima kasih",
+    "manual_migration_import_button": "Impor Sekarang"
   },
   "is": {},
   "it": {
     "newtab_page_title": "Nuova scheda",
     "default_label_loading": "Caricamento…",
     "header_top_sites": "Siti principali",
     "header_stories": "Storie principali",
     "header_visit_again": "Visita di nuovo",
@@ -2917,17 +2947,17 @@
     "search_for_something_with": "{search_term} -ის ძიება:",
     "search_button": "ძიება",
     "search_header": "{search_engine_name} -ში ძიება",
     "search_web_placeholder": "ინტერნეტში ძიება",
     "search_settings": "ძიების პარამეტრების შეცვლა",
     "section_info_option": "ინფორმაცია",
     "welcome_title": "მოგესალმებით ახალ ჩანართზე",
     "welcome_body": "Firefox ამ სივრცეს გამოიყენებს თქვენთვის ყველაზე საჭირო სანიშნეების, სტატიების, ვიდეოებისა და ბოლოს მონახულებული გვერდებისთვის, რომ ადვილად შეძლოთ მათზე დაბრუნება.",
-    "welcome_label": "რჩეული ვებ-გვერდების დადგენა",
+    "welcome_label": "რჩეული ვებგვერდების დადგენა",
     "time_label_less_than_minute": "<1წთ",
     "time_label_minute": "{number}წთ",
     "time_label_hour": "{number}სთ",
     "time_label_day": "{number}დღე",
     "settings_pane_button_label": "მოირგეთ ახალი ჩანართის გვერდი",
     "settings_pane_header": "ახალი ჩანართის პარამეტრები",
     "settings_pane_body": "აირჩიეთ რისი ხილვა გსურთ ახალი ჩანართის გახსნისას.",
     "settings_pane_search_header": "ძიება",
@@ -3347,24 +3377,26 @@
   },
   "lt": {
     "newtab_page_title": "Nauja kortelė",
     "default_label_loading": "Įkeliama…",
     "header_top_sites": "Lankomiausios svetainės",
     "header_stories": "Populiariausi straipsniai",
     "header_visit_again": "Aplankykite vėl",
     "header_bookmarks": "Paskiausi adresyno įrašai",
+    "header_recommended_by": "Rekomendavo „{provider}“",
     "header_bookmarks_placeholder": "Jūs dar neturite adresyno įrašų.",
     "header_stories_from": "iš",
     "type_label_visited": "Aplankyti",
     "type_label_bookmarked": "Adresyne",
     "type_label_synced": "Sinchronizuoti iš kito įrenginio",
     "type_label_recommended": "Populiaru",
     "type_label_open": "Atviri",
     "type_label_topic": "Tema",
+    "type_label_now": "Dabar",
     "menu_action_bookmark": "Įrašyti į adresyną",
     "menu_action_remove_bookmark": "Pašalinti iš adresyno",
     "menu_action_copy_address": "Kopijuoti adresą",
     "menu_action_email_link": "Siųsti saitą el. paštu…",
     "menu_action_open_new_window": "Atverti naujame lange",
     "menu_action_open_private_window": "Atverti naujame privačiajame lange",
     "menu_action_dismiss": "Paslėpti",
     "menu_action_delete": "Pašalinti iš istorijos",
@@ -3373,16 +3405,17 @@
     "confirm_history_delete_p1": "Ar tikrai norite pašalinti visus šio tinklalapio įrašus iš savo naršymo žurnalo?",
     "confirm_history_delete_notice_p2": "Atlikus šį veiksmą, jo atšaukti neįmanoma.",
     "menu_action_save_to_pocket": "Įrašyti į „Pocket“",
     "search_for_something_with": "Ieškoti „{search_term}“ per:",
     "search_button": "Ieškoti",
     "search_header": "{search_engine_name} paieška",
     "search_web_placeholder": "Ieškokite saityne",
     "search_settings": "Keisti paieškos nuostatas",
+    "section_info_option": "Informacija",
     "welcome_title": "Sveiki, čia nauja kortelė",
     "welcome_body": "„Firefox“ naudos šią vietą jums aktualiausių adresyno įrašų, straipsnių, vaizdo įrašų bei neseniai lankytų tinklalapių rodymui, kad galėtumėte lengvai į juos sugrįžti.",
     "welcome_label": "Nustatomi jūsų akcentai",
     "time_label_less_than_minute": "<1 min.",
     "time_label_minute": "{number} min.",
     "time_label_hour": "{number} val.",
     "time_label_day": "{number} d.",
     "settings_pane_button_label": "Tinkinkite savo naujos kortelės puslapį",
@@ -3417,17 +3450,21 @@
     "topsites_form_add_button": "Pridėti",
     "topsites_form_save_button": "Įrašyti",
     "topsites_form_cancel_button": "Atsisakyti",
     "topsites_form_url_validation": "Reikalingas tinkamas URL",
     "pocket_read_more": "Populiarios temos:",
     "pocket_read_even_more": "Rodyti daugiau straipsnių",
     "pocket_feedback_header": "Geriausi dalykai internete, kuruojami daugiau nei 25 milijonų žmonių.",
     "pocket_feedback_body": "„Pocket“, „Mozillos“ šeimos dalis, padės jums atrasti kokybišką turinį, kurio kitaip gal nebūtumėte radę.",
-    "pocket_send_feedback": "Siųsti atsiliepimą"
+    "pocket_send_feedback": "Siųsti atsiliepimą",
+    "topstories_empty_state": "Viską perskaitėte. Užsukite vėliau, norėdami rasti daugiau gerų straipsnių iš „{provider}“. Nekantraujate? Pasirinkite populiarią temą, norėdami rasti daugiau puikių straipsnių saityne.",
+    "manual_migration_explanation": "Išbandykite „Firefox“ su mėgstamiausiomis svetainėmis bei adresyno įrašais iš kitos naršyklės.",
+    "manual_migration_cancel_button": "Ačiū, ne",
+    "manual_migration_import_button": "Importuoti dabar"
   },
   "ltg": {},
   "lv": {
     "newtab_page_title": "Jauna cilne"
   },
   "mai": {},
   "mk": {},
   "ml": {
@@ -3874,51 +3911,51 @@
     "topstories_empty_state": "U bent weer bij. Kijk later nog eens voor meer topverhalen van {provider}. Kunt u niet wachten? Selecteer een populair onderwerp voor meer geweldige verhalen van het hele web.",
     "manual_migration_explanation": "Probeer Firefox met uw favoriete websites en bladwijzers uit een andere browser.",
     "manual_migration_cancel_button": "Nee bedankt",
     "manual_migration_import_button": "Nu importeren"
   },
   "nn-NO": {
     "newtab_page_title": "Ny fane",
     "default_label_loading": "Lastar…",
-    "header_top_sites": "Mest vitja",
+    "header_top_sites": "Mest besøkte nettsider",
     "header_stories": "Hovudsakene",
-    "header_visit_again": "Bes;kigjen",
+    "header_visit_again": "Besøk igjen",
     "header_bookmarks": "Nylege bokmerke",
     "header_recommended_by": "Tilrådd av {provider}",
     "header_bookmarks_placeholder": "Du har ingen bokmerke enno.",
     "header_stories_from": "frå",
-    "type_label_visited": "Vitja",
+    "type_label_visited": "Besøkt",
     "type_label_bookmarked": "Bokmerkte",
     "type_label_synced": "Synkronisert frå ei anna eining",
     "type_label_recommended": "Trendar",
-    "type_label_open": "Opna",
+    "type_label_open": "Opne",
     "type_label_topic": "Emne",
     "type_label_now": "No",
     "menu_action_bookmark": "Bokmerke",
     "menu_action_remove_bookmark": "Fjern bokmerke",
     "menu_action_copy_address": "Kopier adresse",
     "menu_action_email_link": "E-postlenke…",
     "menu_action_open_new_window": "Opne i nytt vindauge",
-    "menu_action_open_private_window": "Opna i eit nytt privat vindauge",
+    "menu_action_open_private_window": "Opne i eit nytt privat vindauge",
     "menu_action_dismiss": "Avslå",
     "menu_action_delete": "Slett frå historikk",
     "menu_action_pin": "Fest",
     "menu_action_unpin": "L:ys",
     "confirm_history_delete_p1": "Er du sikker på at du vil slette alle førekomstar av denne sida frå historikken din?",
     "confirm_history_delete_notice_p2": "Denne handlinga kan ikkje angrast.",
     "menu_action_save_to_pocket": "Lagre til Pocket",
     "search_for_something_with": "Søk etter {search_term} med:",
     "search_button": "Søk",
     "search_header": "{search_engine_name}",
     "search_web_placeholder": "Søk på nettet",
     "search_settings": "Endra søkjeinnstillingar",
     "section_info_option": "Info",
     "welcome_title": "Velkomen til ny fane",
-    "welcome_body": "Firefox vil bruka denne plassen til å visa deg dei mest relevante bokmerka, artiklane, videoane og sidene du nettopp har vitja, slik at du enkelt kan finna tilbake til dei.",
+    "welcome_body": "Firefox vil bruke denne plassen til å vise deg dei mest relevante bokmerka, artiklane, videoane og sidene du nettopp har vitja, slik at du enkelt kan finne tilbake til dei.",
     "welcome_label": "Identifiserer høgdepunkta dine",
     "time_label_less_than_minute": "<1 min.",
     "time_label_minute": "{number} m",
     "time_label_hour": "{number} t",
     "time_label_day": "{number} d",
     "settings_pane_button_label": "Tilpass sida for Ny fane",
     "settings_pane_header": "Innstillingar for Ny fane",
     "settings_pane_body": "Vel kva som skal visast når du opnar ei ny fane.",
@@ -3939,18 +3976,18 @@
     "edit_topsites_showmore_button": "Vis meir",
     "edit_topsites_showless_button": "Vis mindre",
     "edit_topsites_done_button": "Ferdig",
     "edit_topsites_pin_button": "Fest sida",
     "edit_topsites_unpin_button": "Løys frå denne nettsida",
     "edit_topsites_edit_button": "Rediger denne nettsida",
     "edit_topsites_dismiss_button": "Avvis denne nettsida",
     "edit_topsites_add_button": "Legg til",
-    "topsites_form_add_header": "Ny toppstad",
-    "topsites_form_edit_header": "Rediger mest vitja",
+    "topsites_form_add_header": "Ny Mest besøkt",
+    "topsites_form_edit_header": "Rediger Mest besøkt",
     "topsites_form_title_placeholder": "Skriv inn ein tittel",
     "topsites_form_url_placeholder": "Skriv eller lim inn ein URL",
     "topsites_form_add_button": "Legg til",
     "topsites_form_save_button": "Lagre",
     "topsites_form_cancel_button": "Avbryt",
     "topsites_form_url_validation": "Gyldig URL er påkravd",
     "pocket_read_more": "Populære emne:",
     "pocket_read_even_more": "Vis fleire saker",
@@ -4270,56 +4307,67 @@
     "manual_migration_explanation": "Experimente o Firefox com os seus sites favoritos e marcadores de outro navegador.",
     "manual_migration_cancel_button": "Não obrigado",
     "manual_migration_import_button": "Importar agora"
   },
   "rm": {
     "newtab_page_title": "Nov tab",
     "default_label_loading": "Chargiar…",
     "header_top_sites": "Paginas preferidas",
-    "header_highlights": "Accents",
     "header_stories": "Artitgels populars",
+    "header_visit_again": "Turnar a visitar",
+    "header_bookmarks": "Segnapaginas novs",
+    "header_recommended_by": "Recumandà da {provider}",
+    "header_bookmarks_placeholder": "Ti n'has anc nagins segnapaginas.",
     "header_stories_from": "da",
     "type_label_visited": "Visità",
     "type_label_bookmarked": "Cun segnapagina",
     "type_label_synced": "Sincronisà dad auters apparats",
     "type_label_recommended": "Popular",
     "type_label_open": "Avert",
     "type_label_topic": "Tema",
+    "type_label_now": "Ussa",
     "menu_action_bookmark": "Marcar sco segnapagina",
     "menu_action_remove_bookmark": "Allontanar il segnapagina",
     "menu_action_copy_address": "Copiar l'adressa",
     "menu_action_email_link": "Trametter la colliaziun per e-mail…",
     "menu_action_open_new_window": "Avrir en ina nova fanestra",
     "menu_action_open_private_window": "Avrir en ina nova fanestra privata",
     "menu_action_dismiss": "Serrar",
     "menu_action_delete": "Stizzar da la cronologia",
+    "menu_action_pin": "Fixar",
+    "menu_action_unpin": "Betg pli fixar",
+    "confirm_history_delete_p1": "Vuls ti propi stizzar mintga instanza da questa pagina ord la cronologia?",
+    "confirm_history_delete_notice_p2": "Questa acziun na po betg vegnir revocada.",
     "menu_action_save_to_pocket": "Memorisar en Pocket",
     "search_for_something_with": "Tschertgar {search_term} cun:",
     "search_button": "Tschertgar",
     "search_header": "Tschertga da {search_engine_name}",
     "search_web_placeholder": "Tschertgar en il Web",
     "search_settings": "Midar las preferenzas per tschertgar",
+    "section_info_option": "Info",
     "welcome_title": "Bainvegni sin in nov tab",
     "welcome_body": "Firefox utilisescha quest plaz per ta mussar ils segnapaginas, ils artitgels, ils videos e las paginas las pli relevantas che ti has visità dacurt, uschè che ti pos turnar a moda simpla tar quellas.",
     "welcome_label": "Identifitgar tes accents",
     "time_label_less_than_minute": "< 1 min",
     "time_label_minute": "{number} min",
     "time_label_hour": "{number} uras",
     "time_label_day": "{number} dis",
     "settings_pane_button_label": "Persunalisar tia pagina per novs tabs",
     "settings_pane_header": "Preferenzas per novs tabs",
     "settings_pane_body": "Tscherna tge che ti vesas sche ti avras in nov tab.",
     "settings_pane_search_header": "Tschertgar",
     "settings_pane_search_body": "Tschertgar en l'internet da tes nov tab.",
     "settings_pane_topsites_header": "Paginas preferidas",
     "settings_pane_topsites_body": "Acceder las websites che ti visitas il pli savens.",
     "settings_pane_topsites_options_showmore": "Mussar duas colonnas",
-    "settings_pane_highlights_header": "Accents",
-    "settings_pane_highlights_body": "Dar in sguard enavos sin websites visitadas dacurt e sin segnapaginas creads dacurt.",
+    "settings_pane_bookmarks_header": "Novs segnapaginas",
+    "settings_pane_bookmarks_body": "Tes novs segnapaginas en in lieu pratic.",
+    "settings_pane_visit_again_header": "Turnar a visitar",
+    "settings_pane_visit_again_body": "Firefox ta mussa parts da tia cronologia da navigaziun che pudessan esser interessantas per turnar.",
     "settings_pane_pocketstories_header": "Artitgels populars",
     "settings_pane_pocketstories_body": "Pocket che fa part da Mozilla ta gida da scuvrir cuntegn dad auta qualitad che ti avessas uschiglio forsa manchentà.",
     "settings_pane_done_button": "Finì",
     "edit_topsites_button_text": "Modifitgar",
     "edit_topsites_button_label": "Persunalisar la secziun da paginas preferidas",
     "edit_topsites_showmore_button": "Mussar dapli",
     "edit_topsites_showless_button": "Mussar pli pauc",
     "edit_topsites_done_button": "Finì",
@@ -4335,17 +4383,21 @@
     "topsites_form_add_button": "Agiuntar",
     "topsites_form_save_button": "Memorisar",
     "topsites_form_cancel_button": "Interrumper",
     "topsites_form_url_validation": "In URL valid è necessari",
     "pocket_read_more": "Temas populars:",
     "pocket_read_even_more": "Mussar dapli artitgels",
     "pocket_feedback_header": "Il meglier ord il web, selecziunà da dapli che 25 milliuns umans.",
     "pocket_feedback_body": "Pocket che fa part da Mozilla ta gida da scuvrir cuntegn dad auta qualitad che ti avessas uschiglio forsa manchentà.",
-    "pocket_send_feedback": "Trametter in resun"
+    "pocket_send_feedback": "Trametter in resun",
+    "topstories_empty_state": "Ussa has ti legì tut las novitads. Turna pli tard per ulteriuras novitads da {provider}. Na pos betg spetgar? Tscherna in tema popular per chattar ulteriuras istorgias ord il web.",
+    "manual_migration_explanation": "Utilisescha Firefox cun tias paginas preferidas e tes segnapaginas d'in auter navigatur.",
+    "manual_migration_cancel_button": "Na, grazia",
+    "manual_migration_import_button": "Importar ussa"
   },
   "ro": {
     "newtab_page_title": "Filă nouă",
     "default_label_loading": "Se încarcă…",
     "header_top_sites": "Site-uri de top",
     "header_highlights": "Evidențieri",
     "header_stories_from": "de la",
     "type_label_visited": "Vizitate",
@@ -4406,24 +4458,26 @@
   },
   "ru": {
     "newtab_page_title": "Новая вкладка",
     "default_label_loading": "Загрузка…",
     "header_top_sites": "Топ сайтов",
     "header_stories": "Топ статей",
     "header_visit_again": "Посетить снова",
     "header_bookmarks": "Недавние закладки",
+    "header_recommended_by": "Рекомендовано {provider}",
     "header_bookmarks_placeholder": "У вас ещё нет каких-либо закладок.",
     "header_stories_from": "от",
     "type_label_visited": "Посещено",
     "type_label_bookmarked": "В закладках",
     "type_label_synced": "Синхронизировано с другого устройства",
     "type_label_recommended": "Популярные",
     "type_label_open": "Открыта",
     "type_label_topic": "Тема",
+    "type_label_now": "Сейчас",
     "menu_action_bookmark": "Добавить в закладки",
     "menu_action_remove_bookmark": "Удалить закладку",
     "menu_action_copy_address": "Скопировать ссылку",
     "menu_action_email_link": "Отправить ссылку…",
     "menu_action_open_new_window": "Открыть в новом окне",
     "menu_action_open_private_window": "Открыть в новом приватном окне",
     "menu_action_dismiss": "Скрыть",
     "menu_action_delete": "Удалить из истории",
@@ -4432,16 +4486,17 @@
     "confirm_history_delete_p1": "Вы действительно хотите удалить все записи об этой странице из вашей истории?",
     "confirm_history_delete_notice_p2": "Это действие не может быть отменено.",
     "menu_action_save_to_pocket": "Сохранить в Pocket",
     "search_for_something_with": "Искать {search_term} в:",
     "search_button": "Искать",
     "search_header": "Искать в {search_engine_name}",
     "search_web_placeholder": "Искать в Интернете",
     "search_settings": "Изменить настройки поиска",
+    "section_info_option": "Информация",
     "welcome_title": "Добро пожаловать на новую вкладку",
     "welcome_body": "Firefox будет использовать это место, чтобы отображать самые актуальные закладки, статьи, видео и страницы, которые вы недавно посетили, чтобы вы смогли легко попасть на них снова.",
     "welcome_label": "Определение вашего избранного",
     "time_label_less_than_minute": "<1 мин.",
     "time_label_minute": "{number} мин.",
     "time_label_hour": "{number} ч.",
     "time_label_day": "{number} д.",
     "settings_pane_button_label": "Настроить свою страницу новой вкладки",
@@ -4476,17 +4531,21 @@
     "topsites_form_add_button": "Добавить",
     "topsites_form_save_button": "Сохранить",
     "topsites_form_cancel_button": "Отмена",
     "topsites_form_url_validation": "Введите корректный URL",
     "pocket_read_more": "Популярные темы:",
     "pocket_read_even_more": "Больше статей",
     "pocket_feedback_header": "Лучшее из Интернета, отобранное более чем 25 миллионами людей.",
     "pocket_feedback_body": "Pocket, часть семьи Mozilla, поможет подключить вас к высококачественному контенту, который вы можете иначе и не найти.",
-    "pocket_send_feedback": "Отправить отзыв"
+    "pocket_send_feedback": "Отправить отзыв",
+    "topstories_empty_state": "Вы всё прочитали. Зайдите попозже, чтобы увидеть больше лучших статей от {provider}. Не можете ждать? Выберите популярную тему, чтобы найти больше интересных статей со всего Интернета.",
+    "manual_migration_explanation": "Попробуйте Firefox со своими любимыми сайтами и закладками из другого браузера.",
+    "manual_migration_cancel_button": "Нет, спасибо",
+    "manual_migration_import_button": "Импортировать сейчас"
   },
   "si": {},
   "sk": {
     "newtab_page_title": "Nová karta",
     "default_label_loading": "Načítava sa…",
     "header_top_sites": "Top stránky",
     "header_stories": "Top príbehy",
     "header_visit_again": "Navštívte znova",
@@ -4646,16 +4705,17 @@
     "topsites_form_save_button": "Shrani",
     "topsites_form_cancel_button": "Prekliči",
     "topsites_form_url_validation": "Vnesite veljaven URL",
     "pocket_read_more": "Priljubljene teme:",
     "pocket_read_even_more": "Prikaži več vesti",
     "pocket_feedback_header": "Najboljše s spleta, kar je izbralo več kot 25 milijonov ljudi.",
     "pocket_feedback_body": "Pocket, del Mozilline družine, vam bo pomagal pridobiti visokokakovostne vsebine, ki jih sicer ne bi našli.",
     "pocket_send_feedback": "Pošlji povratne informacije",
+    "topstories_empty_state": "Zdaj ste seznanjeni z novicami. Vrnite se pozneje in si oglejte nove prispevke iz {provider}. Komaj čakate? Izberite priljubljeno temo in odkrijte več velikih zgodb na spletu.",
     "manual_migration_explanation": "Preskusite Firefox s svojimi priljubljenimi stranmi in zaznamki iz drugih brskalnikov.",
     "manual_migration_cancel_button": "Ne, hvala",
     "manual_migration_import_button": "Uvozi zdaj"
   },
   "son": {},
   "sq": {
     "newtab_page_title": "Skedë e Re",
     "default_label_loading": "Po ngarkohet…",
@@ -5416,16 +5476,17 @@
   "uz": {},
   "vi": {
     "newtab_page_title": "Tab mới",
     "default_label_loading": "Đang tải…",
     "header_top_sites": "Trang web hàng đầu",
     "header_stories": "Câu chuyện hàng đầu",
     "header_visit_again": "Truy cập lại",
     "header_bookmarks": "Các bookmark gần đây",
+    "header_recommended_by": "Được đề nghị bởi {provider}",
     "header_bookmarks_placeholder": "Bạn chưa có bookmark nào.",
     "header_stories_from": "từ",
     "type_label_visited": "Đã truy cập",
     "type_label_bookmarked": "Đã được đánh dấu",
     "type_label_synced": "Đồng bộ từ thiết bị khác",
     "type_label_recommended": "Xu hướng",
     "type_label_open": "Mở",
     "type_label_topic": "Chủ đề",
@@ -5439,20 +5500,43 @@
     "menu_action_delete": "Xóa từ lịch xử",
     "menu_action_pin": "Ghim",
     "menu_action_unpin": "Bỏ ghim",
     "confirm_history_delete_notice_p2": "Hành động này không thể hoàn tác.",
     "menu_action_save_to_pocket": "Lưu vào Pocket",
     "search_for_something_with": "Tìm {search_term} với:",
     "search_button": "Tìm kiếm",
     "search_header": "Công cụ tìm kiếm {search_engine_name}",
+    "search_web_placeholder": "Tìm trên mạng",
+    "search_settings": "Thay đổi thiết lập tìm kiếm",
+    "section_info_option": "Thông tin",
+    "welcome_title": "Chào mừng đến với tab mới",
     "time_label_less_than_minute": "<1phút",
     "time_label_minute": "{number}phút",
     "time_label_hour": "{number}giờ",
-    "settings_pane_search_header": "Tìm kiếm"
+    "time_label_day": "{number}ngày",
+    "settings_pane_header": "Tùy chỉnh cho tab mới",
+    "settings_pane_body": "Chọn cái bạn muốn tải khi một tab mới được mở ra.",
+    "settings_pane_search_header": "Tìm kiếm",
+    "settings_pane_done_button": "Xong",
+    "edit_topsites_button_text": "Chỉnh sửa",
+    "edit_topsites_showmore_button": "Xem thêm",
+    "edit_topsites_showless_button": "Hiển thị ngắn gọn lại",
+    "edit_topsites_done_button": "Xong",
+    "edit_topsites_pin_button": "Ghim trang này",
+    "edit_topsites_unpin_button": "Bỏ ghim trang này",
+    "edit_topsites_edit_button": "Chỉnh sửa trang web này",
+    "edit_topsites_dismiss_button": "Bỏ qua trang này",
+    "edit_topsites_add_button": "Thêm",
+    "topsites_form_add_button": "Thêm",
+    "topsites_form_save_button": "Lưu lại",
+    "topsites_form_cancel_button": "Hủy bỏ",
+    "pocket_read_more": "Các chủ đề phổ biến:",
+    "pocket_send_feedback": "Gửi phản hồi",
+    "manual_migration_cancel_button": "Không, cảm ơn"
   },
   "wo": {},
   "xh": {},
   "zh-CN": {
     "newtab_page_title": "新标签页",
     "default_label_loading": "正在载入…",
     "header_top_sites": "常用网站",
     "header_stories": "热门报道",
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -3,58 +3,80 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/Services.jsm");
 
 // NB: Eagerly load modules that will be loaded/constructed/initialized in the
 // common case to avoid the overhead of wrapping and detecting lazy loading.
-const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 const {DefaultPrefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
 const {LocalizationFeed} = Cu.import("resource://activity-stream/lib/LocalizationFeed.jsm", {});
 const {ManualMigration} = Cu.import("resource://activity-stream/lib/ManualMigration.jsm", {});
 const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {});
 const {PlacesFeed} = Cu.import("resource://activity-stream/lib/PlacesFeed.jsm", {});
 const {PrefsFeed} = Cu.import("resource://activity-stream/lib/PrefsFeed.jsm", {});
 const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
 const {SnippetsFeed} = Cu.import("resource://activity-stream/lib/SnippetsFeed.jsm", {});
 const {SystemTickFeed} = Cu.import("resource://activity-stream/lib/SystemTickFeed.jsm", {});
 const {TelemetryFeed} = Cu.import("resource://activity-stream/lib/TelemetryFeed.jsm", {});
 const {TopSitesFeed} = Cu.import("resource://activity-stream/lib/TopSitesFeed.jsm", {});
 const {TopStoriesFeed} = Cu.import("resource://activity-stream/lib/TopStoriesFeed.jsm", {});
 
+const DEFAULT_SITES = new Map([
+  // This first item is the global list fallback for any unexpected geos
+  ["", "https://www.youtube.com/,https://www.facebook.com/,https://www.wikipedia.org/,https://www.reddit.com/,https://www.amazon.com/,https://twitter.com/"],
+  ["US", "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"],
+  ["CA", "https://www.youtube.com/,https://www.facebook.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://www.amazon.ca/,https://twitter.com/"],
+  ["DE", "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.de/,https://www.ebay.de/,https://www.wikipedia.org/,https://www.reddit.com/"],
+  ["PL", "https://www.youtube.com/,https://www.facebook.com/,https://allegro.pl/,https://www.wikipedia.org/,https://www.olx.pl/,https://www.wykop.pl/"],
+  ["RU", "https://vk.com/,https://www.youtube.com/,https://ok.ru/,https://www.avito.ru/,https://www.aliexpress.com/,https://www.wikipedia.org/"],
+  ["GB", "https://www.youtube.com/,https://www.facebook.com/,https://www.reddit.com/,https://www.amazon.co.uk/,https://www.bbc.co.uk/,https://www.ebay.co.uk/"],
+  ["FR", "https://www.youtube.com/,https://www.facebook.com/,https://www.wikipedia.org/,https://www.amazon.fr/,https://www.leboncoin.fr/,https://twitter.com/"]
+]);
+const GEO_PREF = "browser.search.region";
 const REASON_ADDON_UNINSTALL = 6;
 
-// For now, we only want to show top stories by default to the following locales
-const showTopStoriesByDefault = ["en-US", "en-CA"].includes(Services.locale.getRequestedLocale());
-// Sections, keyed by section id
-const SECTIONS = new Map([
-  ["topstories", {
-    feed: TopStoriesFeed,
-    prefTitle: "Fetches content recommendations from a configurable content provider",
-    showByDefault: showTopStoriesByDefault
-  }]
-]);
-
-const SECTION_FEEDS_CONFIG = Array.from(SECTIONS.entries()).map(entry => {
-  const id = entry[0];
-  const {feed: Feed, prefTitle, showByDefault: value} = entry[1];
-  return {
-    name: `section.${id}`,
-    factory: () => new Feed(),
-    title: prefTitle || `${id} section feed`,
-    value
-  };
-});
-
+// Configure default Activity Stream prefs with a plain `value` or a `getValue`
+// that computes a value. A `value_local_dev` is used for development defaults.
 const PREFS_CONFIG = new Map([
   ["default.sites", {
     title: "Comma-separated list of default top sites to fill in behind visited sites",
-    value: "https://www.facebook.com/,https://www.youtube.com/,https://www.amazon.com/,https://www.yahoo.com/,https://www.ebay.com/,https://twitter.com/"
+    getValue: ({geo}) => DEFAULT_SITES.get(DEFAULT_SITES.has(geo) ? geo : "")
+  }],
+  ["feeds.section.topstories.options", {
+    title: "Configuration options for top stories feed",
+    // This is a dynamic pref as it depends on the feed being shown or not
+    getValue: args => JSON.stringify({
+      api_key_pref: "extensions.pocket.oAuthConsumerKey",
+      // Use the opposite value as what default value the feed would have used
+      hidden: !PREFS_CONFIG.get("feeds.section.topstories").getValue(args),
+      learn_more_endpoint: "https://getpocket.com/firefox_learnmore?src=ff_newtab",
+      provider_description: "pocket_feedback_body",
+      provider_icon: "pocket",
+      provider_name: "Pocket",
+      read_more_endpoint: "https://getpocket.com/explore/trending?src=ff_new_tab",
+      stories_endpoint: `https://getpocket.com/v3/firefox/global-recs?consumer_key=$apiKey&locale_lang=${args.locale}`,
+      stories_referrer: "https://getpocket.com/recommendations",
+      survey_link: "https://www.surveymonkey.com/r/newtabffx",
+      topics_endpoint: `https://getpocket.com/v3/firefox/trending-topics?consumer_key=$apiKey&locale_lang=${args.locale}`
+    })
+  }],
+  ["migrationExpired", {
+    title: "Boolean flag that decides whether to show the migration message or not.",
+    value: false
+  }],
+  ["migrationLastShownDate", {
+    title: "Timestamp when migration message was last shown. In seconds.",
+    value: 0
+  }],
+  ["migrationRemainingDays", {
+    title: "Number of days to show the manual migration message",
+    value: 4
   }],
   ["showSearch", {
     title: "Show the Search bar on the New Tab page",
     value: true
   }],
   ["showTopSites", {
     title: "Show the Top Sites section on the New Tab page",
     value: true
@@ -67,56 +89,34 @@ const PREFS_CONFIG = new Map([
   ["telemetry.log", {
     title: "Log telemetry events in the console",
     value: false,
     value_local_dev: true
   }],
   ["telemetry.ping.endpoint", {
     title: "Telemetry server endpoint",
     value: "https://tiles.services.mozilla.com/v4/links/activity-stream"
-  }],
-  ["feeds.section.topstories.options", {
-    title: "Configuration options for top stories feed",
-    value: `{
-      "stories_endpoint": "https://getpocket.com/v3/firefox/global-recs?consumer_key=$apiKey&locale_lang=$locale",
-      "stories_referrer": "https://getpocket.com/recommendations",
-      "topics_endpoint": "https://getpocket.com/v3/firefox/trending-topics?consumer_key=$apiKey&locale_lang=$locale",
-      "read_more_endpoint": "https://getpocket.com/explore/trending?src=ff_new_tab",
-      "learn_more_endpoint": "https://getpocket.com/firefox_learnmore?src=ff_newtab",
-      "survey_link": "https://www.surveymonkey.com/r/newtabffx",
-      "api_key_pref": "extensions.pocket.oAuthConsumerKey",
-      "provider_name": "Pocket",
-      "provider_icon": "pocket",
-      "provider_description": "pocket_feedback_body",
-      "hidden": ${!showTopStoriesByDefault}
-    }`
-  }],
-  ["migrationExpired", {
-    title: "Boolean flag that decides whether to show the migration message or not.",
-    value: false
-  }],
-  ["migrationRemainingDays", {
-    title: "Number of days to show the manual migration message",
-    value: 4
-  }],
-  ["migrationLastShownDate", {
-    title: "Timestamp when migration message was last shown. In seconds.",
-    value: 0
   }]
 ]);
 
-const FEEDS_CONFIG = new Map();
-for (const {name, factory, title, value} of SECTION_FEEDS_CONFIG.concat([
+// Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
+const FEEDS_DATA = [
   {
     name: "localization",
     factory: () => new LocalizationFeed(),
     title: "Initialize strings and detect locale for Activity Stream",
     value: true
   },
   {
+    name: "migration",
+    factory: () => new ManualMigration(),
+    title: "Manual migration wizard",
+    value: true
+  },
+  {
     name: "newtabinit",
     factory: () => new NewTabInit(),
     title: "Sends a copy of the state to each new tab that is opened",
     value: true
   },
   {
     name: "places",
     factory: () => new PlacesFeed(),
@@ -125,20 +125,34 @@ for (const {name, factory, title, value}
   },
   {
     name: "prefs",
     factory: () => new PrefsFeed(PREFS_CONFIG),
     title: "Preferences",
     value: true
   },
   {
+    name: "section.topstories",
+    factory: () => new TopStoriesFeed(),
+    title: "Fetches content recommendations from a configurable content provider",
+    // Dynamically determine if Pocket should be shown for a geo / locale
+    getValue: ({geo, locale}) => {
+      const locales = ({
+        "US": ["en-US", "en-GB", "en-ZA"],
+        "CA": ["en-US", "en-GB", "en-ZA"],
+        "DE": ["de", "de-DE", "de-AT", "de-CH"]
+      })[geo];
+      return !!locales && locales.includes(locale);
+    }
+  },
+  {
     name: "snippets",
     factory: () => new SnippetsFeed(),
     title: "Gets snippets data",
-    value: false
+    value: true
   },
   {
     name: "systemtick",
     factory: () => new SystemTickFeed(),
     title: "Produces system tick events to periodically check for data expiry",
     value: true
   },
   {
@@ -147,27 +161,24 @@ for (const {name, factory, title, value}
     title: "Relays telemetry-related actions to TelemetrySender",
     value: true
   },
   {
     name: "topsites",
     factory: () => new TopSitesFeed(),
     title: "Queries places and gets metadata for Top Sites section",
     value: true
-  },
-  {
-    name: "migration",
-    factory: () => new ManualMigration(),
-    title: "Manual migration wizard",
-    value: true
   }
-])) {
-  const pref = `feeds.${name}`;
-  FEEDS_CONFIG.set(pref, factory);
-  PREFS_CONFIG.set(pref, {title, value});
+];
+
+const FEEDS_CONFIG = new Map();
+for (const config of FEEDS_DATA) {
+  const pref = `feeds.${config.name}`;
+  FEEDS_CONFIG.set(pref, config.factory);
+  PREFS_CONFIG.set(pref, config);
 }
 
 this.ActivityStream = class ActivityStream {
 
   /**
    * constructor - Initializes an instance of ActivityStream
    *
    * @param  {object} options Options for the ActivityStream instance
@@ -178,34 +189,85 @@ this.ActivityStream = class ActivityStre
   constructor(options = {}) {
     this.initialized = false;
     this.options = options;
     this.store = new Store();
     this.feeds = FEEDS_CONFIG;
     this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
   }
   init() {
+    this._updateDynamicPrefs();
     this._defaultPrefs.init();
+
+    // Hook up the store and let all feeds and pages initialize
     this.store.init(this.feeds);
-    this.store.dispatch({
+    this.store.dispatch(ac.BroadcastToContent({
       type: at.INIT,
       data: {version: this.options.version}
-    });
+    }));
+
     this.initialized = true;
   }
   uninit() {
+    if (this.geo === "") {
+      Services.prefs.removeObserver(GEO_PREF, this);
+    }
+
     this.store.dispatch({type: at.UNINIT});
     this.store.uninit();
 
     this.initialized = false;
   }
   uninstall(reason) {
     if (reason === REASON_ADDON_UNINSTALL) {
       // This resets all prefs in the config to their default values,
       // so we DON'T want to do this on an upgrade/downgrade, only on a
       // real uninstall
       this._defaultPrefs.reset();
     }
   }
+  _updateDynamicPrefs() {
+    // Save the geo pref if we have it
+    if (Services.prefs.prefHasUserValue(GEO_PREF)) {
+      this.geo = Services.prefs.getStringPref(GEO_PREF);
+    } else if (this.geo !== "") {
+      // Watch for geo changes and use a dummy value for now
+      Services.prefs.addObserver(GEO_PREF, this);
+      this.geo = "";
+    }
+
+    this.locale = Services.locale.getRequestedLocale();
+
+    // Update the pref config of those with dynamic values
+    for (const pref of PREFS_CONFIG.keys()) {
+      const prefConfig = PREFS_CONFIG.get(pref);
+      if (!prefConfig.getValue) {
+        continue;
+      }
+
+      const newValue = prefConfig.getValue({
+        geo: this.geo,
+        locale: this.locale
+      });
+
+      // If there's an existing value and it has changed, that means we need to
+      // overwrite the default with the new value.
+      if (prefConfig.value !== undefined && prefConfig.value !== newValue) {
+        this._defaultPrefs.setDefaultPref(pref, newValue);
+      }
+
+      prefConfig.value = newValue;
+    }
+  }
+  observe(subject, topic, data) {
+    switch (topic) {
+      case "nsPref:changed":
+        // We should only expect one geo change, so update and stop observing
+        if (data === GEO_PREF) {
+          this._updateDynamicPrefs();
+          Services.prefs.removeObserver(GEO_PREF, this);
+        }
+        break;
+    }
+  }
 };
 
-this.PREFS_CONFIG = PREFS_CONFIG;
-this.EXPORTED_SYMBOLS = ["ActivityStream", "SECTIONS"];
+this.EXPORTED_SYMBOLS = ["ActivityStream", "PREFS_CONFIG"];
--- a/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -121,36 +121,39 @@ this.ActivityStreamMessageChannel = clas
     return null;
   }
 
   /**
    * createChannel - Create RemotePages channel to establishing message passing
    *                 between the main process and child pages
    */
   createChannel() {
-    //  RemotePageManager must be disabled for about:newtab, since only one can exist at once
-    if (this.pageURL === ABOUT_NEW_TAB_URL) {
-      AboutNewTab.override();
-    }
-    this.channel = new RemotePages(this.pageURL);
+    //  Receive AboutNewTab's Remote Pages instance, if it exists, on override
+    const channel = this.pageURL === ABOUT_NEW_TAB_URL && AboutNewTab.override(true);
+    this.channel = channel || new RemotePages(this.pageURL);
     this.channel.addMessageListener("RemotePage:Init", this.onNewTabInit);
     this.channel.addMessageListener("RemotePage:Load", this.onNewTabLoad);
     this.channel.addMessageListener("RemotePage:Unload", this.onNewTabUnload);
     this.channel.addMessageListener(this.incomingMessageName, this.onMessage);
   }
 
   /**
    * destroyChannel - Destroys the RemotePages channel
    */
   destroyChannel() {
-    this.channel.destroy();
-    this.channel = null;
+    this.channel.removeMessageListener("RemotePage:Init", this.onNewTabInit);
+    this.channel.removeMessageListener("RemotePage:Load", this.onNewTabLoad);
+    this.channel.removeMessageListener("RemotePage:Unload", this.onNewTabUnload);
+    this.channel.removeMessageListener(this.incomingMessageName, this.onMessage);
     if (this.pageURL === ABOUT_NEW_TAB_URL) {
-      AboutNewTab.reset();
+      AboutNewTab.reset(this.channel);
+    } else {
+      this.channel.destroy();
     }
+    this.channel = null;
   }
 
 /**
  * onNewTabInit - Handler for special RemotePage:Init message fired
  * by RemotePages
  *
  * @param  {obj} msg The messsage from a page that was just initialized
  */
--- a/browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
@@ -49,22 +49,22 @@ this.DefaultPrefs = class DefaultPrefs {
    * @param  {string} branch (optional) The pref branch (defaults to ACTIVITY_STREAM_PREF_BRANCH)
    */
   constructor(config, branch = ACTIVITY_STREAM_PREF_BRANCH) {
     this._config = config;
     this.branch = Services.prefs.getDefaultBranch(branch);
   }
 
   /**
-   * _setDefaultPref - Sets the default value (not user-defined) for a given pref
+   * setDefaultPref - Sets the default value (not user-defined) for a given pref
    *
    * @param  {string} key The name of the pref
    * @param  {type} val The default value of the pref
    */
-  _setDefaultPref(key, val) {
+  setDefaultPref(key, val) {
     switch (typeof val) {
       case "boolean":
         this.branch.setBoolPref(key, val);
         break;
       case "number":
         this.branch.setIntPref(key, val);
         break;
       case "string":
@@ -84,17 +84,17 @@ this.DefaultPrefs = class DefaultPrefs {
     for (const pref of this._config.keys()) {
       const prefConfig = this._config.get(pref);
       let value;
       if (IS_UNOFFICIAL_BUILD && "value_local_dev" in prefConfig) {
         value = prefConfig.value_local_dev;
       } else {
         value = prefConfig.value;
       }
-      this._setDefaultPref(pref, value);
+      this.setDefaultPref(pref, value);
     }
   }
 
   /**
    * reset - Resets all user-defined prefs for prefs in ._config to their defaults
    */
   reset() {
     for (const name of this._config.keys()) {
--- a/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
+++ b/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
@@ -25,17 +25,24 @@ this.LocalizationFeed = class Localizati
 
     this.updateLocale();
   }
   uninit() {
     Services.obs.removeObserver(this, LOCALES_CHANGE_TOPIC);
   }
 
   updateLocale() {
-    let locale = Services.locale.getRequestedLocale() || DEFAULT_LOCALE;
+    // Just take the first element in the result array, as it should be
+    // the best locale
+    let locale = Services.locale.negotiateLanguages(
+      Services.locale.getAppLocalesAsLangTags(), // desired locales
+      Object.keys(this.allStrings), // available locales
+      DEFAULT_LOCALE // fallback
+    )[0];
+
     let strings = this.allStrings[locale];
 
     // Use the default strings for any that are missing
     if (locale !== DEFAULT_LOCALE) {
       strings = Object.assign({}, this.allStrings[DEFAULT_LOCALE], strings || {});
     }
 
     this.store.dispatch(ac.BroadcastToContent({
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -220,20 +220,22 @@ class PlacesFeed {
         break;
       case at.OPEN_PRIVATE_WINDOW:
         this.openNewWindow(action, true);
         break;
       case at.SAVE_TO_POCKET:
         Pocket.savePage(action._target.browser, action.data.site.url, action.data.site.title);
         break;
       case at.OPEN_LINK: {
+        const win = action._target.browser.ownerGlobal;
+        const where = win.whereToOpenLink(action.data.event);
         if (action.data.referrer) {
-          action._target.browser.loadURI(action.data.url, Services.io.newURI(action.data.referrer));
+          win.openLinkIn(action.data.url, where, {referrerURI: Services.io.newURI(action.data.referrer)});
         } else {
-          action._target.browser.loadURI(action.data.url);
+          win.openLinkIn(action.data.url, where);
         }
         break;
       }
     }
   }
 }
 
 this.PlacesFeed = PlacesFeed;
--- a/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
@@ -9,18 +9,18 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
   "resource://gre/modules/ProfileAge.jsm");
 
 // Url to fetch snippets, in the urlFormatter service format.
 const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
-const TELEMETRY_PREF = "datareporting.healthreport.uploadEnabled";
 const ONBOARDING_FINISHED_PREF = "browser.onboarding.notification.finished";
+const FXA_USERNAME_PREF = "services.sync.username";
 
 // Should be bumped up if the snippets content format changes.
 const STARTPAGE_VERSION = 5;
 
 const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
 
 this.SnippetsFeed = class SnippetsFeed {
   constructor() {
@@ -43,39 +43,49 @@ this.SnippetsFeed = class SnippetsFeed {
   }
   async _refresh() {
     const profileInfo = await this.getProfileInfo();
     const data = {
       snippetsURL: this.snippetsURL,
       version: STARTPAGE_VERSION,
       profileCreatedWeeksAgo: profileInfo.createdWeeksAgo,
       profileResetWeeksAgo: profileInfo.resetWeeksAgo,
-      telemetryEnabled: Services.prefs.getBoolPref(TELEMETRY_PREF),
-      onboardingFinished: Services.prefs.getBoolPref(ONBOARDING_FINISHED_PREF)
+      telemetryEnabled: Services.telemetry.canRecordBase,
+      onboardingFinished: Services.prefs.getBoolPref(ONBOARDING_FINISHED_PREF),
+      fxaccount: Services.prefs.prefHasUserValue(FXA_USERNAME_PREF)
     };
 
     this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_DATA, data}));
   }
+  _refreshCanRecordBase() {
+    // TODO: There is currently no way to listen for changes to this value, so
+    // we are just refreshing it on every new tab instead. A bug is filed
+    // here to fix this: https://bugzilla.mozilla.org/show_bug.cgi?id=1386318
+    this.store.dispatch({type: at.SNIPPETS_DATA, data: {telemetryEnabled: Services.telemetry.canRecordBase}});
+  }
   async init() {
     await this._refresh();
     Services.prefs.addObserver(ONBOARDING_FINISHED_PREF, this._refresh);
     Services.prefs.addObserver(SNIPPETS_URL_PREF, this._refresh);
-    Services.prefs.addObserver(TELEMETRY_PREF, this._refresh);
+    Services.prefs.addObserver(FXA_USERNAME_PREF, this._refresh);
   }
   uninit() {
     Services.prefs.removeObserver(ONBOARDING_FINISHED_PREF, this._refresh);
     Services.prefs.removeObserver(SNIPPETS_URL_PREF, this._refresh);
-    Services.prefs.removeObserver(TELEMETRY_PREF, this._refresh);
+    Services.prefs.removeObserver(FXA_USERNAME_PREF, this._refresh);
     this.store.dispatch({type: at.SNIPPETS_RESET});
   }
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
         break;
       case at.FEED_INIT:
         if (action.data === "feeds.snippets") { this.init(); }
         break;
+      case at.NEW_TAB_INIT:
+        this._refreshCanRecordBase();
+        break;
     }
   }
 };
 
 this.EXPORTED_SYMBOLS = ["SnippetsFeed"];
--- a/browser/extensions/activity-stream/lib/TelemetrySender.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetrySender.jsm
@@ -1,31 +1,30 @@
 /* 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/. */
 
 const {interfaces: Ci, utils: Cu} = Components;
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.importGlobalProperties(["fetch"]);
+Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/Console.jsm");
 
 // This is intentionally a different pref-branch than the SDK-based add-on
 // used, to avoid extra weirdness for people who happen to have the SDK-based
 // installed.  Though maybe we should just forcibly disable the old add-on?
 const PREF_BRANCH = "browser.newtabpage.activity-stream.";
 
 const ENDPOINT_PREF = `${PREF_BRANCH}telemetry.ping.endpoint`;
 const TELEMETRY_PREF = `${PREF_BRANCH}telemetry`;
 const LOGGING_PREF = `${PREF_BRANCH}telemetry.log`;
 
-const FHR_UPLOAD_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
-
 /**
  * Observe various notifications and send them to a telemetry endpoint.
  *
  * @param {Object} args - optional arguments
  * @param {Function} args.prefInitHook - if present, will be called back
  *                   inside the Prefs constructor. Typically used from tests
  *                   to save off a pointer to a fake Prefs instance so that
  *                   stubs and spies can be inspected by the test code.
@@ -39,44 +38,38 @@ function TelemetrySender(args) {
   }
 
   this._prefs = new Preferences(prefArgs);
 
   this._enabled = this._prefs.get(TELEMETRY_PREF);
   this._onTelemetryPrefChange = this._onTelemetryPrefChange.bind(this);
   this._prefs.observe(TELEMETRY_PREF, this._onTelemetryPrefChange);
 
-  this._fhrEnabled = this._prefs.get(FHR_UPLOAD_ENABLED_PREF);
-  this._onFhrPrefChange = this._onFhrPrefChange.bind(this);
-  this._prefs.observe(FHR_UPLOAD_ENABLED_PREF, this._onFhrPrefChange);
-
   this.logging = this._prefs.get(LOGGING_PREF);
   this._onLoggingPrefChange = this._onLoggingPrefChange.bind(this);
   this._prefs.observe(LOGGING_PREF, this._onLoggingPrefChange);
 
   this._pingEndpoint = this._prefs.get(ENDPOINT_PREF);
 }
 
 TelemetrySender.prototype = {
   get enabled() {
-    return this._enabled && this._fhrEnabled;
+    // Note: Services.telemetry.canRecordBase is the general indicator for
+    // opt-out Firefox Telemetry
+    return this._enabled && Services.telemetry.canRecordBase;
   },
 
   _onLoggingPrefChange(prefVal) {
     this.logging = prefVal;
   },
 
   _onTelemetryPrefChange(prefVal) {
     this._enabled = prefVal;
   },
 
-  _onFhrPrefChange(prefVal) {
-    this._fhrEnabled = prefVal;
-  },
-
   sendPing(data) {
     if (this.logging) {
       // performance related pings cause a lot of logging, so we mute them
       if (data.action !== "activity_stream_performance") {
         console.log(`TELEMETRY PING: ${JSON.stringify(data)}\n`); // eslint-disable-line no-console
       }
     }
     if (!this.enabled) {
@@ -90,23 +83,21 @@ TelemetrySender.prototype = {
       Cu.reportError(`Ping failure with error: ${e}`);
     });
   },
 
   uninit() {
     try {
       this._prefs.ignore(TELEMETRY_PREF, this._onTelemetryPrefChange);
       this._prefs.ignore(LOGGING_PREF, this._onLoggingPrefChange);
-      this._prefs.ignore(FHR_UPLOAD_ENABLED_PREF, this._onFhrPrefChange);
     } catch (e) {
       Cu.reportError(e);
     }
   }
 };
 
 this.TelemetrySender = TelemetrySender;
 this.TelemetrySenderConstants = {
   ENDPOINT_PREF,
-  FHR_UPLOAD_ENABLED_PREF,
   TELEMETRY_PREF,
   LOGGING_PREF
 };
 this.EXPORTED_SYMBOLS = ["TelemetrySender", "TelemetrySenderConstants"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/TippyTopProvider.jsm
@@ -0,0 +1,61 @@
+/* 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/. */
+
+const {utils: Cu} = Components;
+
+Cu.importGlobalProperties(["fetch", "URL"]);
+
+const TIPPYTOP_JSON_PATH = "resource://activity-stream/data/content/tippytop/top_sites.json";
+const TIPPYTOP_URL_PREFIX = "resource://activity-stream/data/content/tippytop/images/";
+
+function getDomain(url) {
+  let domain = new URL(url).hostname;
+  if (domain && domain.startsWith("www.")) {
+    domain = domain.slice(4);
+  }
+  return domain;
+}
+
+function getPath(url) {
+  return new URL(url).pathname;
+}
+
+this.TippyTopProvider = class TippyTopProvider {
+  constructor() {
+    this._sitesByDomain = new Map();
+  }
+  async init() {
+    // Load the Tippy Top sites from the json manifest.
+    try {
+      for (const site of await (await fetch(TIPPYTOP_JSON_PATH)).json()) {
+        // The tippy top manifest can have a url property (string) or a
+        // urls property (array of strings)
+        for (const url of site.url ? [site.url] : site.urls || []) {
+          this._sitesByDomain.set(getDomain(url), site);
+        }
+      }
+    } catch (error) {
+      Cu.reportError("Failed to load tippy top manifest.");
+    }
+  }
+  processSite(site) {
+    // Skip URLs with a path that isn't the root path /
+    let path;
+    try {
+      path = getPath(site.url);
+    } catch (e) {}
+    if (path !== "/") {
+      return site;
+    }
+
+    const tippyTop = this._sitesByDomain.get(getDomain(site.url));
+    if (tippyTop) {
+      site.tippyTopIcon = TIPPYTOP_URL_PREFIX + tippyTop.image_url;
+      site.backgroundColor = tippyTop.background_color;
+    }
+    return site;
+  }
+};
+
+this.EXPORTED_SYMBOLS = ["TippyTopProvider"];
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -2,35 +2,46 @@
  * 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/. */
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
-const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
+const {TippyTopProvider} = Cu.import("resource://activity-stream/lib/TippyTopProvider.jsm", {});
 const {insertPinned} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
+const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
+const {shortURL} = Cu.import("resource://activity-stream/common/ShortURL.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Screenshots",
   "resource://activity-stream/lib/Screenshots.jsm");
 
 const TOP_SITES_SHOWMORE_LENGTH = 12;
 const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
+const DEFAULT_SITES_PREF = "default.sites";
 const DEFAULT_TOP_SITES = [];
 
 this.TopSitesFeed = class TopSitesFeed {
   constructor() {
     this.lastUpdated = 0;
+    this._tippyTopProvider = new TippyTopProvider();
+    this._tippyTopProvider.init();
+    this.dedupe = new Dedupe(this._dedupeKey);
   }
-  init() {
+  _dedupeKey(site) {
+    return site && site.hostname;
+  }
+  refreshDefaults(sites) {
+    // Clear out the array of any previous defaults
+    DEFAULT_TOP_SITES.length = 0;
+
     // Add default sites if any based on the pref
-    let sites = new Prefs().get("default.sites");
     if (sites) {
       for (const url of sites.split(",")) {
         DEFAULT_TOP_SITES.push({
           isDefault: true,
           url
         });
       }
     }
@@ -39,40 +50,59 @@ this.TopSitesFeed = class TopSitesFeed {
     let screenshot = await Screenshots.getScreenshotForURL(url);
     const action = {type: at.SCREENSHOT_UPDATED, data: {url, screenshot}};
     this.store.dispatch(ac.BroadcastToContent(action));
   }
   async getLinksWithDefaults(action) {
     let frecent = await NewTabUtils.activityStreamLinks.getTopSites();
     const defaultUrls = DEFAULT_TOP_SITES.map(site => site.url);
     let pinned = NewTabUtils.pinnedLinks.links;
-    pinned = pinned.map(site => site && Object.assign({}, site, {isDefault: defaultUrls.indexOf(site.url) !== -1}));
+    pinned = pinned.map(site => site && Object.assign({}, site, {
+      isDefault: defaultUrls.indexOf(site.url) !== -1,
+      hostname: shortURL(site)
+    }));
 
     if (!frecent) {
       frecent = [];
     } else {
       frecent = frecent.filter(link => link && link.type !== "affiliate");
     }
 
-    return insertPinned([...frecent, ...DEFAULT_TOP_SITES], pinned).slice(0, TOP_SITES_SHOWMORE_LENGTH);
+    // Group together websites that require deduping.
+    let topsitesGroup = [];
+    for (const group of [pinned, frecent, DEFAULT_TOP_SITES]) {
+      topsitesGroup.push(group.filter(site => site).map(site => Object.assign({}, site, {hostname: shortURL(site)})));
+    }
+
+    const dedupedGroups = this.dedupe.group(topsitesGroup);
+    // Insert original pinned websites in the result of the dedupe operation.
+    pinned = insertPinned([...dedupedGroups[1], ...dedupedGroups[2]], pinned);
+
+    return pinned.slice(0, TOP_SITES_SHOWMORE_LENGTH);
   }
   async refresh(target = null) {
     const links = await this.getLinksWithDefaults();
 
     // First, cache existing screenshots in case we need to reuse them
     const currentScreenshots = {};
     for (const link of this.store.getState().TopSites.rows) {
       if (link && link.screenshot) {
         currentScreenshots[link.url] = link.screenshot;
       }
     }
 
-    // Now, get a screenshot for every item
+    // Now, get a tippy top icon or screenshot for every item
     for (let link of links) {
       if (!link) { continue; }
+
+      // Check for tippy top icon.
+      link = this._tippyTopProvider.processSite(link);
+      if (link.tippyTopIcon) { continue; }
+
+      // If no tippy top, then we get a screenshot.
       if (currentScreenshots[link.url]) {
         link.screenshot = currentScreenshots[link.url];
       } else {
         this.getScreenshot(link.url);
       }
     }
     const newAction = {type: at.TOP_SITES_UPDATED, data: links};
 
@@ -103,38 +133,37 @@ this.TopSitesFeed = class TopSitesFeed {
     const {site} = action.data;
     NewTabUtils.pinnedLinks.unpin(site);
     this.store.dispatch(ac.BroadcastToContent({
       type: at.PINNED_SITES_UPDATED,
       data: this._getPinnedWithData()
     }));
   }
   onAction(action) {
-    let realRows;
     switch (action.type) {
-      case at.INIT:
-        this.init();
-        break;
       case at.NEW_TAB_LOAD:
-        // Only check against real rows returned from history, not default ones.
-        realRows = this.store.getState().TopSites.rows.filter(row => !row.isDefault);
         if (
-          // When a new tab is opened, if we don't have enough top sites yet, refresh the data.
-          (realRows.length < TOP_SITES_SHOWMORE_LENGTH) ||
-
           // When a new tab is opened, if the last time we refreshed the data
           // is greater than 15 minutes, refresh the data.
           (Date.now() - this.lastUpdated >= UPDATE_TIME)
         ) {
           this.refresh(action.meta.fromTarget);
         }
         break;
       case at.PLACES_HISTORY_CLEARED:
         this.refresh();
         break;
+      case at.PREF_CHANGED:
+        if (action.data.name === DEFAULT_SITES_PREF) {
+          this.refreshDefaults(action.data.value);
+        }
+        break;
+      case at.PREFS_INITIAL_VALUES:
+        this.refreshDefaults(action.data[DEFAULT_SITES_PREF]);
+        break;
       case at.TOP_SITES_PIN:
         this.pin(action);
         break;
       case at.TOP_SITES_UNPIN:
         this.unpin(action);
         break;
     }
   }
--- a/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
@@ -6,36 +6,37 @@
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NewTabUtils.jsm");
 Cu.importGlobalProperties(["fetch"]);
 
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
+const {shortURL} = Cu.import("resource://activity-stream/common/ShortURL.jsm", {});
 
 const STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours
 const STORIES_NOW_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours
 const SECTION_ID = "TopStories";
+const FEED_PREF = "feeds.section.topstories";
+const SECTION_OPTIONS_PREF = "feeds.section.topstories.options";
 
 this.TopStoriesFeed = class TopStoriesFeed {
-  constructor() {
-    this.storiesLastUpdated = 0;
-    this.topicsLastUpdated = 0;
-  }
 
   init() {
     try {
+      this.storiesLastUpdated = 0;
+      this.topicsLastUpdated = 0;
+
       const prefs = new Prefs();
-      const options = JSON.parse(prefs.get("feeds.section.topstories.options"));
+      const options = JSON.parse(prefs.get(SECTION_OPTIONS_PREF));
       const apiKey = this._getApiKeyFromPref(options.api_key_pref);
-      const locale = Services.locale.getRequestedLocale();
-      this.stories_endpoint = this._produceFinalEndpointUrl(options.stories_endpoint, apiKey, locale);
-      this.topics_endpoint = this._produceFinalEndpointUrl(options.topics_endpoint, apiKey, locale);
+      this.stories_endpoint = this._produceFinalEndpointUrl(options.stories_endpoint, apiKey);
+      this.topics_endpoint = this._produceFinalEndpointUrl(options.topics_endpoint, apiKey);
 
       this.read_more_endpoint = options.read_more_endpoint;
       this.stories_referrer = options.stories_referrer;
 
       // TODO https://github.com/mozilla/activity-stream/issues/2902
       const sectionOptions = {
         id: SECTION_ID,
         eventSource: "TOP_STORIES",
@@ -80,22 +81,24 @@ this.TopStoriesFeed = class TopStoriesFe
           throw new Error(`Stories endpoint returned unexpected status: ${response.status}`);
         })
         .then(body => {
           let items = JSON.parse(body).list;
           items = items
             .filter(s => !NewTabUtils.blockedLinks.isBlocked({"url": s.dedupe_url}))
             .map(s => ({
               "guid": s.id,
+              "hostname": shortURL(Object.assign({}, s, {url: s.dedupe_url})),
               "type": (Date.now() - (s.published_timestamp * 1000)) <= STORIES_NOW_THRESHOLD ? "now" : "trending",
               "title": s.title,
               "description": s.excerpt,
               "image": this._normalizeUrl(s.image_src),
               "referrer": this.stories_referrer,
-              "url": s.dedupe_url
+              "url": s.dedupe_url,
+              "eTLD": this._addETLD(s.dedupe_url)
             }));
           return items;
         })
         .catch(error => Cu.reportError(`Failed to fetch content: ${error.message}`));
 
       if (stories) {
         this.dispatchUpdateEvent(this.storiesLastUpdated,
           {"type": at.SECTION_ROWS_UPDATE, "data": {"id": SECTION_ID, "rows": stories}});
@@ -135,38 +138,43 @@ this.TopStoriesFeed = class TopStoriesFe
   _getApiKeyFromPref(apiKeyPref) {
     if (!apiKeyPref) {
       return apiKeyPref;
     }
 
     return new Prefs().get(apiKeyPref) || Services.prefs.getCharPref(apiKeyPref);
   }
 
-  _produceFinalEndpointUrl(url, apiKey, locale) {
+  _produceFinalEndpointUrl(url, apiKey) {
     if (!url) {
       return url;
     }
     if (url.includes("$apiKey") && !apiKey) {
       throw new Error(`An API key was specified but none configured: ${url}`);
     }
-    if (url.includes("$locale") && !locale) {
-      throw new Error(`A locale was specified but none detected: ${url}`);
-    }
-    return url.replace("$apiKey", apiKey).replace("$locale", locale);
+    return url.replace("$apiKey", apiKey);
   }
 
   // Need to remove parenthesis from image URLs as React will otherwise
   // fail to render them properly as part of the card template.
   _normalizeUrl(url) {
     if (url) {
       return url.replace(/\(/g, "%28").replace(/\)/g, "%29");
     }
     return url;
   }
 
+  _addETLD(url) {
+    try {
+      return Services.eTLD.getPublicSuffix(Services.io.newURI(url));
+    } catch (err) {
+      return "";
+    }
+  }
+
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
         break;
       case at.SYSTEM_TICK:
         if (Date.now() - this.storiesLastUpdated >= STORIES_UPDATE_TIME) {
           this.fetchStories();
@@ -174,20 +182,27 @@ this.TopStoriesFeed = class TopStoriesFe
         if (Date.now() - this.topicsLastUpdated >= TOPICS_UPDATE_TIME) {
           this.fetchTopics();
         }
         break;
       case at.UNINIT:
         this.uninit();
         break;
       case at.FEED_INIT:
-        if (action.data === "feeds.section.topstories") {
+        if (action.data === FEED_PREF) {
+          this.init();
+        }
+        break;
+      case at.PREF_CHANGED:
+        if (action.data.name === SECTION_OPTIONS_PREF) {
           this.init();
         }
         break;
     }
   }
 };
 
 this.STORIES_UPDATE_TIME = STORIES_UPDATE_TIME;
 this.TOPICS_UPDATE_TIME = TOPICS_UPDATE_TIME;
 this.SECTION_ID = SECTION_ID;
-this.EXPORTED_SYMBOLS = ["TopStoriesFeed", "STORIES_UPDATE_TIME", "TOPICS_UPDATE_TIME", "SECTION_ID"];
+this.FEED_PREF = FEED_PREF;
+this.SECTION_OPTIONS_PREF = SECTION_OPTIONS_PREF;
+this.EXPORTED_SYMBOLS = ["TopStoriesFeed", "STORIES_UPDATE_TIME", "TOPICS_UPDATE_TIME", "SECTION_ID", "FEED_PREF", "SECTION_OPTIONS_PREF"];
--- a/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
+++ b/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
@@ -1,41 +1,5 @@
 module.exports = {
-  "globals": {
-    "add_task": false,
-    "Assert": false,
-    "BrowserOpenTab": false,
-    "BrowserTestUtils": false,
-    "content": false,
-    "ContentTask": false,
-    "ContentTaskUtils": false,
-    "Components": false,
-    "EventUtils": false,
-    "executeSoon": false,
-    "expectUncaughtException": false,
-    "export_assertions": false,
-    "extractJarToTmp": false,
-    "finish": false,
-    "getJar": false,
-    "getRootDirectory": false,
-    "getTestFilePath": false,
-    "gBrowser": false,
-    "gTestPath": false,
-    "info": false,
-    "is": false,
-    "isnot": false,
-    "ok": false,
-    "OpenBrowserWindow": false,
-    "Preferences": false,
-    "registerCleanupFunction": false,
-    "requestLongerTimeout": false,
-    "Services": false,
-    "SimpleTest": false,
-    "SpecialPowers": false,
-    "TestUtils": false,
-    "todo": false,
-    "todo_is": false,
-    "todo_isnot": false,
-    "waitForClipboard": false,
-    "waitForExplicitFinish": false,
-    "waitForFocus": false
-  }
+  "extends": [
+    "plugin:mozilla/browser-test"
+  ]
 };
--- a/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
@@ -1,7 +1,9 @@
 [DEFAULT]
 support-files =
   blue_page.html
+  head.js
 
 [browser_as_load_location.js]
+[browser_as_render.js]
 [browser_getScreenshots.js]
 skip-if=true # issue 2851
--- a/browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
@@ -1,34 +1,53 @@
 "use strict";
 
-let Cu = Components.utils;
-Cu.import("resource://gre/modules/Services.jsm");
-
 /**
- * Tests that opening a new tab opens a page with the expected activity stream
- * content.
+ * Helper to test that a newtab page loads its html document.
  *
- * XXX /browser/components/newtab/tests/browser/browser_newtab_overrides in
- * mozilla-central is where this test was adapted from.  Once we get decide on
- * and implement how we're going to set the URL in mozilla-central, we may well
- * want to (separately from this test), clone/adapt that entire file for our
- * new setup.
+ * @param selector {String} CSS selector to find an element in newtab content
+ * @param message {String} Description of the test printed with the assertion
  */
-add_task(async function checkActivityStreamLoads() {
-  const asURL = "resource://activity-stream/data/content/activity-stream.html";
-
+async function checkNewtabLoads(selector, message) {
   // simulate a newtab open as a user would
   BrowserOpenTab();
 
   // wait until the browser loads
   let browser = gBrowser.selectedBrowser;
-  await BrowserTestUtils.browserLoaded(browser);
+  await waitForPreloaded(browser);
 
   // check what the content task thinks has been loaded.
-  await ContentTask.spawn(browser, {url: asURL}, args => {
-    Assert.ok(content.document.querySelector("body.activity-stream"),
-      'Got <body class="activity-stream" Element');
-  });
+  let found = await ContentTask.spawn(browser, selector, arg =>
+    content.document.querySelector(arg) !== null);
+  ok(found, message);
 
   // avoid leakage
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+// Test with activity stream on
+async function checkActivityStreamLoads() {
+  await checkNewtabLoads("body.activity-stream", "Got <body class='activity-stream'> Element");
+}
+
+// Run a first time not from a preloaded browser
+add_task(async function checkActivityStreamNotPreloadedLoad() {
+  gBrowser.removePreloadedBrowser();
+  await checkActivityStreamLoads();
 });
+
+// Run a second time from a preloaded browser
+add_task(checkActivityStreamLoads);
+
+// Test with activity stream off
+async function checkNotActivityStreamLoads() {
+  await SpecialPowers.pushPrefEnv({set: [["browser.newtabpage.activity-stream.enabled", false]]});
+  await checkNewtabLoads("body:not(.activity-stream)", "Got <body> Element not for activity-stream");
+}
+
+// Run a first time not from a preloaded browser
+add_task(async function checkNotActivityStreamNotPreloadedLoad() {
+  gBrowser.removePreloadedBrowser();
+  await checkNotActivityStreamLoads();
+});
+
+// Run a second time from a preloaded browser
+add_task(checkNotActivityStreamLoads);
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser_as_render.js
@@ -0,0 +1,29 @@
+"use strict";
+
+test_newtab(function test_render_search() {
+  let search = content.document.getElementById("newtab-search-text");
+  ok(search, "Got the search box");
+  isnot(search.placeholder, "search_web_placeholder", "Search box is localized");
+});
+
+test_newtab(function test_render_topsites() {
+  let topSites = content.document.querySelector(".top-sites-list");
+  ok(topSites, "Got the top sites section");
+});
+
+test_newtab({
+  async before({pushPrefs}) {
+    await pushPrefs(["browser.newtabpage.activity-stream.showTopSites", false]);
+  },
+  test: function test_render_no_topsites() {
+    let topSites = content.document.querySelector(".top-sites-list");
+    ok(!topSites, "No top sites section");
+  }
+});
+
+// This next test runs immediately after test_render_no_topsites to make sure
+// the topsites pref is restored
+test_newtab(function test_render_topsites_again() {
+  let topSites = content.document.querySelector(".top-sites-list");
+  ok(topSites, "Got the top sites section again");
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/head.js
@@ -0,0 +1,95 @@
+"use strict";
+
+function popPrefs() {
+  return SpecialPowers.popPrefEnv();
+}
+function pushPrefs(...prefs) {
+  return SpecialPowers.pushPrefEnv({set: prefs});
+}
+
+// Activity Stream tests expect it to be enabled, and make sure to clear out any
+// preloaded browsers that might have about:newtab that we don't want to test
+const ACTIVITY_STREAM_PREF = "browser.newtabpage.activity-stream.enabled";
+pushPrefs([ACTIVITY_STREAM_PREF, true]);
+gBrowser.removePreloadedBrowser();
+
+/**
+ * Helper to wait for potentially preloaded browsers to "load" where a preloaded
+ * page has already loaded and won't trigger "load", and a "load"ed page might
+ * not necessarily have had all its javascript/render logic executed.
+ */
+async function waitForPreloaded(browser) {
+  let readyState = await ContentTask.spawn(browser, {}, () => content.document.readyState);
+  if (readyState !== "complete") {
+    await BrowserTestUtils.browserLoaded(browser);
+  }
+}
+
+/**
+ * Helper to run Activity Stream about:newtab test tasks in content.
+ *
+ * @param testInfo {Function|Object}
+ *   {Function} This parameter will be used as if the function were called with
+ *              an Object with this parameter as "test" key's value.
+ *   {Object} The following keys are expected:
+ *     before {Function} Optional. Runs before and returns an arg for "test"
+ *     test   {Function} The test to run in the about:newtab content task taking
+ *                       an arg from "before" and returns a result to "after"
+ *     after  {Function} Optional. Runs after and with the result of "test"
+ */
+function test_newtab(testInfo) { // eslint-disable-line no-unused-vars
+  // Extract any test parts or default to just the single content task
+  let {before, test: contentTask, after} = testInfo;
+  if (!before) {
+    before = () => ({});
+  }
+  if (!contentTask) {
+    contentTask = testInfo;
+  }
+  if (!after) {
+    after = () => {};
+  }
+
+  // Helper to push prefs for just this test and pop them when done
+  let needPopPrefs = false;
+  let scopedPushPrefs = async(...args) => {
+    needPopPrefs = true;
+    await pushPrefs(...args);
+  };
+  let scopedPopPrefs = async() => {
+    if (needPopPrefs) {
+      await popPrefs();
+    }
+  };
+
+  // Make the test task with optional before/after and content task to run in a
+  // new tab that opens and closes.
+  let testTask = async() => {
+    // Open about:newtab without using the default load listener
+    let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+
+    // Specially wait for potentially preloaded browsers
+    let browser = tab.linkedBrowser;
+    await waitForPreloaded(browser);
+
+    // Wait for React to render something
+    await BrowserTestUtils.waitForCondition(() => ContentTask.spawn(browser, {},
+      () => content.document.getElementById("root").children.length),
+      "Should render activity stream content");
+
+    // Chain together before -> contentTask -> after data passing
+    try {
+      let contentArg = await before({pushPrefs: scopedPushPrefs, tab});
+      let contentResult = await ContentTask.spawn(browser, contentArg, contentTask);
+      await after(contentResult);
+    } finally {
+      // Clean up for next tests
+      await scopedPopPrefs();
+      await BrowserTestUtils.removeTab(tab);
+    }
+  };
+
+  // Copy the name of the content task to identify the test
+  Object.defineProperty(testTask, "name", {value: contentTask.name});
+  add_task(testTask);
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/common/Dedupe.test.js
@@ -0,0 +1,42 @@
+const {Dedupe} = require("common/Dedupe.jsm");
+
+describe("Dedupe", () => {
+  let instance;
+  beforeEach(() => {
+    instance = new Dedupe();
+  });
+  describe("group", () => {
+    it("should remove duplicates inside the groups", () => {
+      const beforeItems = [[1, 1, 1], [2, 2, 2], [3, 3, 3]];
+      const afterItems = [[1], [2], [3]];
+      assert.deepEqual(instance.group(beforeItems), afterItems);
+    });
+    it("should remove duplicates between groups, favouring earlier groups", () => {
+      const beforeItems = [[1, 2, 3], [2, 3, 4], [3, 4, 5]];
+      const afterItems = [[1, 2, 3], [4], [5]];
+      assert.deepEqual(instance.group(beforeItems), afterItems);
+    });
+    it("should remove duplicates from groups of objects", () => {
+      instance = new Dedupe(item => item.id);
+      const beforeItems = [[{id: 1}, {id: 1}, {id: 2}], [{id: 1}, {id: 3}, {id: 2}], [{id: 1}, {id: 2}, {id: 5}]];
+      const afterItems = [[{id: 1}, {id: 2}], [{id: 3}], [{id: 5}]];
+      assert.deepEqual(instance.group(beforeItems), afterItems);
+    });
+    it("should take a custom comparison function", () => {
+      function compare(previous, current) {
+        return current.amount > previous.amount;
+      }
+      instance = new Dedupe(item => item.id, compare);
+      const beforeItems = [
+        [{id: 1, amount: 50}, {id: 1, amount: 100}],
+        [{id: 1, amount: 200}, {id: 2, amount: 0}, {id: 2, amount: 100}]
+      ];
+      const afterItems = [
+        [{id: 1, amount: 100}],
+        [{id: 2, amount: 100}]
+      ];
+
+      assert.deepEqual(instance.group(beforeItems), afterItems);
+    });
+  });
+});
--- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -4,24 +4,28 @@ const {TopSites, App, Snippets, Prefs, D
 const {actionTypes: at} = require("common/Actions.jsm");
 
 describe("Reducers", () => {
   describe("App", () => {
     it("should return the initial state", () => {
       const nextState = App(undefined, {type: "FOO"});
       assert.equal(nextState, INITIAL_STATE.App);
     });
-    it("should not set initialized to true on INIT", () => {
+    it("should set initialized to true on INIT", () => {
       const nextState = App(undefined, {type: "INIT"});
+
       assert.propertyVal(nextState, "initialized", true);
     });
     it("should set initialized, version, and locale on INIT", () => {
       const action = {type: "INIT", data: {version: "1.2.3"}};
+
       const nextState = App(undefined, action);
+
       assert.propertyVal(nextState, "version", "1.2.3");
+      assert.propertyVal(nextState, "locale", INITIAL_STATE.App.locale);
     });
     it("should not update state for empty action.data on LOCALE_UPDATED", () => {
       const nextState = App(undefined, {type: at.LOCALE_UPDATED});
       assert.equal(nextState, INITIAL_STATE.App);
     });
     it("should set locale, strings on LOCALE_UPDATE", () => {
       const strings = {};
       const action = {type: "LOCALE_UPDATED", data: {locale: "zh-CN", strings}};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/common/ShortUrl.test.js
@@ -0,0 +1,46 @@
+const {shortURL} = require("common/ShortURL.jsm");
+
+describe("shortURL", () => {
+  it("should return a blank string if url and hostname is falsey", () => {
+    assert.equal(shortURL({url: ""}), "");
+    assert.equal(shortURL({hostname: null}), "");
+  });
+
+  it("should remove the eTLD, if provided", () => {
+    assert.equal(shortURL({hostname: "com.blah.com", eTLD: "com"}), "com.blah");
+  });
+
+  it("should use the hostname, if provided", () => {
+    assert.equal(shortURL({hostname: "foo.com", url: "http://bar.com", eTLD: "com"}), "foo");
+  });
+
+  it("should get the hostname from .url if necessary", () => {
+    assert.equal(shortURL({url: "http://bar.com", eTLD: "com"}), "bar");
+  });
+
+  it("should not strip out www if not first subdomain", () => {
+    assert.equal(shortURL({hostname: "foo.www.com", eTLD: "com"}), "foo.www");
+  });
+
+  it("should convert to lowercase", () => {
+    assert.equal(shortURL({url: "HTTP://FOO.COM", eTLD: "com"}), "foo");
+  });
+
+  it("should return hostname for localhost", () => {
+    assert.equal(shortURL({url: "http://localhost:8000/", eTLD: "localhost"}), "localhost");
+  });
+
+  it("should fallback to link title if it exists", () => {
+    const link = {
+      url: "file:///Users/voprea/Work/activity-stream/logs/coverage/system-addon/report-html/index.html",
+      title: "Code coverage report"
+    };
+
+    assert.equal(shortURL(link), link.title);
+  });
+
+  it("should return the url if no hostname or title is provided", () => {
+    const url = "file://foo/bar.txt";
+    assert.equal(shortURL({url, eTLD: "foo"}), url);
+  });
+});
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -1,22 +1,23 @@
 const injector = require("inject!lib/ActivityStream.jsm");
+const {CONTENT_MESSAGE_TYPE} = require("common/Actions.jsm");
 
 const REASON_ADDON_UNINSTALL = 6;
 
 describe("ActivityStream", () => {
   let sandbox;
   let as;
   let ActivityStream;
-  let SECTIONS;
+  let PREFS_CONFIG;
   function Fake() {}
 
   beforeEach(() => {
     sandbox = sinon.sandbox.create();
-    ({ActivityStream, SECTIONS} = injector({
+    ({ActivityStream, PREFS_CONFIG} = injector({
       "lib/LocalizationFeed.jsm": {LocalizationFeed: Fake},
       "lib/ManualMigration.jsm": {ManualMigration: Fake},
       "lib/NewTabInit.jsm": {NewTabInit: Fake},
       "lib/PlacesFeed.jsm": {PlacesFeed: Fake},
       "lib/PrefsFeed.jsm": {PrefsFeed: Fake},
       "lib/SnippetsFeed.jsm": {SnippetsFeed: Fake},
       "lib/SystemTickFeed.jsm": {SystemTickFeed: Fake},
       "lib/TelemetryFeed.jsm": {TelemetryFeed: Fake},
@@ -58,16 +59,24 @@ describe("ActivityStream", () => {
       sandbox.stub(as._defaultPrefs, "init");
 
       as.init();
 
       assert.calledOnce(as.store.dispatch);
       const action = as.store.dispatch.firstCall.args[0];
       assert.propertyVal(action.data, "version", "1.2.3");
     });
+    it("should emit an INIT event to content", () => {
+      sandbox.stub(as.store, "dispatch");
+
+      as.init();
+
+      const action = as.store.dispatch.firstCall.args[0];
+      assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
+    });
   });
   describe("#uninit", () => {
     beforeEach(() => {
       as.init();
       as.uninit();
     });
     it("should set .initialized to false", () => {
       assert.isFalse(as.initialized, ".initialized");
@@ -106,30 +115,113 @@ describe("ActivityStream", () => {
     it("should create a Telemetry feed", () => {
       const feed = as.feeds.get("feeds.telemetry")();
       assert.instanceOf(feed, Fake);
     });
     it("should create a Prefs feed", () => {
       const feed = as.feeds.get("feeds.prefs")();
       assert.instanceOf(feed, Fake);
     });
-    it("should create a section feed for each section in SECTIONS", () => {
+    it("should create a section feed for each section in PREFS_CONFIG", () => {
       // If new sections are added, their feeds will have to be added to the
       // list of injected feeds above for this test to pass
-      SECTIONS.forEach((value, key) => {
-        const feed = as.feeds.get(`feeds.section.${key}`)();
-        assert.instanceOf(feed, Fake);
-      });
+      let feedCount = 0;
+      for (const pref of PREFS_CONFIG.keys()) {
+        if (pref.search(/^feeds\.section\.[^.]+$/) === 0) {
+          const feed = as.feeds.get(pref)();
+          assert.instanceOf(feed, Fake);
+          feedCount++;
+        }
+      }
+      assert.isAbove(feedCount, 0);
     });
     it("should create a ManualMigration feed", () => {
       const feed = as.feeds.get("feeds.migration")();
       assert.instanceOf(feed, Fake);
     });
     it("should create a Snippets feed", () => {
       const feed = as.feeds.get("feeds.snippets")();
       assert.instanceOf(feed, Fake);
     });
     it("should create a SystemTick feed", () => {
       const feed = as.feeds.get("feeds.systemtick")();
       assert.instanceOf(feed, Fake);
     });
   });
+  describe("_updateDynamicPrefs topstories default value", () => {
+    it("should be false with no geo/locale", () => {
+      as._updateDynamicPrefs();
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should be false with unexpected geo", () => {
+      sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("NOGEO");
+
+      as._updateDynamicPrefs();
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should be false with expected geo and unexpected locale", () => {
+      sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("US");
+      sandbox.stub(global.Services.locale, "getRequestedLocale").returns("no-LOCALE");
+
+      as._updateDynamicPrefs();
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should be true with expected geo and locale", () => {
+      sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("US");
+      sandbox.stub(global.Services.locale, "getRequestedLocale").returns("en-US");
+
+      as._updateDynamicPrefs();
+
+      assert.isTrue(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should be false after expected geo and locale then unexpected", () => {
+      sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+      sandbox.stub(global.Services.prefs, "getStringPref")
+        .onFirstCall()
+        .returns("US")
+        .onSecondCall()
+        .returns("NOGEO");
+      sandbox.stub(global.Services.locale, "getRequestedLocale").returns("en-US");
+
+      as._updateDynamicPrefs();
+      as._updateDynamicPrefs();
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+  });
+  describe("_updateDynamicPrefs topstories delayed default value", () => {
+    let clock;
+    beforeEach(() => {
+      clock = sinon.useFakeTimers();
+
+      // Have addObserver cause prefHasUserValue to now return true then observe
+      sandbox.stub(global.Services.prefs, "addObserver").callsFake((pref, obs) => {
+        sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+        setTimeout(() => obs.observe(null, "nsPref:changed", pref)); // eslint-disable-line max-nested-callbacks
+      });
+    });
+    afterEach(() => clock.restore());
+
+    it("should set false with unexpected geo", () => {
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("NOGEO");
+
+      as._updateDynamicPrefs();
+      clock.tick(1);
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should set true with expected geo and locale", () => {
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("US");
+      sandbox.stub(global.Services.locale, "getRequestedLocale").returns("en-US");
+
+      as._updateDynamicPrefs();
+      clock.tick(1);
+
+      assert.isTrue(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+  });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
@@ -5,26 +5,31 @@ const {actionTypes: at, actionCreators: 
 
 const OPTIONS = ["pageURL, outgoingMessageName", "incomingMessageName", "dispatch"];
 
 describe("ActivityStreamMessageChannel", () => {
   let globals;
   let dispatch;
   let mm;
   beforeEach(() => {
-    function RP(url) {
+    function RP(url, isFromAboutNewTab = false) {
       this.url = url;
       this.messagePorts = [];
       this.addMessageListener = globals.sandbox.spy();
+      this.removeMessageListener = globals.sandbox.spy();
       this.sendAsyncMessage = globals.sandbox.spy();
       this.destroy = globals.sandbox.spy();
+      this.isFromAboutNewTab = isFromAboutNewTab;
     }
     globals = new GlobalOverrider();
+    const override = globals.sandbox.stub();
+    override.withArgs(true).returns(new RP("about:newtab", true));
+    override.withArgs(false).returns(null);
     globals.set("AboutNewTab", {
-      override: globals.sandbox.spy(),
+      override,
       reset: globals.sandbox.spy()
     });
     globals.set("RemotePages", RP);
     dispatch = globals.sandbox.spy();
     mm = new ActivityStreamMessageChannel({dispatch});
   });
 
   afterEach(() => globals.restore());
@@ -59,46 +64,54 @@ describe("ActivityStreamMessageChannel",
       it("should add the custom message listener to the channel", () => {
         mm.createChannel();
         assert.calledWith(mm.channel.addMessageListener, mm.incomingMessageName, mm.onMessage);
       });
       it("should override AboutNewTab", () => {
         mm.createChannel();
         assert.calledOnce(global.AboutNewTab.override);
       });
+      it("should use the channel passed by AboutNewTab on override", () => {
+        mm.createChannel();
+        assert.ok(mm.channel.isFromAboutNewTab);
+      });
       it("should not override AboutNewTab if the pageURL is not about:newtab", () => {
         mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
         mm.createChannel();
         assert.notCalled(global.AboutNewTab.override);
       });
     });
     describe("#destroyChannel", () => {
       let channel;
       beforeEach(() => {
         mm.createChannel();
         channel = mm.channel;
       });
-      it("should call channel.destroy()", () => {
-        mm.destroyChannel();
-        assert.calledOnce(channel.destroy);
-      });
       it("should set .channel to null", () => {
         mm.destroyChannel();
         assert.isNull(mm.channel);
       });
-      it("should reset AboutNewTab", () => {
+      it("should reset AboutNewTab, and pass back its channel", () => {
         mm.destroyChannel();
         assert.calledOnce(global.AboutNewTab.reset);
+        assert.calledWith(global.AboutNewTab.reset, channel);
       });
       it("should not reset AboutNewTab if the pageURL is not about:newtab", () => {
         mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
         mm.createChannel();
         mm.destroyChannel();
         assert.notCalled(global.AboutNewTab.reset);
       });
+      it("should call channel.destroy() if pageURL is not about:newtab", () => {
+        mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
+        mm.createChannel();
+        channel = mm.channel;
+        mm.destroyChannel();
+        assert.calledOnce(channel.destroy);
+      });
     });
   });
   describe("Message handling", () => {
     describe("#getTargetById", () => {
       it("should get an id if it exists", () => {
         const t = {portID: "foo"};
         mm.createChannel();
         mm.channel.messagePorts.push(t);
--- a/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
@@ -20,18 +20,16 @@ describe("Localization Feed", () => {
   let feed;
   let globals;
   let sandbox;
   beforeEach(() => {
     globals = new GlobalOverrider();
     sandbox = globals.sandbox;
     feed = new LocalizationFeed();
     feed.store = {dispatch: sinon.spy()};
-
-    sandbox.stub(global.Services.locale, "getRequestedLocale");
   });
   afterEach(() => {
     globals.restore();
   });
 
   it("should fetch strings on init", async () => {
     sandbox.stub(feed, "updateLocale");
     sandbox.stub(global, "fetch");
@@ -45,55 +43,59 @@ describe("Localization Feed", () => {
 
   describe("#updateLocale", () => {
     beforeEach(() => {
       feed.allStrings = TEST_STRINGS;
     });
 
     it("should dispatch with locale and strings for default", () => {
       const locale = DEFAULT_LOCALE;
+      sandbox.stub(global.Services.locale, "negotiateLanguages")
+        .returns([locale]);
       feed.updateLocale();
 
       assert.calledOnce(feed.store.dispatch);
       const arg = feed.store.dispatch.firstCall.args[0];
       assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
       assert.propertyVal(arg.data, "locale", locale);
       assert.deepEqual(arg.data.strings, TEST_STRINGS[locale]);
     });
     it("should use strings for other locale", () => {
       const locale = "it";
-      global.Services.locale.getRequestedLocale.returns(locale);
+      sandbox.stub(global.Services.locale, "negotiateLanguages")
+        .returns([locale]);
 
       feed.updateLocale();
 
       assert.calledOnce(feed.store.dispatch);
       const arg = feed.store.dispatch.firstCall.args[0];
       assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
       assert.propertyVal(arg.data, "locale", locale);
       assert.deepEqual(arg.data.strings, TEST_STRINGS[locale]);
     });
     it("should use some fallback strings for partial locale", () => {
       const locale = "ru";
-      global.Services.locale.getRequestedLocale.returns(locale);
+      sandbox.stub(global.Services.locale, "negotiateLanguages")
+        .returns([locale]);
 
       feed.updateLocale();
 
       assert.calledOnce(feed.store.dispatch);
       const arg = feed.store.dispatch.firstCall.args[0];
       assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
       assert.propertyVal(arg.data, "locale", locale);
       assert.deepEqual(arg.data.strings, {
         foo: TEST_STRINGS[locale].foo,
         too: TEST_STRINGS[DEFAULT_LOCALE].too
       });
     });
     it("should use all default strings for unknown locale", () => {
       const locale = "xyz";
-      global.Services.locale.getRequestedLocale.returns(locale);
-
+      sandbox.stub(global.Services.locale, "negotiateLanguages")
+        .returns([locale]);
       feed.updateLocale();
 
       assert.calledOnce(feed.store.dispatch);
       const arg = feed.store.dispatch.firstCall.args[0];
       assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
       assert.propertyVal(arg.data, "locale", locale);
       assert.deepEqual(arg.data.strings, TEST_STRINGS[DEFAULT_LOCALE]);
     });
--- a/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
@@ -120,32 +120,32 @@ describe("PlacesFeed", () => {
       sinon.stub(openWindowAction._target.browser.ownerGlobal, "openLinkIn");
       feed.onAction(openWindowAction);
       assert.calledOnce(openWindowAction._target.browser.ownerGlobal.openLinkIn);
     });
     it("should open link on OPEN_LINK", () => {
       sinon.stub(feed, "openNewWindow");
       const openLinkAction = {
         type: at.OPEN_LINK,
-        data: {url: "foo.com"},
-        _target: {browser: {loadURI: sinon.spy()}}
+        data: {url: "foo.com", event: {where: "current"}},
+        _target: {browser: {ownerGlobal: {openLinkIn: sinon.spy(), whereToOpenLink: e => e.where}}}
       };
       feed.onAction(openLinkAction);
-      assert.calledWith(openLinkAction._target.browser.loadURI, openLinkAction.data.url);
+      assert.calledWith(openLinkAction._target.browser.ownerGlobal.openLinkIn, openLinkAction.data.url, "current");
     });
     it("should open link with referrer on OPEN_LINK", () => {
       globals.set("Services", {io: {newURI: url => `URI:${url}`}});
       sinon.stub(feed, "openNewWindow");
       const openLinkAction = {
         type: at.OPEN_LINK,
-        data: {url: "foo.com", referrer: "foo.com/ref"},
-        _target: {browser: {loadURI: sinon.spy()}}
+        data: {url: "foo.com", referrer: "foo.com/ref", event: {where: "tab"}},
+        _target: {browser: {ownerGlobal: {openLinkIn: sinon.spy(), whereToOpenLink: e => e.where}}}
       };
       feed.onAction(openLinkAction);
-      assert.calledWith(openLinkAction._target.browser.loadURI, openLinkAction.data.url, `URI:${openLinkAction.data.referrer}`);
+      assert.calledWith(openLinkAction._target.browser.ownerGlobal.openLinkIn, openLinkAction.data.url, "tab", {referrerURI: `URI:${openLinkAction.data.referrer}`});
     });
     it("should save to Pocket on SAVE_TO_POCKET", () => {
       feed.onAction({type: at.SAVE_TO_POCKET, data: {site: {url: "raspberry.com", title: "raspberry"}}, _target: {browser: {}}});
       assert.calledWith(global.Pocket.savePage, {}, "raspberry.com", "raspberry");
     });
   });
 
   describe("#observe", () => {
--- a/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
@@ -1,82 +1,96 @@
 const {SnippetsFeed} = require("lib/SnippetsFeed.jsm");
 const {actionTypes: at} = require("common/Actions.jsm");
 const {GlobalOverrider} = require("test/unit/utils");
+const {createStore, combineReducers} = require("redux");
+const {reducers} = require("common/Reducers.jsm");
 
 const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
 
 let overrider = new GlobalOverrider();
 
 describe("SnippetsFeed", () => {
   let sandbox;
+  let store;
   let clock;
   beforeEach(() => {
     clock = sinon.useFakeTimers();
     overrider.set({
       ProfileAge: class ProfileAge {
         constructor() {
           this.created = Promise.resolve(0);
           this.reset = Promise.resolve(WEEK_IN_MS);
         }
       }
     });
     sandbox = sinon.sandbox.create();
+    store = createStore(combineReducers(reducers));
+    sinon.spy(store, "dispatch");
   });
   afterEach(() => {
     clock.restore();
     overrider.restore();
     sandbox.restore();
   });
   it("should dispatch a SNIPPETS_DATA action with the right data on INIT", async () => {
     const url = "foo.com/%STARTPAGE_VERSION%";
     sandbox.stub(global.Services.prefs, "getStringPref").returns(url);
     sandbox.stub(global.Services.prefs, "getBoolPref")
-      .withArgs("datareporting.healthreport.uploadEnabled")
-      .returns(true)
       .withArgs("browser.onboarding.notification.finished")
       .returns(false);
+    sandbox.stub(global.Services.prefs, "prefHasUserValue")
+      .withArgs("services.sync.username")
+      .returns(true);
+    sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
 
     const feed = new SnippetsFeed();
-    feed.store = {dispatch: sandbox.stub()};
-
+    feed.store = store;
     clock.tick(WEEK_IN_MS * 2);
 
     await feed.init();
 
-    assert.calledOnce(feed.store.dispatch);
+    const state = store.getState().Snippets;
 
-    const action = feed.store.dispatch.firstCall.args[0];
-    assert.propertyVal(action, "type", at.SNIPPETS_DATA);
-    assert.isObject(action.data);
-    assert.propertyVal(action.data, "snippetsURL", "foo.com/5");
-    assert.propertyVal(action.data, "version", 5);
-    assert.propertyVal(action.data, "profileCreatedWeeksAgo", 2);
-    assert.propertyVal(action.data, "profileResetWeeksAgo", 1);
-    assert.propertyVal(action.data, "telemetryEnabled", true);
-    assert.propertyVal(action.data, "onboardingFinished", false);
+    assert.propertyVal(state, "snippetsURL", "foo.com/5");
+    assert.propertyVal(state, "version", 5);
+    assert.propertyVal(state, "profileCreatedWeeksAgo", 2);
+    assert.propertyVal(state, "profileResetWeeksAgo", 1);
+    assert.propertyVal(state, "telemetryEnabled", false);
+    assert.propertyVal(state, "onboardingFinished", false);
+    assert.propertyVal(state, "fxaccount", true);
+  });
+  it("should update telemetryEnabled on each new tab", () => {
+    sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
+    const feed = new SnippetsFeed();
+    feed.store = store;
+
+    feed.onAction({type: at.NEW_TAB_INIT});
+
+    const state = store.getState().Snippets;
+    assert.propertyVal(state, "telemetryEnabled", false);
   });
   it("should call .init on an INIT aciton", () => {
     const feed = new SnippetsFeed();
     sandbox.stub(feed, "init");
-    feed.store = {dispatch: sandbox.stub()};
+    feed.store = store;
 
     feed.onAction({type: at.INIT});
     assert.calledOnce(feed.init);
   });
   it("should call .init when a FEED_INIT happens for feeds.snippets", () => {
     const feed = new SnippetsFeed();
     sandbox.stub(feed, "init");
-    feed.store = {dispatch: sandbox.stub()};
+    feed.store = store;
 
     feed.onAction({type: at.FEED_INIT, data: "feeds.snippets"});
 
     assert.calledOnce(feed.init);
   });
   it("should dispatch a SNIPPETS_RESET on uninit", () => {
     const feed = new SnippetsFeed();
-    feed.store = {dispatch: sandbox.stub()};
+    feed.store = store;
 
     feed.uninit();
 
     assert.calledWith(feed.store.dispatch, {type: at.SNIPPETS_RESET});
   });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
@@ -1,14 +1,14 @@
 // Any copyright is dedicated to the Public Domain.
 // http://creativecommons.org/publicdomain/zero/1.0/
 
 const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
 const {TelemetrySender, TelemetrySenderConstants} = require("lib/TelemetrySender.jsm");
-const {ENDPOINT_PREF, FHR_UPLOAD_ENABLED_PREF, TELEMETRY_PREF, LOGGING_PREF} =
+const {ENDPOINT_PREF, TELEMETRY_PREF, LOGGING_PREF} =
   TelemetrySenderConstants;
 
 /**
  * A reference to the fake preferences object created by the TelemetrySender
  * constructor so that we can use the API.
  */
 let fakePrefs;
 const prefInitHook = function() {
@@ -47,108 +47,108 @@ describe("TelemetrySender", () => {
 
     tSender = new TelemetrySender(tsArgs);
 
     assert.calledOnce(global.Preferences);
   });
 
   describe("#enabled", () => {
     let testParams = [
-      {enabledPref: true, fhrPref: true, result: true},
-      {enabledPref: false, fhrPref: true, result: false},
-      {enabledPref: true, fhrPref: false, result: false},
-      {enabledPref: false, fhrPref: false, result: false}
+      {enabledPref: true, canRecordBase: true, result: true},
+      {enabledPref: false, canRecordBase: true, result: false},
+      {enabledPref: true, canRecordBase: false, result: false},
+      {enabledPref: false, canRecordBase: false, result: false}
     ];
 
     function testEnabled(p) {
       FakePrefs.prototype.prefs[TELEMETRY_PREF] = p.enabledPref;
-      FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = p.fhrPref;
+      sandbox.stub(global.Services.telemetry, "canRecordBase").value(p.canRecordBase);
 
       tSender = new TelemetrySender(tsArgs);
 
       assert.equal(tSender.enabled, p.result);
     }
 
     for (let p of testParams) {
-      it(`should return ${p.result} if the fhrPref is ${p.fhrPref} and telemetry.enabled is ${p.enabledPref}`, () => {
+      it(`should return ${p.result} if the Services.telemetry.canRecordBase is ${p.canRecordBase} and telemetry.enabled is ${p.enabledPref}`, () => {
         testEnabled(p);
       });
     }
 
     describe("telemetry.enabled pref changes from true to false", () => {
       beforeEach(() => {
         FakePrefs.prototype.prefs = {};
         FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
-        FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
+        sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
         tSender = new TelemetrySender(tsArgs);
         assert.propertyVal(tSender, "enabled", true);
       });
 
       it("should set the enabled property to false", () => {
         fakePrefs.set(TELEMETRY_PREF, false);
 
         assert.propertyVal(tSender, "enabled", false);
       });
     });
 
     describe("telemetry.enabled pref changes from false to true", () => {
       beforeEach(() => {
         FakePrefs.prototype.prefs = {};
-        FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
+        sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
         FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;
         tSender = new TelemetrySender(tsArgs);
 
         assert.propertyVal(tSender, "enabled", false);
       });
 
       it("should set the enabled property to true", () => {
         fakePrefs.set(TELEMETRY_PREF, true);
 
         assert.propertyVal(tSender, "enabled", true);
       });
     });
 
-    describe("FHR enabled pref changes from true to false", () => {
+    describe("canRecordBase changes from true to false", () => {
       beforeEach(() => {
         FakePrefs.prototype.prefs = {};
         FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
-        FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
+        sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
         tSender = new TelemetrySender(tsArgs);
         assert.propertyVal(tSender, "enabled", true);
       });
 
       it("should set the enabled property to false", () => {
-        fakePrefs.set(FHR_UPLOAD_ENABLED_PREF, false);
+        sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
 
         assert.propertyVal(tSender, "enabled", false);
       });
     });
 
-    describe("FHR enabled pref changes from false to true", () => {
+    describe("canRecordBase changes from false to true", () => {
       beforeEach(() => {
         FakePrefs.prototype.prefs = {};
-        FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = false;
+        sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
         FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
         tSender = new TelemetrySender(tsArgs);
 
         assert.propertyVal(tSender, "enabled", false);
       });
 
       it("should set the enabled property to true", () => {
-        fakePrefs.set(FHR_UPLOAD_ENABLED_PREF, true);
+        sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
 
         assert.propertyVal(tSender, "enabled", true);
       });
     });
   });
 
   describe("#sendPing()", () => {
     beforeEach(() => {
       FakePrefs.prototype.prefs = {};
-      FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
+      sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
       FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
       FakePrefs.prototype.prefs[ENDPOINT_PREF] = fakeEndpointUrl;
       tSender = new TelemetrySender(tsArgs);
     });
 
     it("should not send if the TelemetrySender is disabled", async () => {
       FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;
       tSender = new TelemetrySender(tsArgs);
@@ -203,25 +203,16 @@ describe("TelemetrySender", () => {
       tSender = new TelemetrySender(tsArgs);
       assert.property(fakePrefs.observers, TELEMETRY_PREF);
 
       tSender.uninit();
 
       assert.notProperty(fakePrefs.observers, TELEMETRY_PREF);
     });
 
-    it("should remove the fhrpref listener", () => {
-      tSender = new TelemetrySender(tsArgs);
-      assert.property(fakePrefs.observers, FHR_UPLOAD_ENABLED_PREF);
-
-      tSender.uninit();
-
-      assert.notProperty(fakePrefs.observers, FHR_UPLOAD_ENABLED_PREF);
-    });
-
     it("should remove the telemetry log listener", () => {
       tSender = new TelemetrySender(tsArgs);
       assert.property(fakePrefs.observers, LOGGING_PREF);
 
       tSender.uninit();
 
       assert.notProperty(fakePrefs.observers, TELEMETRY_PREF);
     });
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/TippyTopProvider.test.js
@@ -0,0 +1,71 @@
+"use strict";
+const {TippyTopProvider} = require("lib/TippyTopProvider.jsm");
+const {GlobalOverrider} = require("test/unit/utils");
+
+describe("TippyTopProvider", () => {
+  let instance;
+  let globals;
+  beforeEach(async () => {
+    globals = new GlobalOverrider();
+    let fetchStub = globals.sandbox.stub();
+    globals.set("fetch", fetchStub);
+    fetchStub.resolves({
+      ok: true,
+      status: 200,
+      json: () => Promise.resolve([{
+        "title": "facebook",
+        "url": "https://www.facebook.com/",
+        "image_url": "facebook-com.png",
+        "background_color": "#3b5998",
+        "domain": "facebook.com"
+      }, {
+        "title": "gmail",
+        "urls": ["https://www.gmail.com/", "https://mail.google.com"],
+        "image_url": "gmail-com.png",
+        "background_color": "#000000",
+        "domain": "gmail.com"
+      }])
+    });
+    instance = new TippyTopProvider();
+    await instance.init();
+  });
+  it("should provide an icon for facebook.com", () => {
+    const site = instance.processSite({url: "https://facebook.com"});
+    assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/facebook-com.png");
+    assert.equal(site.backgroundColor, "#3b5998");
+  });
+  it("should provide an icon for www.facebook.com", () => {
+    const site = instance.processSite({url: "https://www.facebook.com"});
+    assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/facebook-com.png");
+    assert.equal(site.backgroundColor, "#3b5998");
+  });
+  it("should not provide an icon for facebook.com/foobar", () => {
+    const site = instance.processSite({url: "https://facebook.com/foobar"});
+    assert.isUndefined(site.tippyTopIcon);
+    assert.isUndefined(site.backgroundColor);
+  });
+  it("should provide an icon for gmail.com", () => {
+    const site = instance.processSite({url: "https://gmail.com"});
+    assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/gmail-com.png");
+    assert.equal(site.backgroundColor, "#000000");
+  });
+  it("should provide an icon for mail.google.com", () => {
+    const site = instance.processSite({url: "https://mail.google.com"});
+    assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/gmail-com.png");
+    assert.equal(site.backgroundColor, "#000000");
+  });
+  it("should handle garbage URLs gracefully", () => {
+    const site = instance.processSite({url: "garbagejlfkdsa"});
+    assert.isUndefined(site.tippyTopIcon);
+    assert.isUndefined(site.backgroundColor);
+  });
+  it("should handle error when fetching and parsing manifest", async () => {
+    globals = new GlobalOverrider();
+    let fetchStub = globals.sandbox.stub();
+    globals.set("fetch", fetchStub);
+    fetchStub.rejects("whaaaa");
+    instance = new TippyTopProvider();
+    await instance.init();
+    instance.processSite("https://facebook.com");
+  });
+});
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -1,109 +1,214 @@
 "use strict";
 const injector = require("inject!lib/TopSitesFeed.jsm");
 const {UPDATE_TIME, TOP_SITES_SHOWMORE_LENGTH} = require("lib/TopSitesFeed.jsm");
 const {FakePrefs, GlobalOverrider} = require("test/unit/utils");
 const action = {meta: {fromTarget: {}}};
 const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
 const {insertPinned} = require("common/Reducers.jsm");
-const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `site${i}.com`}));
+const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `http://www.site${i}.com`}));
 const FAKE_SCREENSHOT = "data123";
 
+function FakeTippyTopProvider() {}
+FakeTippyTopProvider.prototype = {
+  init() {},
+  processSite(site) { return site; }
+};
+
 describe("Top Sites Feed", () => {
   let TopSitesFeed;
   let DEFAULT_TOP_SITES;
   let feed;
   let globals;
   let sandbox;
   let links;
   let clock;
   let fakeNewTabUtils;
   let fakeScreenshot;
+  let shortURLStub;
 
   beforeEach(() => {
     globals = new GlobalOverrider();
     sandbox = globals.sandbox;
     fakeNewTabUtils = {
       activityStreamLinks: {getTopSites: sandbox.spy(() => Promise.resolve(links))},
       pinnedLinks: {
         links: [],
         isPinned: () => false,
         pin: sandbox.spy(),
         unpin: sandbox.spy()
       }
     };
     fakeScreenshot = {getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))};
+    shortURLStub = sinon.stub().callsFake(site => site.url);
+    const fakeDedupe = function() {};
     globals.set("NewTabUtils", fakeNewTabUtils);
     FakePrefs.prototype.prefs["default.sites"] = "https://foo.com/";
     ({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
       "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
+      "common/Dedupe.jsm": {Dedupe: fakeDedupe},
       "common/Reducers.jsm": {insertPinned},
-      "lib/Screenshots.jsm": {Screenshots: fakeScreenshot}
+      "lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
+      "lib/TippyTopProvider.jsm": {TippyTopProvider: FakeTippyTopProvider},
+      "common/ShortURL.jsm": {shortURL: shortURLStub}
     }));
     feed = new TopSitesFeed();
     feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
+    feed.dedupe.group = sites => sites;
     links = FAKE_LINKS;
     clock = sinon.useFakeTimers();
   });
   afterEach(() => {
     globals.restore();
     clock.restore();
   });
 
-  describe("#init", () => {
-    it("should add defaults on INIT", () => {
-      feed.onAction({type: at.INIT});
-      assert.ok(DEFAULT_TOP_SITES.length);
+  describe("#refreshDefaults", () => {
+    it("should add defaults on PREFS_INITIAL_VALUES", () => {
+      feed.onAction({type: at.PREFS_INITIAL_VALUES, data: {"default.sites": "https://foo.com"}});
+
+      assert.isAbove(DEFAULT_TOP_SITES.length, 0);
+    });
+    it("should add defaults on PREF_CHANGED", () => {
+      feed.onAction({type: at.PREF_CHANGED, data: {name: "default.sites", value: "https://foo.com"}});
+
+      assert.isAbove(DEFAULT_TOP_SITES.length, 0);
     });
     it("should have default sites with .isDefault = true", () => {
-      feed.init();
+      feed.refreshDefaults("https://foo.com");
+
       DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "isDefault", true));
     });
     it("should add no defaults on empty pref", () => {
-      FakePrefs.prototype.prefs["default.sites"] = "";
-      feed.init();
+      feed.refreshDefaults("");
+
+      assert.equal(DEFAULT_TOP_SITES.length, 0);
+    });
+    it("should clear defaults", () => {
+      feed.refreshDefaults("https://foo.com");
+      feed.refreshDefaults("");
+
       assert.equal(DEFAULT_TOP_SITES.length, 0);
     });
   });
   describe("#getLinksWithDefaults", () => {
     beforeEach(() => {
-      feed.init();
+      feed.refreshDefaults("https://foo.com");
     });
 
     it("should get the links from NewTabUtils", async () => {
       const result = await feed.getLinksWithDefaults();
-      assert.deepEqual(result, links);
+      const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
+
+      assert.deepEqual(result, reference);
       assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
     });
+    it("should call dedupe on the links", async () => {
+      const stub = sinon.stub(feed.dedupe, "group", id => id);
+
+      await feed.getLinksWithDefaults();
+
+      assert.calledOnce(stub);
+    });
+    it("should dedupe the links by hostname", async () => {
+      const site = {url: "foo", hostname: "bar"};
+      const result = feed._dedupeKey(site);
+
+      assert.equal(result, site.hostname);
+    });
     it("should add defaults if there are are not enough links", async () => {
       links = [{url: "foo.com"}];
+
       const result = await feed.getLinksWithDefaults();
-      assert.deepEqual(result, [{url: "foo.com"}, ...DEFAULT_TOP_SITES]);
+      const reference = [...links, ...DEFAULT_TOP_SITES].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
+
+      assert.deepEqual(result, reference);
     });
     it("should only add defaults up to TOP_SITES_SHOWMORE_LENGTH", async () => {
-      links = new Array(TOP_SITES_SHOWMORE_LENGTH - 1).fill({url: "foo.com"});
+      links = [];
+      for (let i = 0; i < TOP_SITES_SHOWMORE_LENGTH - 1; i++) {
+        links.push({url: `foo${i}.com`});
+      }
       const result = await feed.getLinksWithDefaults();
+      const reference = [...links, DEFAULT_TOP_SITES[0]].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
+
       assert.lengthOf(result, TOP_SITES_SHOWMORE_LENGTH);
-      assert.deepEqual(result, [...links, DEFAULT_TOP_SITES[0]]);
+      assert.deepEqual(result, reference);
     });
     it("should not throw if NewTabUtils returns null", () => {
       links = null;
       assert.doesNotThrow(() => {
         feed.getLinksWithDefaults(action);
       });
     });
+    describe("deduping", () => {
+      beforeEach(() => {
+        ({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
+          "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
+          "common/Reducers.jsm": {insertPinned},
+          "lib/Screenshots.jsm": {Screenshots: fakeScreenshot}
+        }));
+        feed = new TopSitesFeed();
+      });
+      it("should not dedupe pinned sites", async () => {
+        fakeNewTabUtils.pinnedLinks.links = [
+          {url: "https://developer.mozilla.org/en-US/docs/Web"},
+          {url: "https://developer.mozilla.org/en-US/docs/Learn"}
+        ];
+
+        const sites = await feed.getLinksWithDefaults();
+
+        assert.lengthOf(sites, 12);
+        assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
+        assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url);
+        assert.equal(sites[0].hostname, sites[1].hostname);
+      });
+      it("should not dedupe pinned sites", async () => {
+        fakeNewTabUtils.pinnedLinks.links = [
+          {url: "https://developer.mozilla.org/en-US/docs/Web"},
+          {url: "https://developer.mozilla.org/en-US/docs/Learn"}
+        ];
+        // These will be the frecent results.
+        links = [
+          {url: "https://developer.mozilla.org/en-US/docs/Web"},
+          {url: "https://developer.mozilla.org/en-US/docs/Learn"}
+        ];
+
+        const sites = await feed.getLinksWithDefaults();
+
+        // Frecent results are removed and only pinned are kept.
+        assert.lengthOf(sites, 2);
+      });
+      it("should return sites that have a title", async () => {
+        // Simulate a pinned link with no pinTitle.
+        fakeNewTabUtils.pinnedLinks.links = [{url: "https://github.com/mozilla/activity-stream"}];
+
+        const sites = await feed.getLinksWithDefaults();
+
+        for (const site of sites) {
+          assert.isDefined(site.pinTitle || site.hostname);
+        }
+      });
+      it("should check against null entries", async () => {
+        fakeNewTabUtils.pinnedLinks.links = [null];
+
+        await feed.getLinksWithDefaults();
+      });
+    });
   });
   describe("#refresh", () => {
     it("should dispatch an action with the links returned", async () => {
       sandbox.stub(feed, "getScreenshot");
       await feed.refresh(action);
+      const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
+
       assert.calledOnce(feed.store.dispatch);
       assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
-      assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, links);
+      assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, reference);
     });
     it("should reuse screenshots for existing links, and call feed.getScreenshot for others", async () => {
       sandbox.stub(feed, "getScreenshot");
       const rows = [{url: FAKE_LINKS[0].url, screenshot: "foo.jpg"}];
       feed.store.getState = () => ({TopSites: {rows}});
       await feed.refresh(action);
 
       const results = feed.store.dispatch.firstCall.args[0].data;
@@ -118,38 +223,37 @@ describe("Top Sites Feed", () => {
     });
     it("should handle empty slots in the resulting top sites array", async () => {
       links = [FAKE_LINKS[0]];
       fakeNewTabUtils.pinnedLinks.links = [null, null, FAKE_LINKS[1], null, null, null, null, null, FAKE_LINKS[2]];
       sandbox.stub(feed, "getScreenshot");
       await feed.refresh(action);
       assert.calledOnce(feed.store.dispatch);
     });
+    it("should skip getting screenshot if there is a tippy top icon", async () => {
+      sandbox.stub(feed, "getScreenshot");
+      feed._tippyTopProvider.processSite = site => {
+        site.tippyTopIcon = "icon.png";
+        site.backgroundColor = "#fff";
+        return site;
+      };
+      await feed.refresh(action);
+      assert.calledOnce(feed.store.dispatch);
+      assert.notCalled(feed.getScreenshot);
+    });
   });
   describe("getScreenshot", () => {
     it("should call Screenshots.getScreenshotForURL with the right url", async () => {
       const url = "foo.com";
       await feed.getScreenshot(url);
       assert.calledWith(fakeScreenshot.getScreenshotForURL, url);
     });
   });
   describe("#onAction", () => {
     const newTabAction = {type: at.NEW_TAB_LOAD, meta: {fromTarget: "target"}};
-    it("should call refresh if there are not enough sites on NEW_TAB_LOAD", () => {
-      feed.store.getState = function() { return {TopSites: {rows: []}}; };
-      sinon.stub(feed, "refresh");
-      feed.onAction(newTabAction);
-      assert.calledWith(feed.refresh, newTabAction.meta.fromTarget);
-    });
-    it("should call refresh if there are not sites on NEW_TAB_LOAD, not counting defaults", () => {
-      feed.store.getState = function() { return {TopSites: {rows: [{url: "foo.com"}, ...DEFAULT_TOP_SITES]}}; };
-      sinon.stub(feed, "refresh");
-      feed.onAction(newTabAction);
-      assert.calledWith(feed.refresh, newTabAction.meta.fromTarget);
-    });
     it("should not call refresh if there are enough sites on NEW_TAB_LOAD", () => {
       feed.lastUpdated = Date.now();
       sinon.stub(feed, "refresh");
       feed.onAction(newTabAction);
       assert.notCalled(feed.refresh);
     });
     it("should call refresh if .lastUpdated is too old on NEW_TAB_LOAD", () => {
       feed.lastUpdated = 0;
--- a/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
@@ -4,54 +4,64 @@ const {FakePrefs} = require("test/unit/u
 const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
 const {GlobalOverrider} = require("test/unit/utils");
 
 describe("Top Stories Feed", () => {
   let TopStoriesFeed;
   let STORIES_UPDATE_TIME;
   let TOPICS_UPDATE_TIME;
   let SECTION_ID;
+  let FEED_PREF;
+  let SECTION_OPTIONS_PREF;
   let instance;
   let clock;
   let globals;
+  let shortURLStub;
 
   beforeEach(() => {
     FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
-      "stories_endpoint": "https://somedomain.org/stories?key=$apiKey&locale=$locale",
+      "stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
       "stories_referrer": "https://somedomain.org/referrer",
-      "topics_endpoint": "https://somedomain.org/topics?key=$apiKey&locale=$locale",
+      "topics_endpoint": "https://somedomain.org/topics?key=$apiKey",
       "survey_link": "https://www.surveymonkey.com/r/newtabffx",
       "api_key_pref": "apiKeyPref",
       "provider_name": "test-provider",
       "provider_icon": "provider-icon",
       "provider_description": "provider_desc"
     }`;
     FakePrefs.prototype.prefs.apiKeyPref = "test-api-key";
 
     globals = new GlobalOverrider();
     globals.set("Services", {locale: {getRequestedLocale: () => "en-CA"}});
     clock = sinon.useFakeTimers();
 
-    ({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
+    shortURLStub = sinon.stub().callsFake(site => site.url);
+
+    ({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID, FEED_PREF, SECTION_OPTIONS_PREF} = injector({
+      "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
+      "common/ShortURL.jsm": {shortURL: shortURLStub}
+    }));
     instance = new TopStoriesFeed();
     instance.store = {getState() { return {}; }, dispatch: sinon.spy()};
+    instance.storiesLastUpdated = 0;
+    instance.topicsLastUpdated = 0;
   });
   afterEach(() => {
     globals.restore();
     clock.restore();
   });
   describe("#init", () => {
     it("should create a TopStoriesFeed", () => {
       assert.instanceOf(instance, TopStoriesFeed);
     });
     it("should initialize endpoints based on prefs", () => {
       instance.onAction({type: at.INIT});
-      assert.equal("https://somedomain.org/stories?key=test-api-key&locale=en-CA", instance.stories_endpoint);
+      assert.equal("https://somedomain.org/stories?key=test-api-key", instance.stories_endpoint);
       assert.equal("https://somedomain.org/referrer", instance.stories_referrer);
-      assert.equal("https://somedomain.org/topics?key=test-api-key&locale=en-CA", instance.topics_endpoint);
+      assert.equal("https://somedomain.org/topics?key=test-api-key", instance.topics_endpoint);
     });
     it("should register section", () => {
       const expectedSectionOptions = {
         id: SECTION_ID,
         eventSource: "TOP_STORIES",
         icon: "provider-icon",
         title: {id: "header_recommended_by", values: {provider: "test-provider"}},
         rows: [],
@@ -112,39 +122,32 @@ describe("Top Stories Feed", () => {
       FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
         "stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
         "topics_endpoint": "https://somedomain.org/topics?key=$apiKey"
       }`;
       instance.init();
 
       assert.called(Components.utils.reportError);
     });
-    it("should report error for missing locale", () => {
-      let fakeServices = {locale: {getRequestedLocale: sinon.spy()}};
-      globals.set("Services", fakeServices);
-      globals.sandbox.spy(global.Components.utils, "reportError");
-      FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
-        "stories_endpoint": "https://somedomain.org/stories?locale=$locale",
-        "topics_endpoint": "https://somedomain.org/topics?locale=$locale"
-      }`;
-      instance.init();
-
-      assert.called(Components.utils.reportError);
-    });
     it("should deregister section", () => {
       instance.onAction({type: at.UNINIT});
       assert.calledOnce(instance.store.dispatch);
       assert.calledWith(instance.store.dispatch, ac.BroadcastToContent({
         type: at.SECTION_DEREGISTER,
         data: SECTION_ID
       }));
     });
     it("should initialize on FEED_INIT", () => {
       instance.init = sinon.spy();
-      instance.onAction({type: at.FEED_INIT, data: "feeds.section.topstories"});
+      instance.onAction({type: at.FEED_INIT, data: FEED_PREF});
+      assert.calledOnce(instance.init);
+    });
+    it("should initialize on PREF_CHANGED", () => {
+      instance.init = sinon.spy();
+      instance.onAction({type: at.PREF_CHANGED, data: {name: SECTION_OPTIONS_PREF}});
       assert.calledOnce(instance.init);
     });
   });
   describe("#fetch", () => {
     it("should fetch stories and send event", async () => {
       let fetchStub = globals.sandbox.stub();
       globals.set("fetch", fetchStub);
       globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
@@ -158,25 +161,28 @@ describe("Top Stories Feed", () => {
       }]}`;
       const stories = [{
         "guid": "1",
         "type": "now",
         "title": "title",
         "description": "description",
         "image": "image-url",
         "referrer": "referrer",
-        "url": "rec-url"
+        "url": "rec-url",
+        "eTLD": "",
+        "hostname": "rec-url"
       }];
 
       instance.stories_endpoint = "stories-endpoint";
       instance.stories_referrer = "referrer";
       fetchStub.resolves({ok: true, status: 200, text: () => response});
       await instance.fetchStories();
 
       assert.calledOnce(fetchStub);
+      assert.calledOnce(shortURLStub);
       assert.calledWithExactly(fetchStub, instance.stories_endpoint);
       assert.calledOnce(instance.store.dispatch);
       assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE);
       assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.id, SECTION_ID);
       assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.rows, stories);
     });
     it("should dispatch events", () => {
       instance.dispatchUpdateEvent(123, {});
@@ -272,26 +278,24 @@ describe("Top Stories Feed", () => {
       assert.calledWithExactly(fetchStub, instance.topics_endpoint);
       assert.notCalled(instance.store.dispatch);
       assert.called(Components.utils.reportError);
     });
   });
   describe("#update", () => {
     it("should fetch stories after update interval", () => {
       instance.fetchStories = sinon.spy();
-      instance.fetchTopics = sinon.spy();
       instance.onAction({type: at.SYSTEM_TICK});
       assert.notCalled(instance.fetchStories);
 
       clock.tick(STORIES_UPDATE_TIME);
       instance.onAction({type: at.SYSTEM_TICK});
       assert.calledOnce(instance.fetchStories);
     });
     it("should fetch topics after update interval", () => {
-      instance.fetchStories = sinon.spy();
       instance.fetchTopics = sinon.spy();
       instance.onAction({type: at.SYSTEM_TICK});
       assert.notCalled(instance.fetchTopics);
 
       clock.tick(TOPICS_UPDATE_TIME);
       instance.onAction({type: at.SYSTEM_TICK});
       assert.calledOnce(instance.fetchTopics);
     });
--- a/browser/extensions/activity-stream/test/unit/unit-entry.js
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -23,42 +23,53 @@ overrider.set({
     }
   },
   // eslint-disable-next-line object-shorthand
   ContentSearchUIController: function() {}, // NB: This is a function/constructor
   dump() {},
   fetch() {},
   Preferences: FakePrefs,
   Services: {
-    locale: {getRequestedLocale() {}},
+    telemetry: {
+      canRecordBase: true,
+      canRecordExtended: true
+    },
+    locale: {
+      getAppLocalesAsLangTags() {},
+      getRequestedLocale() {},
+      negotiateLanguages() {}
+    },
     urlFormatter: {formatURL: str => str},
     mm: {
       addMessageListener: (msg, cb) => cb(),
       removeMessageListener() {}
     },
     appShell: {hiddenDOMWindow: {performance: new FakePerformance()}},
     obs: {
       addObserver() {},
       removeObserver() {}
     },
     prefs: {
       addObserver() {},
+      prefHasUserValue() {},
       removeObserver() {},
       getStringPref() {},
       getBoolPref() {},
       getDefaultBranch() {
         return {
           setBoolPref() {},
           setIntPref() {},
           setStringPref() {},
           clearUserPref() {}
         };
       }
     },
-    tm: {dispatchToMainThread: cb => cb()}
+    tm: {dispatchToMainThread: cb => cb()},
+    eTLD: {getPublicSuffix() {}},
+    io: {NewURI() {}}
   },
   XPCOMUtils: {
     defineLazyModuleGetter() {},
     defineLazyServiceGetter() {},
     generateQI() { return {}; }
   }
 });
 
--- a/browser/extensions/followonsearch/bootstrap.js
+++ b/browser/extensions/followonsearch/bootstrap.js
@@ -42,33 +42,33 @@ function log(message) {
 
 /**
  * Handles receiving a message from the content process to save telemetry.
  *
  * @param {Object} message The message received.
  */
 function handleSaveTelemetryMsg(message) {
   if (message.name != kSaveTelemetryMsg) {
-    throw new Error(`Unexpected message received: ${kSaveTelemetryMsg}`);
+    throw new Error(`Unexpected message received: ${message.name}`);
   }
 
   let info = message.data;
 
   if (!validSearchTypes.includes(info.type)) {
     throw new Error("Unexpected type!");
   }
 
   log(info);
 
   let histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
   histogram.add(`${info.sap}.${info.type}:unknown:${info.code}`);
 }
 
 /**
- * Activites recording of telemetry if it isn't already activated.
+ * Activates recording of telemetry if it isn't already activated.
  */
 function activateTelemetry() {
   if (gTelemetryActivated) {
     return;
   }
 
   gTelemetryActivated = true;
 
--- a/browser/extensions/followonsearch/content/followonsearch-fs.js
+++ b/browser/extensions/followonsearch/content/followonsearch-fs.js
@@ -30,17 +30,17 @@ let searchDomains = [{
   "prefix": "pc",
   "reportPrefix": "form",
   "codes": ["MOZI"],
   "sap": "bing",
 }, {
   // The Yahoo domains to watch for.
   "domains": [
     "search.yahoo.com", "ca.search.yahoo.com", "hk.search.yahoo.com",
-    "tw.search.yahoo.com"
+    "tw.search.yahoo.com", "mozilla.search.yahoo.com", "us.search.yahoo.com"
   ],
   "search": "p",
   "followOnSearch": "fr2",
   "prefix": "hspart",
   "reportPrefix": "hsimp",
   "codes": ["mozilla"],
   "sap": "yahoo",
 }, {
--- a/browser/extensions/followonsearch/install.rdf
+++ b/browser/extensions/followonsearch/install.rdf
@@ -2,17 +2,17 @@
 <!-- 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/. -->
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
      xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>followonsearch@mozilla.com</em:id>
     <em:name>Follow-on Search Telemetry</em:name>
-    <em:version>0.9.1</em:version>
+    <em:version>0.9.2</em:version>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>52.0</em:minVersion>
         <em:maxVersion>59.*</em:maxVersion>
--- a/browser/extensions/formautofill/.eslintrc.js
+++ b/browser/extensions/formautofill/.eslintrc.js
@@ -65,19 +65,16 @@ module.exports = {
     "comma-dangle": ["error", "always-multiline"],
 
     // Warn about cyclomatic complexity in functions.
     "complexity": ["error", {"max": 20}],
 
     // Enforce dots on the next line with property name.
     "dot-location": ["error", "property"],
 
-    // Encourage the use of dot notation whenever possible.
-    "dot-notation": "error",
-
     // Maximum length of a line.
     // This should be 100 but too many lines were longer than that so set a
     // conservative upper bound for now.
     "max-len": ["error", 140],
 
     // Maximum depth callbacks can be nested.
     "max-nested-callbacks": ["error", 4],
 
--- a/browser/extensions/pocket/bootstrap.js
+++ b/browser/extensions/pocket/bootstrap.js
@@ -11,16 +11,18 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+                                  "resource:///modules/PageActions.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
                                   "resource://gre/modules/ReaderMode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Pocket",
                                   "chrome://pocket/content/Pocket.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AboutPocket",
                                   "chrome://pocket/content/AboutPocket.jsm");
@@ -145,39 +147,89 @@ function CreatePocketWidget(reason) {
     let widgets = CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR);
     let bmbtn = widgets.indexOf("bookmarks-menu-button");
     if (bmbtn > -1) {
       CustomizableUI.moveWidgetWithinArea("pocket-button", bmbtn + 1);
     }
   }
 }
 
+function isPocketEnabled() {
+  return PocketPageAction.shouldUse ? PocketPageAction.enabled :
+         !!CustomizableUI.getPlacementOfWidget("pocket-button");
+}
+
+var PocketPageAction = {
+  pageAction: null,
+
+  get shouldUse() {
+    return !Services.prefs.getBranch(PREF_BRANCH)
+                    .getBoolPref("disablePageAction", false) &&
+           AppConstants.MOZ_PHOTON_THEME;
+  },
+
+  get enabled() {
+    return !!this.pageAction;
+  },
+
+  init() {
+    if (!this.shouldUse) {
+      return;
+    }
+    let id = "pocket";
+    this.pageAction = PageActions.actionForID(id);
+    if (!this.pageAction) {
+      this.pageAction = PageActions.addAction(new PageActions.Action({
+        id,
+        title: gPocketBundle.GetStringFromName("pocket-button.label"),
+        shownInUrlbar: true,
+        wantsIframe: true,
+        _insertBeforeActionID: PageActions.ACTION_ID_BOOKMARK_SEPARATOR,
+        onPlacedInPanel(panelNode, urlbarNode) {
+          PocketOverlay.onWindowOpened(panelNode.ownerGlobal);
+        },
+        onIframeShown(iframe, panel) {
+          Pocket.onShownInPhotonPageActionPanel(panel, iframe);
+        },
+      }));
+    }
+  },
+
+  shutdown() {
+    if (!this.pageAction) {
+      return;
+    }
+    this.pageAction.remove();
+    this.pageAction = null;
+  },
+};
+
 // PocketContextMenu
 // When the context menu is opened check if we need to build and enable pocket UI.
 var PocketContextMenu = {
   init() {
     Services.obs.addObserver(this, "on-build-contextmenu");
   },
   shutdown() {
     Services.obs.removeObserver(this, "on-build-contextmenu");
     // loop through windows and remove context menus
     // iterate through all windows and add pocket to them
-    for (let win of CustomizableUI.windows) {
+    for (let win of browserWindows()) {
       let document = win.document;
       for (let id of ["context-pocket", "context-savelinktopocket"]) {
         let element = document.getElementById(id);
         if (element)
           element.remove();
       }
     }
   },
   observe(aSubject, aTopic, aData) {
     let subject = aSubject.wrappedJSObject;
     let document = subject.menu.ownerDocument;
-    let pocketEnabled = CustomizableUI.getPlacementOfWidget("pocket-button");
+    let pocketEnabled = isPocketEnabled();
 
     let showSaveCurrentPageToPocket = !(subject.onTextInput || subject.onLink ||
                                         subject.isContentSelected || subject.onImage ||
                                         subject.onCanvas || subject.onVideo || subject.onAudio);
     let targetUrl = subject.onLink ? subject.linkUrl : subject.pageUrl;
     let targetURI = Services.io.newURI(targetUrl);
     let canPocket = pocketEnabled && (targetURI.schemeIs("http") || targetURI.schemeIs("https") ||
                     (targetURI.schemeIs("about") && ReaderMode.getOriginalUrl(targetUrl)));
@@ -267,27 +319,34 @@ var PocketReader = {
         message.target.messageManager.
           sendAsyncMessage("Reader:AddButton", { id: "pocket-button",
                                                  title: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
                                                  image: "chrome://pocket/content/panels/img/pocket.svg#pocket-mark"});
         break;
       }
       case "Reader:Clicked-pocket-button": {
         let doc = message.target.ownerDocument;
-        let pocketWidget = doc.getElementById("pocket-button");
-        let placement = CustomizableUI.getPlacementOfWidget("pocket-button");
-        if (placement) {
-          if (placement.area == CustomizableUI.AREA_PANEL) {
-            doc.defaultView.PanelUI.show().then(function() {
-              // The DOM node might not exist yet if the panel wasn't opened before.
-              pocketWidget = doc.getElementById("pocket-button");
+        if (PocketPageAction.shouldUse) {
+          // TODO: PageActions should make this easier.
+          let event = new doc.defaultView.CustomEvent("command");
+          let panelButton = doc.getElementById("pageAction-panel-pocket");
+          panelButton.dispatchEvent(event);
+        } else {
+          let pocketWidget = doc.getElementById("pocket-button");
+          let placement = CustomizableUI.getPlacementOfWidget("pocket-button");
+          if (placement) {
+            if (placement.area == CustomizableUI.AREA_PANEL) {
+              doc.defaultView.PanelUI.show().then(function() {
+                // The DOM node might not exist yet if the panel wasn't opened before.
+                pocketWidget = doc.getElementById("pocket-button");
+                pocketWidget.doCommand();
+              });
+            } else {
               pocketWidget.doCommand();
-            });
-          } else {
-            pocketWidget.doCommand();
+            }
           }
         }
         break;
       }
     }
   }
 }
 
@@ -311,75 +370,87 @@ var PocketOverlay = {
   startup(reason) {
     let styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
                               .getService(Ci.nsIStyleSheetService);
     this._sheetType = styleSheetService.AUTHOR_SHEET;
     this._cachedSheet = styleSheetService.preloadSheet(gPocketStyleURI,
                                                        this._sheetType);
     Services.ppmm.loadProcessScript(PROCESS_SCRIPT, true);
     PocketReader.startup();
-    CustomizableUI.addListener(this);
-    CreatePocketWidget(reason);
+    if (PocketPageAction.shouldUse) {
+      PocketPageAction.init();
+    } else {
+      CustomizableUI.addListener(this);
+      CreatePocketWidget(reason);
+    }
     PocketContextMenu.init();
-
-    for (let win of CustomizableUI.windows) {
+    for (let win of browserWindows()) {
       this.onWindowOpened(win);
     }
   },
   shutdown(reason) {
     let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
                  .getService(Ci.nsIMessageBroadcaster);
     ppmm.broadcastAsyncMessage("PocketShuttingDown");
     // Although the ppmm loads the scripts into the chrome process as well,
     // we need to manually unregister here anyway to ensure these aren't part
     // of the chrome process and avoid errors.
     AboutPocket.aboutSaved.unregister();
     AboutPocket.aboutSignup.unregister();
 
-    CustomizableUI.removeListener(this);
-    for (let window of CustomizableUI.windows) {
+    if (PocketPageAction.shouldUse) {
+      PocketPageAction.shutdown();
+    } else {
+      CustomizableUI.removeListener(this);
+      CustomizableUI.destroyWidget("pocket-button");
+    }
+
+    for (let window of browserWindows()) {
       for (let id of ["panelMenu_pocket", "menu_pocket", "BMB_pocket",
                       "panelMenu_pocketSeparator", "menu_pocketSeparator",
                       "BMB_pocketSeparator", "appMenu-library-pocket-button"]) {
         let element = window.document.getElementById(id);
         if (element)
           element.remove();
       }
       this.removeStyles(window);
       // remove script getters/objects
       delete window.Pocket;
       delete window.pktApi;
       delete window.pktUI;
       delete window.pktUIMessaging;
     }
-    CustomizableUI.destroyWidget("pocket-button");
+
     PocketContextMenu.shutdown();
     PocketReader.shutdown();
   },
   onWindowOpened(window) {
     if (window.hasOwnProperty("pktUI"))
       return;
     this.setWindowScripts(window);
     this.addStyles(window);
     this.updateWindow(window);
+    if (PocketPageAction.shouldUse) {
+      this.updateWindowAfterWidgetPlaced(window);
+    }
   },
   setWindowScripts(window) {
     XPCOMUtils.defineLazyModuleGetter(window, "Pocket",
                                       "chrome://pocket/content/Pocket.jsm");
     // Can't use XPCOMUtils for these because the scripts try to define the variables
     // on window, and so the defineProperty inside defineLazyGetter fails.
     Object.defineProperty(window, "pktApi", pktUIGetter("pktApi", window));
     Object.defineProperty(window, "pktUI", pktUIGetter("pktUI", window));
     Object.defineProperty(window, "pktUIMessaging", pktUIGetter("pktUIMessaging", window));
   },
   // called for each window as it is opened
   updateWindow(window) {
     // insert our three menu items
     let document = window.document;
-    let hidden = !CustomizableUI.getPlacementOfWidget("pocket-button");
+    let hidden = !isPocketEnabled();
 
     // add to bookmarksMenu
     let sib = document.getElementById("menu_bookmarkThisPage");
     if (sib && !document.getElementById("menu_pocket")) {
       let menu = createElementWithAttrs(document, "menuitem", {
         "id": "menu_pocket",
         "label": gPocketBundle.GetStringFromName("pocketMenuitem.label"),
         "class": "menuitem-iconic", // OSX only
@@ -445,18 +516,22 @@ var PocketOverlay = {
       });
       sib.parentNode.insertBefore(menu, sib);
     }
   },
   onWidgetAfterDOMChange(aWidgetNode) {
     if (aWidgetNode.id != "pocket-button") {
       return;
     }
-    let doc = aWidgetNode.ownerDocument;
-    let hidden = !CustomizableUI.getPlacementOfWidget("pocket-button");
+    this.updateWindowAfterWidgetPlaced(aWidgetNode.ownerGlobal);
+  },
+
+  updateWindowAfterWidgetPlaced(browserWindow) {
+    let doc = browserWindow.document;
+    let hidden = !isPocketEnabled();
     let elementIds = [
       "panelMenu_pocket",
       "menu_pocket",
       "BMB_pocket",
       "appMenu-library-pocket-button",
     ];
     for (let elementId of elementIds) {
       let element = doc.getElementById(elementId);
@@ -520,8 +595,15 @@ function shutdown(data, reason) {
   }
 }
 
 function install() {
 }
 
 function uninstall() {
 }
+
+function* browserWindows() {
+  let windows = Services.wm.getEnumerator("navigator:browser");
+  while (windows.hasMoreElements()) {
+    yield windows.getNext();
+  }
+}
--- a/browser/extensions/pocket/content/Pocket.jsm
+++ b/browser/extensions/pocket/content/Pocket.jsm
@@ -23,19 +23,28 @@ var Pocket = {
 
   /**
    * Functions related to the Pocket panel UI.
    */
   onBeforeCommand(event) {
     BrowserUtils.setToolbarButtonHeightProperty(event.target);
   },
 
+  onShownInPhotonPageActionPanel(panel, iframe) {
+    let window = panel.ownerGlobal;
+    window.pktUI.setPhotonPageActionPanelFrame(iframe);
+    Pocket._initPanelView(window);
+  },
+
   onPanelViewShowing(event) {
-    let document = event.target.ownerDocument;
-    let window = document.defaultView;
+    Pocket._initPanelView(event.target.ownerGlobal);
+  },
+
+  _initPanelView(window) {
+    let document = window.document;
     let iframe = window.pktUI.getPanelFrame();
 
     let libraryButton = document.getElementById("library-button");
     if (libraryButton) {
       BrowserUtils.setToolbarButtonHeightProperty(libraryButton);
     }
 
     let urlToSave = Pocket._urlToSave;
--- a/browser/extensions/pocket/content/main.js
+++ b/browser/extensions/pocket/content/main.js
@@ -562,36 +562,54 @@ var pktUI = (function() {
         var frame = getPanelFrame();
         var panel = frame;
         while (panel && panel.localName != "panel") {
             panel = panel.parentNode;
         }
         return panel;
     }
 
+    var photonPageActionPanelFrame;
+
+    function setPhotonPageActionPanelFrame(frame) {
+        photonPageActionPanelFrame = frame;
+    }
+
     function getPanelFrame() {
+        if (photonPageActionPanelFrame) {
+            return photonPageActionPanelFrame;
+        }
+
         var frame = document.getElementById("pocket-panel-iframe");
         if (!frame) {
             var frameParent = document.getElementById("PanelUI-pocketView").firstChild;
             frame = document.createElement("iframe");
             frame.id = "pocket-panel-iframe";
             frame.setAttribute("type", "content");
             frameParent.appendChild(frame);
         }
         return frame;
     }
 
     function getSubview() {
+        if (photonPageActionPanelFrame) {
+            return null;
+        }
+
         var view = document.getElementById("PanelUI-pocketView");
         if (view && view.getAttribute("current") == "true" && !view.getAttribute("mainview"))
             return view;
         return null;
     }
 
     function isInOverflowMenu() {
+        if (photonPageActionPanelFrame) {
+            return false;
+        }
+
         var subview = getSubview();
         return !!subview;
     }
 
     function getFirefoxAccountSignedInUser(callback) {
         fxAccounts.getSignedInUser().then(userData => {
             callback(userData);
         }).then(null, error => {
@@ -602,16 +620,17 @@ var pktUI = (function() {
     function getUILocale() {
         return Services.locale.getAppLocaleAsLangTag();
     }
 
     /**
      * Public functions
      */
     return {
+        setPhotonPageActionPanelFrame,
         getPanelFrame,
 
         openTabWithUrl,
 
         pocketPanelDidShow,
         pocketPanelDidHide,
 
         tryToSaveUrl,
--- a/browser/extensions/pocket/content/pktApi.jsm
+++ b/browser/extensions/pocket/content/pktApi.jsm
@@ -169,42 +169,42 @@ var pktApi = (function() {
     /**
      * Returns access token or undefined if no logged in user was found
      * @return {string | undefined} Access token for logged in user user
      */
     function getAccessToken() {
         var pocketCookies = getCookiesFromPocket();
 
         // If no cookie was found just return undefined
-        if (typeof pocketCookies["ftv1"] === "undefined") {
+        if (typeof pocketCookies.ftv1 === "undefined") {
             return undefined;
         }
 
         // Check if a new user logged in in the meantime and clearUserData if so
-        var sessionId = pocketCookies["fsv1"];
+        var sessionId = pocketCookies.fsv1;
         var lastSessionId = getSetting("fsv1");
         if (sessionId !== lastSessionId) {
             clearUserData();
             setSetting("fsv1", sessionId);
         }
 
         // Return access token
-        return pocketCookies["ftv1"];
+        return pocketCookies.ftv1;
     }
 
     /**
      * Get the current premium status of the user
      * @return {number | undefined} Premium status of user
      */
     function getPremiumStatus() {
         var premiumStatus = getSetting("premium_status");
         if (typeof premiumStatus === "undefined") {
             // Premium status is not in settings try get it from cookie
             var pocketCookies = getCookiesFromPocket();
-            premiumStatus = pocketCookies["ps"];
+            premiumStatus = pocketCookies.ps;
         }
         return premiumStatus;
     }
 
     /**
      * Helper method to check if a user is premium or not
      * @return {Boolean} Boolean if user is premium or not
      */
--- a/browser/extensions/pocket/skin/shared/pocket.css
+++ b/browser/extensions/pocket/skin/shared/pocket.css
@@ -179,17 +179,17 @@ toolbar[brighttext] #pocket-button {
 #library-button[animate="pocket"] > .toolbarbutton-animatable-box > .toolbarbutton-animatable-image {
   height: var(--toolbarbutton-height); /* Height must be equal to height of toolbarbutton padding-box */
 }
 
 #library-button[animate="pocket"] > .toolbarbutton-animatable-box > .toolbarbutton-animatable-image {
   background-image: url("chrome://pocket-shared/skin/library-pocket-animation.svg");
   width: 1078px;
   animation-name: library-pocket-animation;
-  animation-duration: 768ms;
+  animation-duration: 800ms;
   animation-timing-function: steps(48);
 }
 
 #library-button[animate="pocket"]:-moz-locale-dir(rtl) > .toolbarbutton-animatable-box > .toolbarbutton-animatable-image {
   animation-name: library-pocket-animation-rtl;
   transform: scaleX(-1);
 }
 
@@ -243,8 +243,13 @@ toolbar[brighttext] #pocket-button {
   #BMB_pocket {
     list-style-image: url("chrome://pocket/content/panels/img/pocketmenuitem16@2x.png");
   }
 
   #panelMenu_pocket > .toolbarbutton-icon {
     width: 16px;
   }
 }
+
+#pageAction-panel-pocket,
+#pageAction-urlbar-pocket {
+  list-style-image: url("chrome://pocket-shared/skin/pocket.svg");
+}
--- a/browser/extensions/pocket/skin/shared/pocket.svg
+++ b/browser/extensions/pocket/skin/shared/pocket.svg
@@ -1,6 +1,6 @@
 <!-- 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/. -->
 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
-  <path fill="context-fill" d="M9,15.969a8,8,0,0,1-8-8v-4a2,2,0,0,1,2-2H15a2,2,0,0,1,2,2v4A8,8,0,0,1,9,15.969ZM12.985,5.937a0.99,0.99,0,0,0-.725.319L8.978,9.539,5.755,6.305A0.984,0.984,0,0,0,5,5.937a1,1,0,0,0-.714,1.7L4.27,7.648l3.293,3.306h0l0.707,0.707a1,1,0,0,0,1.414,0l0.707-.707h0L13.7,7.648l0,0A1,1,0,0,0,12.985,5.937Z"/>
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M9,15.969a8,8,0,0,1-8-8v-4a2,2,0,0,1,2-2H15a2,2,0,0,1,2,2v4A8,8,0,0,1,9,15.969ZM12.985,5.937a0.99,0.99,0,0,0-.725.319L8.978,9.539,5.755,6.305A0.984,0.984,0,0,0,5,5.937a1,1,0,0,0-.714,1.7L4.27,7.648l3.293,3.306h0l0.707,0.707a1,1,0,0,0,1.414,0l0.707-.707h0L13.7,7.648l0,0A1,1,0,0,0,12.985,5.937Z"/>
 </svg>
--- a/browser/extensions/presentation/content/PresentationDevicePrompt.jsm
+++ b/browser/extensions/presentation/content/PresentationDevicePrompt.jsm
@@ -162,17 +162,17 @@ PresentationPermissionPrompt.prototype =
         this.request.cancel(Cr.NS_ERROR_NOT_AVAILABLE);
       },
       dismiss: true,
     }];
   },
   // PRIVATE APIs
   get _domainName() {
     if (this.principal.URI instanceof Ci.nsIFileURL) {
-      return this.principal.URI.path.split("/")[1];
+      return this.principal.URI.pathQueryRef.split("/")[1];
     }
     return this.principal.URI.hostPort;
   },
   _createPopupContent() {
     log("_createPopupContent");
 
     if (!this._devices.length) {
       log("No available devices can be listed!");
--- a/browser/extensions/screenshots/bootstrap.js
+++ b/browser/extensions/screenshots/bootstrap.js
@@ -15,17 +15,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Console.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LegacyExtensionsUtils",
                                   "resource://gre/modules/LegacyExtensionsUtils.jsm");
 
 let addonResourceURI;
 let appStartupDone;
-const appStartupPromise = new Promise((resolve, reject) => {
+let appStartupPromise = new Promise((resolve, reject) => {
   appStartupDone = resolve;
 });
 
 const prefs = Services.prefs;
 const prefObserver = {
   register() {
     prefs.addObserver(PREF_BRANCH, this, false); // eslint-disable-line mozilla/no-useless-parameters
   },
@@ -34,17 +34,17 @@ const prefObserver = {
     prefs.removeObserver(PREF_BRANCH, this, false); // eslint-disable-line mozilla/no-useless-parameters
   },
 
   observe(aSubject, aTopic, aData) {
     // aSubject is the nsIPrefBranch we're observing (after appropriate QI)
     // aData is the name of the pref that's been changed (relative to aSubject)
     if (aData == USER_DISABLE_PREF || aData == SYSTEM_DISABLE_PREF) {
       // eslint-disable-next-line promise/catch-or-return
-      appStartupPromise.then(handleStartup);
+      appStartupPromise = appStartupPromise.then(handleStartup);
     }
   }
 };
 
 const appStartupObserver = {
   register() {
     Services.obs.addObserver(this, "sessionstore-windows-restored", false); // eslint-disable-line mozilla/no-useless-parameters
   },
@@ -67,26 +67,27 @@ function startup(data, reason) { // esli
   if (reason === APP_STARTUP) {
     appStartupObserver.register();
   } else {
     appStartupDone();
   }
   prefObserver.register();
   addonResourceURI = data.resourceURI;
   // eslint-disable-next-line promise/catch-or-return
-  appStartupPromise.then(handleStartup);
+  appStartupPromise = appStartupPromise.then(handleStartup);
 }
 
 function shutdown(data, reason) { // eslint-disable-line no-unused-vars
   prefObserver.unregister();
   const webExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
     id: ADDON_ID,
     resourceURI: addonResourceURI
   });
-  stop(webExtension, reason);
+  // Because the prefObserver is unregistered above, this _should_ terminate the promise chain.
+  appStartupPromise = appStartupPromise.then(() => { stop(webExtension, reason); });
 }
 
 function install(data, reason) {} // eslint-disable-line no-unused-vars
 
 function uninstall(data, reason) {} // eslint-disable-line no-unused-vars
 
 function getBoolPref(pref) {
   return prefs.getPrefType(pref) && prefs.getBoolPref(pref);
@@ -98,38 +99,39 @@ function shouldDisable() {
 
 function handleStartup() {
   const webExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
     id: ADDON_ID,
     resourceURI: addonResourceURI
   });
 
   if (!shouldDisable() && !webExtension.started) {
-    start(webExtension);
+    return start(webExtension);
   } else if (shouldDisable()) {
-    stop(webExtension, ADDON_DISABLE);
+    return stop(webExtension, ADDON_DISABLE);
   }
 }
 
 function start(webExtension) {
-  webExtension.startup(startupReason).then((api) => {
+  return webExtension.startup(startupReason).then((api) => {
     api.browser.runtime.onMessage.addListener(handleMessage);
+    return Promise.resolve(null);
   }).catch((err) => {
     // The startup() promise will be rejected if the webExtension was
     // already started (a harmless error), or if initializing the
     // WebExtension failed and threw (an important error).
     console.error(err);
     if (err.message !== "This embedded extension has already been started") {
       // TODO: Should we send these errors to Sentry? #2420
     }
   });
 }
 
 function stop(webExtension, reason) {
-  webExtension.shutdown(reason);
+  return Promise.resolve(webExtension.shutdown(reason));
 }
 
 function handleMessage(msg, sender, sendReply) {
   if (!msg) {
     return;
   }
 
   if (msg.funcName === "getTelemetryPref") {
--- a/browser/extensions/screenshots/install.rdf
+++ b/browser/extensions/screenshots/install.rdf
@@ -7,14 +7,14 @@
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
         <em:minVersion>51.0a1</em:minVersion>
         <em:maxVersion>*</em:maxVersion>
       </Description>
     </em:targetApplication>
     <em:type>2</em:type>
-    <em:version>10.9.0</em:version>
+    <em:version>10.10.0</em:version>
     <em:bootstrap>true</em:bootstrap>
     <em:homepageURL>https://pageshot.net/</em:homepageURL>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
   </Description>
 </RDF>
--- a/browser/extensions/screenshots/webextension/manifest.json
+++ b/browser/extensions/screenshots/webextension/manifest.json
@@ -1,12 +1,12 @@
 {
   "manifest_version": 2,
   "name": "Firefox Screenshots",
-  "version": "10.9.0",
+  "version": "10.10.0",
   "description": "__MSG_addonDescription__",
   "author": "__MSG_addonAuthorsList__",
   "homepage_url": "https://github.com/mozilla-services/screenshots",
   "applications": {
     "gecko": {
       "id": "screenshots@mozilla.org"
     }
   },
--- a/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js
@@ -19,44 +19,44 @@ function experimentFactory(attrs) {
     preferenceType: "string",
     previousPreferenceValue: "oldfakevalue",
     preferenceBranchType: "default",
   }, attrs);
 }
 
 // clearAllExperimentStorage
 add_task(withMockExperiments(async function(experiments) {
-  experiments["test"] = experimentFactory({name: "test"});
+  experiments.test = experimentFactory({name: "test"});
   ok(await PreferenceExperiments.has("test"), "Mock experiment is detected.");
   await PreferenceExperiments.clearAllExperimentStorage();
   ok(
     !(await PreferenceExperiments.has("test")),
     "clearAllExperimentStorage removed all stored experiments",
   );
 }));
 
 // start should throw if an experiment with the given name already exists
 add_task(withMockExperiments(async function(experiments) {
-  experiments["test"] = experimentFactory({name: "test"});
+  experiments.test = experimentFactory({name: "test"});
   await Assert.rejects(
     PreferenceExperiments.start({
       name: "test",
       branch: "branch",
       preferenceName: "fake.preference",
       preferenceValue: "value",
       preferenceType: "string",
       preferenceBranchType: "default",
     }),
     "start threw an error due to a conflicting experiment name",
   );
 }));
 
 // start should throw if an experiment for the given preference is active
 add_task(withMockExperiments(async function(experiments) {
-  experiments["test"] = experimentFactory({name: "test", preferenceName: "fake.preference"});
+  experiments.test = experimentFactory({name: "test", preferenceName: "fake.preference"});
   await Assert.rejects(
     PreferenceExperiments.start({
       name: "different",