Bug 1398239 - Add prerendering capability, Highlights diversity and bug fixes to Activity Stream draft bug1398239
authork88hudson <khudson@mozilla.com>
Mon, 11 Sep 2017 17:17:35 -0400
changeset 662636 7ddaa2ef3f125898729410c0ef1d1ee71db37467
parent 662594 15128312c02a238ac158589c59fd3ee3e384ce80
child 730926 906389e78279d37b3fa5be3bc45e26bf144cd2ab
push id79144
push userkhudson@mozilla.com
push dateMon, 11 Sep 2017 21:47:09 +0000
bugs1398239
milestone57.0a1
Bug 1398239 - Add prerendering capability, Highlights diversity and bug fixes to Activity Stream MozReview-Commit-ID: DqKGqHeT0nS
browser/base/content/test/static/browser_all_files_referenced.js
browser/components/search/test/browser_searchEngine_behaviors.js
browser/extensions/activity-stream/common/Actions.jsm
browser/extensions/activity-stream/common/PerfService.jsm
browser/extensions/activity-stream/common/PrerenderData.jsm
browser/extensions/activity-stream/common/Reducers.jsm
browser/extensions/activity-stream/data/content/activity-stream-initial-state.js
browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
browser/extensions/activity-stream/data/content/activity-stream.bundle.js
browser/extensions/activity-stream/data/content/activity-stream.css
browser/extensions/activity-stream/data/content/activity-stream.html
browser/extensions/activity-stream/data/locales.json
browser/extensions/activity-stream/install.rdf.in
browser/extensions/activity-stream/lib/ActivityStream.jsm
browser/extensions/activity-stream/lib/HighlightsFeed.jsm
browser/extensions/activity-stream/lib/NewTabInit.jsm
browser/extensions/activity-stream/lib/PrefsFeed.jsm
browser/extensions/activity-stream/lib/SectionsManager.jsm
browser/extensions/activity-stream/lib/SnippetsFeed.jsm
browser/extensions/activity-stream/lib/TopSitesFeed.jsm
browser/extensions/activity-stream/test/unit/activity-stream-prerender.test.jsx
browser/extensions/activity-stream/test/unit/common/Reducers.test.js
browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
browser/extensions/activity-stream/test/unit/lib/NewTabInit.test.js
browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js
browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
browser/extensions/activity-stream/test/unit/lib/init-store.test.js
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -82,16 +82,20 @@ var whitelist = [
 
   // Add-on API introduced in bug 1118285
   {file: "resource://app/modules/NewTabURL.jsm"},
 
   // browser/components/newtab bug 1355166
   {file: "resource://app/modules/NewTabSearchProvider.jsm"},
   {file: "resource://app/modules/NewTabWebChannel.jsm"},
 
+  // browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
+  // This will used when Bug 1397875 lands
+  {file: "resource://activity-stream/data/content/activity-stream-prerendered.html"},
+
   // layout/mathml/nsMathMLChar.cpp
   {file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties"},
   {file: "resource://gre/res/fonts/mathfontUnicode.properties"},
 
   // toolkit/components/places/ColorAnalyzer_worker.js
   {file: "resource://gre/modules/ClusterLib.js"},
   {file: "resource://gre/modules/ColorConversion.js"},
 
--- a/browser/components/search/test/browser_searchEngine_behaviors.js
+++ b/browser/components/search/test/browser_searchEngine_behaviors.js
@@ -90,24 +90,19 @@ function promiseStateChangeURI() {
     }
 
     gBrowser.addProgressListener(listener);
   });
 }
 
 function promiseContentSearchReady(browser) {
   return ContentTask.spawn(browser, {}, async function(args) {
-    return new Promise(resolve => {
-      content.addEventListener("ContentSearchService", function listener(aEvent) {
-        if (aEvent.detail.type == "State") {
-          content.removeEventListener("ContentSearchService", listener);
-          resolve();
-        }
-      });
-    });
+    await ContentTaskUtils.waitForCondition(() => content.wrappedJSObject.gContentSearchController &&
+      content.wrappedJSObject.gContentSearchController.defaultEngine
+    );
   });
 }
 
 add_task(async function() {
   await SpecialPowers.pushPrefEnv({ set: [
     ["browser.search.widget.inNavBar", true],
   ]});
 });
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -34,16 +34,17 @@ for (const type of [
   "INIT",
   "LOCALE_UPDATED",
   "MIGRATION_CANCEL",
   "MIGRATION_COMPLETED",
   "MIGRATION_START",
   "NEW_TAB_INIT",
   "NEW_TAB_INITIAL_STATE",
   "NEW_TAB_LOAD",
+  "NEW_TAB_STATE_REQUEST",
   "NEW_TAB_UNLOAD",
   "OPEN_LINK",
   "OPEN_NEW_WINDOW",
   "OPEN_PRIVATE_WINDOW",
   "PINNED_SITES_UPDATED",
   "PLACES_BOOKMARK_ADDED",
   "PLACES_BOOKMARK_CHANGED",
   "PLACES_BOOKMARK_REMOVED",
--- a/browser/extensions/activity-stream/common/PerfService.jsm
+++ b/browser/extensions/activity-stream/common/PerfService.jsm
@@ -1,31 +1,33 @@
 /* globals Services */
 "use strict";
 
+/* istanbul ignore if */
+if (typeof Components !== "undefined" && Components.utils) {
+  Components.utils.import("resource://gre/modules/Services.jsm");
+}
+
 let usablePerfObj;
 
-let Cu;
-const isRunningInChrome = typeof Window === "undefined";
-
 /* istanbul ignore if */
-if (isRunningInChrome) {
-  Cu = Components.utils;
-} else {
-  Cu = {import() {}};
-}
-
-Cu.import("resource://gre/modules/Services.jsm");
-
-/* istanbul ignore if */
-if (isRunningInChrome) {
+/* istanbul ignore else */
+if (typeof Services !== "undefined") {
   // Borrow the high-resolution timer from the hidden window....
   usablePerfObj = Services.appShell.hiddenDOMWindow.performance;
-} else { // we must be running in content space
+} else if (typeof performance !== "undefined") {
+  // we must be running in content space
   usablePerfObj = performance;
+} else {
+  // This is a dummy object so this file doesn't crash in the node prerendering
+  // task.
+  usablePerfObj = {
+    now() {},
+    mark() {}
+  };
 }
 
 this._PerfService = function _PerfService(options) {
   // For testing, so that we can use a fake Window.performance object with
   // known state.
   if (options && options.performanceObj) {
     this._perf = options.performanceObj;
   } else {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/common/PrerenderData.jsm
@@ -0,0 +1,42 @@
+const prefConfig = {
+  // Prefs listed with "invalidates: true" will prevent the prerendered version
+  // of AS from being used if their value is something other than what is listed
+  // here. This is required because some preferences cause the page layout to be
+  // too different for the prerendered version to be used. Unfortunately, this
+  // will result in users who have modified some of their preferences not being
+  // able to get the benefits of prerendering.
+  "migrationExpired": {value: true},
+  "showTopSites": {
+    value: true,
+    invalidates: true
+  },
+  "showSearch": {
+    value: true,
+    invalidates: true
+  },
+  "topSitesCount": {value: 6},
+  "feeds.section.topstories": {
+    value: true,
+    invalidates: true
+  }
+};
+
+this.PrerenderData = {
+  invalidatingPrefs: Object.keys(prefConfig).filter(key => prefConfig[key].invalidates),
+  initialPrefs: Object.keys(prefConfig).reduce((obj, key) => {
+    obj[key] = prefConfig[key].value;
+    return obj;
+  }, {}), // This creates an object of the form {prefName: value}
+  initialSections: [
+    {
+      enabled: true,
+      icon: "pocket",
+      id: "topstories",
+      order: 1,
+      title: {id: "header_recommended_by", values: {provider: "Pocket"}},
+      topics: [{}]
+    }
+  ]
+};
+
+this.EXPORTED_SYMBOLS = ["PrerenderData"];
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -1,26 +1,31 @@
 /* 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/. */
 "use strict";
 
 const {actionTypes: at} = Components.utils.import("resource://activity-stream/common/Actions.jsm", {});
 
+// Locales that should be displayed RTL
+const RTL_LIST = ["ar", "he", "fa", "ur"];
+
 const TOP_SITES_DEFAULT_LENGTH = 6;
 const TOP_SITES_SHOWMORE_LENGTH = 12;
 
 const INITIAL_STATE = {
   App: {
     // Have we received real data from the app yet?
     initialized: false,
     // The locale of the browser
     locale: "",
     // Localized strings with defaults
     strings: null,
+    // The text direction for the locale
+    textDirection: "",
     // The version of the system-addon
     version: null
   },
   Snippets: {initialized: false},
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
@@ -43,17 +48,18 @@ function App(prevState = INITIAL_STATE.A
       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
+        strings,
+        textDirection: RTL_LIST.indexOf(locale.split("-")[0]) >= 0 ? "rtl" : "ltr"
       });
     }
     default:
       return prevState;
   }
 }
 
 /**
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream-initial-state.js
@@ -0,0 +1,142 @@
+// Note - this is a generated file.
+  window.gActivityStreamPrerenderedState = {
+  "TopSites": {
+    "initialized": false,
+    "rows": []
+  },
+  "App": {
+    "initialized": false,
+    "locale": "en-PRERENDER",
+    "strings": {
+      "newtab_page_title": " ",
+      "default_label_loading": " ",
+      "header_top_sites": " ",
+      "header_stories": " ",
+      "header_highlights": " ",
+      "header_visit_again": " ",
+      "header_bookmarks": " ",
+      "header_recommended_by": " ",
+      "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": " ",
+      "menu_action_pin": " ",
+      "menu_action_unpin": " ",
+      "confirm_history_delete_p1": " ",
+      "confirm_history_delete_notice_p2": " ",
+      "menu_action_save_to_pocket": " ",
+      "search_for_something_with": " ",
+      "search_button": " ",
+      "search_header": " ",
+      "search_web_placeholder": "Search the Web",
+      "search_settings": " ",
+      "section_info_option": " ",
+      "section_info_send_feedback": " ",
+      "section_info_privacy_notice": " ",
+      "welcome_title": " ",
+      "welcome_body": " ",
+      "welcome_label": " ",
+      "time_label_less_than_minute": " ",
+      "time_label_minute": " ",
+      "time_label_hour": " ",
+      "time_label_day": " ",
+      "settings_pane_button_label": " ",
+      "settings_pane_header": " ",
+      "settings_pane_body2": " ",
+      "settings_pane_search_header": " ",
+      "settings_pane_search_body": " ",
+      "settings_pane_topsites_header": " ",
+      "settings_pane_topsites_body": " ",
+      "settings_pane_topsites_options_showmore": " ",
+      "settings_pane_bookmarks_header": " ",
+      "settings_pane_bookmarks_body": " ",
+      "settings_pane_visit_again_header": " ",
+      "settings_pane_visit_again_body": " ",
+      "settings_pane_highlights_header": " ",
+      "settings_pane_highlights_body2": " ",
+      "settings_pane_highlights_options_bookmarks": " ",
+      "settings_pane_highlights_options_visited": " ",
+      "settings_pane_snippets_header": " ",
+      "settings_pane_snippets_body": " ",
+      "settings_pane_done_button": " ",
+      "edit_topsites_button_text": " ",
+      "edit_topsites_button_label": " ",
+      "edit_topsites_showmore_button": " ",
+      "edit_topsites_showless_button": " ",
+      "edit_topsites_done_button": " ",
+      "edit_topsites_pin_button": " ",
+      "edit_topsites_unpin_button": " ",
+      "edit_topsites_edit_button": " ",
+      "edit_topsites_dismiss_button": " ",
+      "edit_topsites_add_button": " ",
+      "topsites_form_add_header": " ",
+      "topsites_form_edit_header": " ",
+      "topsites_form_title_placeholder": " ",
+      "topsites_form_url_placeholder": " ",
+      "topsites_form_add_button": " ",
+      "topsites_form_save_button": " ",
+      "topsites_form_cancel_button": " ",
+      "topsites_form_url_validation": " ",
+      "pocket_read_more": " ",
+      "pocket_read_even_more": " ",
+      "pocket_feedback_header": " ",
+      "pocket_description": " ",
+      "highlights_empty_state": " ",
+      "topstories_empty_state": " ",
+      "manual_migration_explanation2": " ",
+      "manual_migration_cancel_button": " ",
+      "manual_migration_import_button": " "
+    },
+    "textDirection": "ltr",
+    "version": null
+  },
+  "Snippets": {
+    "initialized": false
+  },
+  "Prefs": {
+    "initialized": true,
+    "values": {
+      "migrationExpired": true,
+      "showTopSites": true,
+      "showSearch": true,
+      "topSitesCount": 6,
+      "feeds.section.topstories": true
+    }
+  },
+  "Dialog": {
+    "visible": false,
+    "data": {}
+  },
+  "Sections": [
+    {
+      "title": {
+        "id": "header_recommended_by",
+        "values": {
+          "provider": "Pocket"
+        }
+      },
+      "rows": [],
+      "order": 1,
+      "enabled": true,
+      "icon": "pocket",
+      "id": "topstories",
+      "topics": [
+        {}
+      ],
+      "initialized": false
+    }
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang="" 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="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
+    <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 class="outer-wrapper" data-reactroot="" data-reactid="1" data-react-checksum="57168132"><main data-reactid="2"><div class="search-wrapper" data-reactid="3"><label for="newtab-search-text" class="search-label" data-reactid="4"><span class="sr-only" data-reactid="5"><span data-reactid="6">Search the Web</span></span></label><input type="search" id="newtab-search-text" maxlength="256" placeholder="Search the Web" title="Search the Web" data-reactid="7"/><button id="searchSubmit" class="search-button" title=" " data-reactid="8"><span class="sr-only" data-reactid="9"><span data-reactid="10"> </span></span></button></div><section class="top-sites" data-reactid="11"><h3 class="section-title" data-reactid="12"><span class="icon icon-small-spacer icon-topsites" data-reactid="13"></span><span data-reactid="14"> </span></h3><ul class="top-sites-list" data-reactid="15"><li class="top-site-outer placeholder" data-reactid="16"><a data-reactid="17"><div class="tile" aria-hidden="true" data-reactid="18"><span class="letter-fallback" data-reactid="19"></span><div class="screenshot" style="background-image:none;" data-reactid="20"></div></div><div class="title " data-reactid="21"><span dir="auto" data-reactid="22"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="23"><a data-reactid="24"><div class="tile" aria-hidden="true" data-reactid="25"><span class="letter-fallback" data-reactid="26"></span><div class="screenshot" style="background-image:none;" data-reactid="27"></div></div><div class="title " data-reactid="28"><span dir="auto" data-reactid="29"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="30"><a data-reactid="31"><div class="tile" aria-hidden="true" data-reactid="32"><span class="letter-fallback" data-reactid="33"></span><div class="screenshot" style="background-image:none;" data-reactid="34"></div></div><div class="title " data-reactid="35"><span dir="auto" data-reactid="36"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="37"><a data-reactid="38"><div class="tile" aria-hidden="true" data-reactid="39"><span class="letter-fallback" data-reactid="40"></span><div class="screenshot" style="background-image:none;" data-reactid="41"></div></div><div class="title " data-reactid="42"><span dir="auto" data-reactid="43"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="44"><a data-reactid="45"><div class="tile" aria-hidden="true" data-reactid="46"><span class="letter-fallback" data-reactid="47"></span><div class="screenshot" style="background-image:none;" data-reactid="48"></div></div><div class="title " data-reactid="49"><span dir="auto" data-reactid="50"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="51"><a data-reactid="52"><div class="tile" aria-hidden="true" data-reactid="53"><span class="letter-fallback" data-reactid="54"></span><div class="screenshot" style="background-image:none;" data-reactid="55"></div></div><div class="title " data-reactid="56"><span dir="auto" data-reactid="57"></span></div></a></li></ul></section><div class="sections-list" data-reactid="58"><section data-reactid="59"><div class="section-top-bar" data-reactid="60"><h3 class="section-title" data-reactid="61"><span class="icon icon-small-spacer icon-pocket" data-reactid="62"></span><span data-reactid="63"> </span></h3></div><ul class="section-list" style="padding:0;" data-reactid="64"><li class="card-outer placeholder" data-reactid="65"><a data-reactid="66"><div class="card" data-reactid="67"><div class="card-details no-image" data-reactid="68"><div class="card-text no-image no-host-name no-context" data-reactid="69"><h4 class="card-title" dir="auto" data-reactid="70"></h4><p class="card-description" dir="auto" data-reactid="71"></p></div></div></div></a></li><li class="card-outer placeholder" data-reactid="72"><a data-reactid="73"><div class="card" data-reactid="74"><div class="card-details no-image" data-reactid="75"><div class="card-text no-image no-host-name no-context" data-reactid="76"><h4 class="card-title" dir="auto" data-reactid="77"></h4><p class="card-description" dir="auto" data-reactid="78"></p></div></div></div></a></li><li class="card-outer placeholder" data-reactid="79"><a data-reactid="80"><div class="card" data-reactid="81"><div class="card-details no-image" data-reactid="82"><div class="card-text no-image no-host-name no-context" data-reactid="83"><h4 class="card-title" dir="auto" data-reactid="84"></h4><p class="card-description" dir="auto" data-reactid="85"></p></div></div></div></a></li></ul><div class="topic" data-reactid="86"><span data-reactid="87"><span data-reactid="88"> </span></span><ul data-reactid="89"><li data-reactid="90"><a class="topic-link" data-reactid="91"></a></li></ul></div></section></div><!-- react-empty: 92 --></main></div></div>
+    <div id="snippets-container">
+      <div id="snippets"></div>
+    </div>
+<script src="resource://activity-stream/data/content/activity-stream-initial-state.js"></script>
+    <script src="chrome://browser/content/contentSearchUI.js"></script>
+    <script src="resource://activity-stream/vendor/react.js"></script>
+    <script src="resource://activity-stream/vendor/react-dom.js"></script>
+    <script src="resource://activity-stream/vendor/react-intl.js"></script>
+    <script src="resource://activity-stream/vendor/redux.js"></script>
+    <script src="resource://activity-stream/vendor/react-redux.js"></script>
+    <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
+  </body>
+</html>
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -60,22 +60,16 @@
 /******/ 	__webpack_require__.p = "";
 /******/
 /******/ 	// Load entry module and return exports
 /******/ 	return __webpack_require__(__webpack_require__.s = 10);
 /******/ })
 /************************************************************************/
 /******/ ([
 /* 0 */
-/***/ (function(module, exports) {
-
-module.exports = React;
-
-/***/ }),
-/* 1 */
 /***/ (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/. */
 
 
@@ -95,17 +89,17 @@ const globalImportContext = typeof Windo
 
 
 // Create an object that avoids accidental differing key/value pairs:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 const actionTypes = {};
-for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
+for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
   actionTypes[type] = type;
 }
 
 // Helper function for creating routed actions between content and main
 // Not intended to be used by consumers
 function _RouteMessage(action, options) {
   const meta = action.meta ? Object.assign({}, action.meta) : {};
   if (!options || !options.from || !options.to) {
@@ -186,63 +180,55 @@ function UserEvent(data) {
 
 /**
  * UndesiredEvent - A telemetry ping indicating an undesired state.
  *
  * @param  {object} data Fields to include in the ping (value, etc.)
  * @param  {int} importContext (For testing) Override the import context for testing.
  * @return {object} An action. For UI code, a SendToMain action.
  */
-function UndesiredEvent(data) {
-  let importContext = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : globalImportContext;
-
+function UndesiredEvent(data, importContext = globalImportContext) {
   const action = {
     type: actionTypes.TELEMETRY_UNDESIRED_EVENT,
     data
   };
   return importContext === UI_CODE ? SendToMain(action) : action;
 }
 
 /**
  * PerfEvent - A telemetry ping indicating a performance-related event.
  *
  * @param  {object} data Fields to include in the ping (value, etc.)
  * @param  {int} importContext (For testing) Override the import context for testing.
  * @return {object} An action. For UI code, a SendToMain action.
  */
-function PerfEvent(data) {
-  let importContext = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : globalImportContext;
-
+function PerfEvent(data, importContext = globalImportContext) {
   const action = {
     type: actionTypes.TELEMETRY_PERFORMANCE_EVENT,
     data
   };
   return importContext === UI_CODE ? SendToMain(action) : action;
 }
 
 /**
  * ImpressionStats - A telemetry ping indicating an impression stats.
  *
  * @param  {object} data Fields to include in the ping
  * @param  {int} importContext (For testing) Override the import context for testing.
  * #return {object} An action. For UI code, a SendToMain action.
  */
-function ImpressionStats(data) {
-  let importContext = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : globalImportContext;
-
+function ImpressionStats(data, importContext = globalImportContext) {
   const action = {
     type: actionTypes.TELEMETRY_IMPRESSION_STATS,
     data
   };
   return importContext === UI_CODE ? SendToMain(action) : action;
 }
 
-function SetPref(name, value) {
-  let importContext = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : globalImportContext;
-
+function SetPref(name, value, importContext = globalImportContext) {
   const action = { type: actionTypes.SET_PREF, data: { name, value } };
   return importContext === UI_CODE ? SendToMain(action) : action;
 }
 
 var actionCreators = {
   BroadcastToContent,
   UserEvent,
   UndesiredEvent,
@@ -292,65 +278,101 @@ module.exports = {
   globalImportContext,
   UI_CODE,
   BACKGROUND_PROCESS,
   MAIN_MESSAGE_TYPE,
   CONTENT_MESSAGE_TYPE
 };
 
 /***/ }),
+/* 1 */
+/***/ (function(module, exports) {
+
+module.exports = React;
+
+/***/ }),
 /* 2 */
 /***/ (function(module, exports) {
 
 module.exports = ReactIntl;
 
 /***/ }),
 /* 3 */
 /***/ (function(module, exports) {
 
 module.exports = ReactRedux;
 
 /***/ }),
 /* 4 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
 
-"use strict";
+var g;
+
+// This works in non-strict mode
+g = (function() {
+	return this;
+})();
+
+try {
+	// This works if eval is allowed (see CSP)
+	g = g || Function("return this")() || (1,eval)("this");
+} catch(e) {
+	// This works if the window reference is available
+	if(typeof window === "object")
+		g = window;
+}
+
+// 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;
 
 
+/***/ }),
+/* 5 */
+/***/ (function(module, exports) {
+
 module.exports = {
   TOP_SITES_SOURCE: "TOP_SITES",
-  TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"]
+  TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
+  // minimum size necessary to show a rich icon instead of a screenshot
+  MIN_RICH_FAVICON_SIZE: 96,
+  // minimum size necessary to show any icon in the top left corner with a screenshot
+  MIN_CORNER_FAVICON_SIZE: 32
 };
 
 /***/ }),
-/* 5 */
+/* 6 */
 /***/ (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/. */
 
 
-var _require = __webpack_require__(1);
+const { actionTypes: at } = __webpack_require__(0);
 
-const at = _require.actionTypes;
-
+// Locales that should be displayed RTL
+const RTL_LIST = ["ar", "he", "fa", "ur"];
 
 const TOP_SITES_DEFAULT_LENGTH = 6;
 const TOP_SITES_SHOWMORE_LENGTH = 12;
 
 const INITIAL_STATE = {
   App: {
     // Have we received real data from the app yet?
     initialized: false,
     // The locale of the browser
     locale: "",
     // Localized strings with defaults
     strings: null,
+    // The text direction for the locale
+    textDirection: "",
     // The version of the system-addon
     version: null
   },
   Snippets: { initialized: false },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
@@ -362,35 +384,30 @@ const INITIAL_STATE = {
   },
   Dialog: {
     visible: false,
     data: {}
   },
   Sections: []
 };
 
-function App() {
-  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.App;
-  let action = arguments[1];
-
+function App(prevState = INITIAL_STATE.App, action) {
   switch (action.type) {
     case at.INIT:
       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;
-
+        let { locale, strings } = action.data;
         return Object.assign({}, prevState, {
           locale,
-          strings
+          strings,
+          textDirection: RTL_LIST.indexOf(locale.split("-")[0]) >= 0 ? "rtl" : "ltr"
         });
       }
     default:
       return prevState;
   }
 }
 
 /**
@@ -423,20 +440,17 @@ function insertPinned(links, pinned) {
     } else {
       newLinks.splice(index, 0, link);
     }
   });
 
   return newLinks;
 }
 
-function TopSites() {
-  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.TopSites;
-  let action = arguments[1];
-
+function TopSites(prevState = INITIAL_STATE.TopSites, action) {
   let hasMatch;
   let newRows;
   let pinned;
   switch (action.type) {
     case at.TOP_SITES_UPDATED:
       if (!action.data) {
         return prevState;
       }
@@ -451,21 +465,17 @@ function TopSites() {
       });
       return hasMatch ? Object.assign({}, prevState, { rows: newRows }) : prevState;
     case at.PLACES_BOOKMARK_ADDED:
       if (!action.data) {
         return prevState;
       }
       newRows = prevState.rows.map(site => {
         if (site && site.url === action.data.url) {
-          var _action$data2 = action.data;
-          const bookmarkGuid = _action$data2.bookmarkGuid,
-                bookmarkTitle = _action$data2.bookmarkTitle,
-                dateAdded = _action$data2.dateAdded;
-
+          const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
           return Object.assign({}, site, { bookmarkGuid, bookmarkTitle, bookmarkDateCreated: dateAdded });
         }
         return site;
       });
       return Object.assign({}, prevState, { rows: newRows });
     case at.PLACES_BOOKMARK_REMOVED:
       if (!action.data) {
         return prevState;
@@ -492,53 +502,44 @@ function TopSites() {
       pinned = action.data;
       newRows = insertPinned(prevState.rows, pinned).slice(0, TOP_SITES_SHOWMORE_LENGTH);
       return Object.assign({}, prevState, { rows: newRows });
     default:
       return prevState;
   }
 }
 
-function Dialog() {
-  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Dialog;
-  let action = arguments[1];
-
+function Dialog(prevState = INITIAL_STATE.Dialog, action) {
   switch (action.type) {
     case at.DIALOG_OPEN:
       return Object.assign({}, prevState, { visible: true, data: action.data });
     case at.DIALOG_CANCEL:
       return Object.assign({}, prevState, { visible: false });
     case at.DELETE_HISTORY_URL:
       return Object.assign({}, INITIAL_STATE.Dialog);
     default:
       return prevState;
   }
 }
 
-function Prefs() {
-  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Prefs;
-  let action = arguments[1];
-
+function Prefs(prevState = INITIAL_STATE.Prefs, action) {
   let newValues;
   switch (action.type) {
     case at.PREFS_INITIAL_VALUES:
       return Object.assign({}, prevState, { initialized: true, values: action.data });
     case at.PREF_CHANGED:
       newValues = Object.assign({}, prevState.values);
       newValues[action.data.name] = action.data.value;
       return Object.assign({}, prevState, { values: newValues });
     default:
       return prevState;
   }
 }
 
-function Sections() {
-  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Sections;
-  let action = arguments[1];
-
+function Sections(prevState = INITIAL_STATE.Sections, action) {
   let hasMatch;
   let newState;
   switch (action.type) {
     case at.SECTION_DEREGISTER:
       return prevState.filter(section => section.id !== action.data);
     case at.SECTION_REGISTER:
       // If section exists in prevState, update it
       newState = prevState.map(section => {
@@ -585,21 +586,17 @@ function Sections() {
     case at.PLACES_BOOKMARK_ADDED:
       if (!action.data) {
         return prevState;
       }
       return prevState.map(section => Object.assign({}, section, {
         rows: section.rows.map(item => {
           // find the item within the rows that is attempted to be bookmarked
           if (item.url === action.data.url) {
-            var _action$data3 = action.data;
-            const bookmarkGuid = _action$data3.bookmarkGuid,
-                  bookmarkTitle = _action$data3.bookmarkTitle,
-                  dateAdded = _action$data3.dateAdded;
-
+            const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
             Object.assign(item, { bookmarkGuid, bookmarkTitle, bookmarkDateCreated: dateAdded });
             if (!item.type || item.type === "history") {
               item.type = "bookmark";
             }
           }
           return item;
         })
       }));
@@ -626,20 +623,17 @@ function Sections() {
     case at.PLACES_LINK_DELETED:
     case at.PLACES_LINK_BLOCKED:
       return prevState.map(section => Object.assign({}, section, { rows: section.rows.filter(site => site.url !== action.data.url) }));
     default:
       return prevState;
   }
 }
 
-function Snippets() {
-  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Snippets;
-  let action = arguments[1];
-
+function Snippets(prevState = INITIAL_STATE.Snippets, action) {
   switch (action.type) {
     case at.SNIPPETS_DATA:
       return Object.assign({}, prevState, { initialized: true }, action.data);
     case at.SNIPPETS_RESET:
       return INITIAL_STATE.Snippets;
     default:
       return prevState;
   }
@@ -650,44 +644,46 @@ module.exports = {
   reducers,
   INITIAL_STATE,
   insertPinned,
   TOP_SITES_DEFAULT_LENGTH,
   TOP_SITES_SHOWMORE_LENGTH
 };
 
 /***/ }),
-/* 6 */
+/* 7 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* globals Services */
 
 
-let usablePerfObj;
+/* istanbul ignore if */
 
-let Cu;
-const isRunningInChrome = typeof Window === "undefined";
+if (typeof Components !== "undefined" && Components.utils) {
+  Components.utils.import("resource://gre/modules/Services.jsm");
+}
+
+let usablePerfObj;
 
 /* istanbul ignore if */
-if (isRunningInChrome) {
-  Cu = Components.utils;
-} else {
-  Cu = { import() {} };
-}
-
-Cu.import("resource://gre/modules/Services.jsm");
-
-/* istanbul ignore if */
-if (isRunningInChrome) {
+/* istanbul ignore else */
+if (typeof Services !== "undefined") {
   // Borrow the high-resolution timer from the hidden window....
   usablePerfObj = Services.appShell.hiddenDOMWindow.performance;
-} else {
+} else if (typeof performance !== "undefined") {
   // we must be running in content space
   usablePerfObj = performance;
+} else {
+  // This is a dummy object so this file doesn't crash in the node prerendering
+  // task.
+  usablePerfObj = {
+    now() {},
+    mark() {}
+  };
 }
 
 var _PerfService = function _PerfService(options) {
   // For testing, so that we can use a fake Window.performance object with
   // known state.
   if (options && options.performanceObj) {
     this._perf = options.performanceObj;
   } else {
@@ -773,73 +769,70 @@ var _PerfService = function _PerfService
 
 var perfService = new _PerfService();
 module.exports = {
   _PerfService,
   perfService
 };
 
 /***/ }),
-/* 7 */
+/* 8 */
 /***/ (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__(1);
-
-const ac = _require.actionCreators,
-      at = _require.actionTypes;
-
+const React = __webpack_require__(1);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
 
-const LinkMenu = __webpack_require__(8);
-
-var _require2 = __webpack_require__(4);
+const LinkMenu = __webpack_require__(9);
 
-const TOP_SITES_SOURCE = _require2.TOP_SITES_SOURCE,
-      TOP_SITES_CONTEXT_MENU_OPTIONS = _require2.TOP_SITES_CONTEXT_MENU_OPTIONS;
-
+const { TOP_SITES_SOURCE, TOP_SITES_CONTEXT_MENU_OPTIONS, MIN_RICH_FAVICON_SIZE, MIN_CORNER_FAVICON_SIZE } = __webpack_require__(5);
 
 const TopSiteLink = props => {
-  const link = props.link;
-
+  const { link } = props;
   const topSiteOuterClassName = `top-site-outer${props.className ? ` ${props.className}` : ""}`;
-  const tippyTopIcon = link.tippyTopIcon;
-
+  const { tippyTopIcon, faviconSize } = link;
   let imageClassName;
   let imageStyle;
-  if (tippyTopIcon) {
-    imageClassName = "tippy-top-icon";
+  let showSmallFavicon = false;
+  let smallFaviconStyle;
+  if (tippyTopIcon || faviconSize >= MIN_RICH_FAVICON_SIZE) {
+    // styles and class names for top sites with rich icons
+    imageClassName = "top-site-icon rich-icon";
     imageStyle = {
       backgroundColor: link.backgroundColor,
-      backgroundImage: `url(${tippyTopIcon})`
+      backgroundImage: `url(${tippyTopIcon || link.favicon})`
     };
   } else {
+    // styles and class names for top sites with screenshot + small icon in top left corner
     imageClassName = `screenshot${link.screenshot ? " active" : ""}`;
     imageStyle = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
+
+    // only show a favicon in top left if it's greater than 32x32
+    if (faviconSize >= MIN_CORNER_FAVICON_SIZE) {
+      showSmallFavicon = true;
+      smallFaviconStyle = { backgroundImage: `url(${link.favicon})` };
+    }
   }
   return React.createElement(
     "li",
     { className: topSiteOuterClassName, key: link.guid || link.url },
     React.createElement(
       "a",
       { href: link.url, onClick: props.onClick },
       React.createElement(
         "div",
         { className: "tile", "aria-hidden": true },
         React.createElement(
           "span",
           { className: "letter-fallback" },
           props.title[0]
         ),
-        React.createElement("div", { className: imageClassName, style: imageStyle })
+        React.createElement("div", { className: imageClassName, style: imageStyle }),
+        showSmallFavicon && React.createElement("div", { className: "top-site-icon default-icon", style: smallFaviconStyle })
       ),
       React.createElement(
         "div",
         { className: `title ${link.isPinned ? "pinned" : ""}` },
         link.isPinned && React.createElement("div", { className: "icon icon-pin-small" }),
         React.createElement(
           "span",
           { dir: "auto" },
@@ -890,35 +883,31 @@ class TopSite extends React.Component {
   onMenuButtonClick(event) {
     event.preventDefault();
     this.toggleContextMenu(event, this.props.index);
   }
   onMenuUpdate(showContextMenu) {
     this.setState({ showContextMenu });
   }
   onDismissButtonClick() {
-    const link = this.props.link;
-
+    const { link } = this.props;
     if (link.isPinned) {
       this.props.dispatch(ac.SendToMain({
         type: at.TOP_SITES_UNPIN,
         data: { site: { url: link.url } }
       }));
     }
     this.props.dispatch(ac.SendToMain({
       type: at.BLOCK_URL,
       data: link.url
     }));
     this.userEvent("BLOCK");
   }
   onPinButtonClick() {
-    var _props = this.props;
-    const link = _props.link,
-          index = _props.index;
-
+    const { link, index } = this.props;
     if (link.isPinned) {
       this.props.dispatch(ac.SendToMain({
         type: at.TOP_SITES_UNPIN,
         data: { site: { url: link.url } }
       }));
       this.userEvent("UNPIN");
     } else {
       this.props.dispatch(ac.SendToMain({
@@ -927,19 +916,18 @@ class TopSite extends React.Component {
       }));
       this.userEvent("PIN");
     }
   }
   onEditButtonClick() {
     this.props.onEdit(this.props.index);
   }
   render() {
-    const props = this.props;
-    const link = props.link;
-
+    const { props } = this;
+    const { link } = props;
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === props.index;
     const title = link.label || link.hostname;
     return React.createElement(
       TopSiteLink,
       _extends({}, props, { onClick: this.onLinkClick, className: isContextMenuOpen ? "active" : "", title: title }),
       !props.editMode && React.createElement(
         "div",
         null,
@@ -988,55 +976,36 @@ TopSite.defaultProps = {
 
 const TopSitePlaceholder = () => React.createElement(TopSiteLink, { className: "placeholder" });
 
 module.exports.TopSite = TopSite;
 module.exports.TopSiteLink = TopSiteLink;
 module.exports.TopSitePlaceholder = TopSitePlaceholder;
 
 /***/ }),
-/* 8 */
+/* 9 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(2);
-
-const injectIntl = _require.injectIntl;
-
+const React = __webpack_require__(1);
+const { injectIntl } = __webpack_require__(2);
 const ContextMenu = __webpack_require__(17);
-
-var _require2 = __webpack_require__(1);
-
-const ac = _require2.actionCreators;
-
+const { actionCreators: ac } = __webpack_require__(0);
 const linkMenuOptions = __webpack_require__(18);
 const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"];
 
 class LinkMenu extends React.Component {
   getOptions() {
     const props = this.props;
-    const site = props.site,
-          index = props.index,
-          source = props.source;
+    const { site, index, source } = props;
 
     // Handle special case of default site
-
     const propOptions = !site.isDefault ? props.options : DEFAULT_SITE_MENU_OPTIONS;
 
     const options = propOptions.map(o => linkMenuOptions[o](site, index, source)).map(option => {
-      const action = option.action,
-            impression = option.impression,
-            id = option.id,
-            type = option.type,
-            userEvent = option.userEvent;
-
+      const { action, impression, id, type, userEvent } = option;
       if (!type && id) {
         option.label = props.intl.formatMessage(option);
         option.onClick = () => {
           props.dispatch(action);
           if (userEvent) {
             props.dispatch(ac.UserEvent({
               event: userEvent,
               source,
@@ -1065,214 +1034,154 @@ class LinkMenu extends React.Component {
       options: this.getOptions() });
   }
 }
 
 module.exports = injectIntl(LinkMenu);
 module.exports._unconnected = LinkMenu;
 
 /***/ }),
-/* 9 */
-/***/ (function(module, exports) {
-
-var g;
-
-// This works in non-strict mode
-g = (function() {
-	return this;
-})();
-
-try {
-	// This works if eval is allowed (see CSP)
-	g = g || Function("return this")() || (1,eval)("this");
-} catch(e) {
-	// This works if the window reference is available
-	if(typeof window === "object")
-		g = window;
-}
-
-// 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;
-
-
-/***/ }),
 /* 10 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-const React = __webpack_require__(0);
+/* WEBPACK VAR INJECTION */(function(global) {const React = __webpack_require__(1);
 const ReactDOM = __webpack_require__(11);
 const Base = __webpack_require__(12);
-
-var _require = __webpack_require__(3);
-
-const Provider = _require.Provider;
-
+const { Provider } = __webpack_require__(3);
 const initStore = __webpack_require__(28);
-
-var _require2 = __webpack_require__(5);
-
-const reducers = _require2.reducers;
-
+const { reducers } = __webpack_require__(6);
 const DetectUserSessionStart = __webpack_require__(30);
-
-var _require3 = __webpack_require__(31);
-
-const addSnippetsSubscriber = _require3.addSnippetsSubscriber;
-
+const { addSnippetsSubscriber } = __webpack_require__(31);
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
 
 new DetectUserSessionStart().sendEventOrAddListener();
 
-const store = initStore(reducers);
+const store = initStore(reducers, global.gActivityStreamPrerenderedState);
+
+// If we are starting in a prerendered state, we must wait until the first render
+// to request state rehydration (see Base.jsx). If we are NOT in a prerendered state,
+// we can request it immedately.
+if (!global.gActivityStreamPrerenderedState) {
+  store.dispatch(ac.SendToMain({ type: at.NEW_TAB_STATE_REQUEST }));
+}
 
 ReactDOM.render(React.createElement(
   Provider,
   { store: store },
-  React.createElement(Base, null)
+  React.createElement(Base, { isPrerendered: !!global.gActivityStreamPrerenderedState })
 ), document.getElementById("root"));
 
 addSnippetsSubscriber(store);
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
 
 /***/ }),
 /* 11 */
 /***/ (function(module, exports) {
 
 module.exports = ReactDOM;
 
 /***/ }),
 /* 12 */
 /***/ (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 React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { addLocaleData, IntlProvider } = __webpack_require__(2);
 const TopSites = __webpack_require__(13);
 const Search = __webpack_require__(19);
 const ConfirmDialog = __webpack_require__(21);
 const ManualMigration = __webpack_require__(22);
 const PreferencesPane = __webpack_require__(23);
 const Sections = __webpack_require__(24);
-
-// Locales that should be displayed RTL
-const RTL_LIST = ["ar", "he", "fa", "ur"];
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
 
 // 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) {
-  let locale = _ref.locale;
-
+function addLocaleDataForReactIntl({ locale, textDirection }) {
   addLocaleData([{ locale, parentLocale: "en" }]);
   document.documentElement.lang = locale;
-  document.documentElement.dir = RTL_LIST.indexOf(locale.split("-")[0]) >= 0 ? "rtl" : "ltr";
+  document.documentElement.dir = textDirection;
 }
 
 class Base extends React.Component {
   componentDidMount() {
+    // Request state AFTER the first render to ensure we don't cause the
+    // prerendered DOM to be unmounted. Otherwise, NEW_TAB_STATE_REQUEST is
+    // dispatched right after the store is ready.
+    if (this.props.isPrerendered) {
+      this.props.dispatch(ac.SendToMain({ type: at.NEW_TAB_STATE_REQUEST }));
+    }
+
     // Also wait for the preloaded page to show, so the tab's title and favicon updates
     addEventListener("visibilitychange", () => {
       this.updateTitle(this.props.App);
       document.getElementById("favicon").href += "#";
     }, { once: true });
   }
-  componentWillUpdate(_ref2) {
-    let App = _ref2.App;
-
+  componentWillUpdate({ App }) {
     // 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;
-
+  updateTitle({ strings }) {
     if (strings) {
       document.title = strings.newtab_page_title;
     }
   }
 
   render() {
     const props = this.props;
-    var _props$App = props.App;
-    const locale = _props$App.locale,
-          strings = _props$App.strings,
-          initialized = _props$App.initialized;
+    const { locale, strings, initialized } = props.App;
+    const prefs = props.Prefs.values;
 
-    const prefs = props.Prefs.values;
-    if (!initialized || !strings) {
+    if (!props.isPrerendered && !initialized) {
       return null;
     }
 
+    // Note: the key on IntlProvider must be static in order to not blow away
+    // all elements on a locale change (such as after preloading).
+    // See https://github.com/yahoo/react-intl/issues/695 for more info.
     return React.createElement(
       IntlProvider,
-      { key: locale, locale: locale, messages: strings },
+      { key: "STATIC", locale: locale, messages: strings },
       React.createElement(
         "div",
         { className: "outer-wrapper" },
         React.createElement(
           "main",
           null,
           prefs.showSearch && React.createElement(Search, null),
           !prefs.migrationExpired && React.createElement(ManualMigration, null),
           prefs.showTopSites && React.createElement(TopSites, null),
           React.createElement(Sections, null),
           React.createElement(ConfirmDialog, null)
         ),
-        React.createElement(PreferencesPane, null)
+        initialized && React.createElement(PreferencesPane, null)
       )
     );
   }
 }
 
 module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
 
 /***/ }),
 /* 13 */
 /***/ (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 React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { FormattedMessage } = __webpack_require__(2);
 
 const TopSitesPerfTimer = __webpack_require__(14);
 const TopSitesEdit = __webpack_require__(15);
-
-var _require3 = __webpack_require__(7);
-
-const TopSite = _require3.TopSite,
-      TopSitePlaceholder = _require3.TopSitePlaceholder;
-
+const { TopSite, TopSitePlaceholder } = __webpack_require__(8);
 
 const TopSites = props => {
   const realTopSites = props.TopSites.rows.slice(0, props.TopSitesCount);
   const placeholderCount = props.TopSitesCount - realTopSites.length;
   return React.createElement(
     TopSitesPerfTimer,
     null,
     React.createElement(
@@ -1302,33 +1211,20 @@ const TopSites = props => {
 
 module.exports = connect(state => ({ TopSites: state.TopSites, TopSitesCount: state.Prefs.values.topSitesCount }))(TopSites);
 module.exports._unconnected = TopSites;
 
 /***/ }),
 /* 14 */
 /***/ (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__(1);
-
-const ac = _require2.actionCreators,
-      at = _require2.actionTypes;
-
-var _require3 = __webpack_require__(6);
-
-const perfSvc = _require3.perfService;
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
+const { perfService: perfSvc } = __webpack_require__(7);
 
 /**
  * A proxy class that uses double requestAnimationFrame from
  * componentDidMount to dispatch a SAVE_SESSION_PERF_DATA to the main procsess
  * after the paint.
  *
  * This uses two callbacks because, after one callback, this part of the tree
  * may have rendered but not yet reflowed.  This strategy is modeled after
@@ -1338,17 +1234,16 @@ const perfSvc = _require3.perfService;
  * make it lag too long.
  *
  * XXX Should be made more generic by using this.props.children, or potentially
  * even split out into a higher-order component to wrap whatever.
  *
  * @class TopSitesPerfTimer
  * @extends {React.Component}
  */
-
 class TopSitesPerfTimer extends React.Component {
   constructor(props) {
     super(props);
     // Just for test dependency injection:
     this.perfSvc = this.props.perfSvc || perfSvc;
 
     this._sendPaintedEvent = this._sendPaintedEvent.bind(this);
     this._timestampHandled = false;
@@ -1435,48 +1330,25 @@ class TopSitesPerfTimer extends React.Co
 
 module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSitesPerfTimer);
 module.exports._unconnected = TopSitesPerfTimer;
 
 /***/ }),
 /* 15 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(2);
-
-const FormattedMessage = _require.FormattedMessage,
-      injectIntl = _require.injectIntl;
-
-var _require2 = __webpack_require__(1);
-
-const ac = _require2.actionCreators,
-      at = _require2.actionTypes;
-
+const React = __webpack_require__(1);
+const { FormattedMessage, injectIntl } = __webpack_require__(2);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
 
 const TopSiteForm = __webpack_require__(16);
-
-var _require3 = __webpack_require__(7);
-
-const TopSite = _require3.TopSite,
-      TopSitePlaceholder = _require3.TopSitePlaceholder;
-
-var _require4 = __webpack_require__(5);
+const { TopSite, TopSitePlaceholder } = __webpack_require__(8);
 
-const TOP_SITES_DEFAULT_LENGTH = _require4.TOP_SITES_DEFAULT_LENGTH,
-      TOP_SITES_SHOWMORE_LENGTH = _require4.TOP_SITES_SHOWMORE_LENGTH;
-
-var _require5 = __webpack_require__(4);
-
-const TOP_SITES_SOURCE = _require5.TOP_SITES_SOURCE;
-
+const { TOP_SITES_DEFAULT_LENGTH, TOP_SITES_SHOWMORE_LENGTH } = __webpack_require__(6);
+const { TOP_SITES_SOURCE } = __webpack_require__(5);
 
 class TopSitesEdit extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
       showEditModal: false,
       showAddForm: false,
       showEditForm: false,
@@ -1618,50 +1490,38 @@ class TopSitesEdit extends React.Compone
         React.createElement(
           "div",
           { className: "modal" },
           React.createElement(TopSiteForm, {
             label: this.props.TopSites.rows[this.state.editIndex].label || this.props.TopSites.rows[this.state.editIndex].hostname,
             url: this.props.TopSites.rows[this.state.editIndex].url,
             index: this.state.editIndex,
             editMode: true,
+            link: this.props.TopSites.rows[this.state.editIndex],
             onClose: this.onFormClose,
             dispatch: this.props.dispatch,
             intl: this.props.intl })
         )
       )
     );
   }
 }
 
 module.exports = injectIntl(TopSitesEdit);
 module.exports._unconnected = TopSitesEdit;
 
 /***/ }),
 /* 16 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(1);
+const React = __webpack_require__(1);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
+const { FormattedMessage } = __webpack_require__(2);
 
-const ac = _require.actionCreators,
-      at = _require.actionTypes;
-
-var _require2 = __webpack_require__(2);
-
-const FormattedMessage = _require2.FormattedMessage;
-
-var _require3 = __webpack_require__(4);
-
-const TOP_SITES_SOURCE = _require3.TOP_SITES_SOURCE;
-
+const { TOP_SITES_SOURCE } = __webpack_require__(5);
 
 class TopSiteForm extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
       label: props.label || "",
       url: props.url || "",
       validationError: false
@@ -1705,16 +1565,23 @@ class TopSiteForm extends React.Componen
   }
   onSaveButtonClick(ev) {
     ev.preventDefault();
     if (this.validateForm()) {
       let site = { url: this.cleanUrl() };
       if (this.state.label !== "") {
         site.label = this.state.label;
       }
+      // Unpin links if the URL changed.
+      if (this.props.link.isPinned && this.props.link.url !== site.url) {
+        this.props.dispatch(ac.SendToMain({
+          type: at.TOP_SITES_UNPIN,
+          data: { site: { url: this.props.link.url } }
+        }));
+      }
       this.props.dispatch(ac.SendToMain({
         type: at.TOP_SITES_PIN,
         data: { site, index: this.props.index }
       }));
       this.props.dispatch(ac.UserEvent({
         source: TOP_SITES_SOURCE,
         event: "TOP_SITES_EDIT",
         action_position: this.props.index
@@ -1827,20 +1694,17 @@ TopSiteForm.defaultProps = {
 };
 
 module.exports = TopSiteForm;
 
 /***/ }),
 /* 17 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-const React = __webpack_require__(0);
+const React = __webpack_require__(1);
 
 class ContextMenu extends React.Component {
   constructor(props) {
     super(props);
     this.hideContext = this.hideContext.bind(this);
   }
   hideContext() {
     this.props.onUpdate(false);
@@ -1880,36 +1744,34 @@ class ContextMenuItem extends React.Comp
     this.onClick = this.onClick.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
   }
   onClick() {
     this.props.hideContext();
     this.props.option.onClick();
   }
   onKeyDown(event) {
-    const option = this.props.option;
-
+    const { option } = this.props;
     switch (event.key) {
       case "Tab":
         // tab goes down in context menu, shift + tab goes up in context menu
         // if we're on the last item, one more tab will close the context menu
         // similarly, if we're on the first item, one more shift + tab will close it
         if (event.shiftKey && option.first || !event.shiftKey && option.last) {
           this.props.hideContext();
         }
         break;
       case "Enter":
         this.props.hideContext();
         option.onClick();
         break;
     }
   }
   render() {
-    const option = this.props.option;
-
+    const { option } = this.props;
     return React.createElement(
       "li",
       { role: "menuitem", className: "context-menu-item" },
       React.createElement(
         "a",
         { onClick: this.onClick, onKeyDown: this.onKeyDown, tabIndex: "0" },
         option.icon && React.createElement("span", { className: `icon icon-spacer icon-${option.icon}` }),
         option.label
@@ -1921,30 +1783,23 @@ class ContextMenuItem extends React.Comp
 module.exports = ContextMenu;
 module.exports.ContextMenu = ContextMenu;
 module.exports.ContextMenuItem = ContextMenuItem;
 
 /***/ }),
 /* 18 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-var _require = __webpack_require__(1);
-
-const at = _require.actionTypes,
-      ac = _require.actionCreators;
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
 
 /**
  * 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-added",
     action: ac.SendToMain({
       type: at.DELETE_BOOKMARK_BY_ID,
       data: site.bookmarkGuid
@@ -2047,35 +1902,21 @@ module.exports.CheckPinTopSite = (site, 
 /***/ }),
 /* 19 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* globals ContentSearchUIController */
 
 
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(3);
-
-const connect = _require.connect;
-
-var _require2 = __webpack_require__(2);
-
-const FormattedMessage = _require2.FormattedMessage,
-      injectIntl = _require2.injectIntl;
-
-var _require3 = __webpack_require__(1);
-
-const ac = _require3.actionCreators;
-
-var _require4 = __webpack_require__(20);
-
-const IS_NEWTAB = _require4.IS_NEWTAB;
-
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { FormattedMessage, injectIntl } = __webpack_require__(2);
+const { actionCreators: ac } = __webpack_require__(0);
+const { IS_NEWTAB } = __webpack_require__(20);
 
 class Search extends React.Component {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);
     this.onInputMount = this.onInputMount.bind(this);
   }
 
@@ -2155,52 +1996,38 @@ class Search extends React.Component {
           { className: "sr-only" },
           React.createElement(FormattedMessage, { id: "search_button" })
         )
       )
     );
   }
 }
 
-module.exports = connect()(injectIntl(Search));
+// initialized is passed to props so that Search will rerender when it receives strings
+module.exports = connect(state => ({ locale: state.App.locale }))(injectIntl(Search));
 module.exports._unconnected = Search;
 
 /***/ }),
 /* 20 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-module.exports = {
+/* WEBPACK VAR INJECTION */(function(global) {module.exports = {
   // constant to know if the page is about:newtab or about:home
-  IS_NEWTAB: document.documentURI === "about:newtab"
+  IS_NEWTAB: global.document && global.document.documentURI === "about:newtab"
 };
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
 
 /***/ }),
 /* 21 */
 /***/ (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;
-
-var _require3 = __webpack_require__(1);
-
-const actionTypes = _require3.actionTypes,
-      ac = _require3.actionCreators;
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { FormattedMessage } = __webpack_require__(2);
+const { actionTypes, actionCreators: ac } = __webpack_require__(0);
 
 /**
  * ConfirmDialog component.
  * One primary action button, one cancel button.
  *
  * Content displayed is controlled by `data` prop the component receives.
  * Example:
  * data: {
@@ -2211,17 +2038,16 @@ const actionTypes = _require3.actionType
  *   // Primary button USerEvent action.
  *   userEvent: "DELETE",
  *   // Array of locale ids to display.
  *   message_body: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"],
  *   // Text for primary button.
  *   confirm_button_string_id: "menu_action_delete"
  * },
  */
-
 const ConfirmDialog = React.createClass({
   displayName: "ConfirmDialog",
 
   getDefaultProps() {
     return {
       visible: false,
       data: {}
     };
@@ -2293,43 +2119,29 @@ const ConfirmDialog = React.createClass(
 module.exports = connect(state => state.Dialog)(ConfirmDialog);
 module.exports._unconnected = ConfirmDialog;
 module.exports.Dialog = ConfirmDialog;
 
 /***/ }),
 /* 22 */
 /***/ (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;
-
-var _require3 = __webpack_require__(1);
-
-const at = _require3.actionTypes,
-      ac = _require3.actionCreators;
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { FormattedMessage } = __webpack_require__(2);
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
 
 /**
  * Manual migration component used to start the profile import wizard.
  * Message is presented temporarily and will go away if:
  * 1.  User clicks "No Thanks"
  * 2.  User completed the data import
  * 3.  After 3 active days
  * 4.  User clicks "Cancel" on the import wizard (currently not implemented).
  */
-
 class ManualMigration extends React.Component {
   constructor(props) {
     super(props);
     this.onLaunchTour = this.onLaunchTour.bind(this);
     this.onCancelTour = this.onCancelTour.bind(this);
   }
   onLaunchTour() {
     this.props.dispatch(ac.SendToMain({ type: at.MIGRATION_START }));
@@ -2371,40 +2183,21 @@ class ManualMigration extends React.Comp
 
 module.exports = connect()(ManualMigration);
 module.exports._unconnected = ManualMigration;
 
 /***/ }),
 /* 23 */
 /***/ (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 injectIntl = _require2.injectIntl,
-      FormattedMessage = _require2.FormattedMessage;
-
-var _require3 = __webpack_require__(1);
-
-const ac = _require3.actionCreators,
-      at = _require3.actionTypes;
-
-var _require4 = __webpack_require__(5);
-
-const TOP_SITES_DEFAULT_LENGTH = _require4.TOP_SITES_DEFAULT_LENGTH,
-      TOP_SITES_SHOWMORE_LENGTH = _require4.TOP_SITES_SHOWMORE_LENGTH;
-
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { injectIntl, FormattedMessage } = __webpack_require__(2);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
+const { TOP_SITES_DEFAULT_LENGTH, TOP_SITES_SHOWMORE_LENGTH } = __webpack_require__(6);
 
 const getFormattedMessage = message => typeof message === "string" ? React.createElement(
   "span",
   null,
   message
 ) : React.createElement(FormattedMessage, message);
 
 const PreferencesInput = props => React.createElement(
@@ -2442,19 +2235,17 @@ class PreferencesPane extends React.Comp
   handleClickOutside(event) {
     // if we are showing the sidebar and there is a click outside, close it.
     if (this.state.visible && !this.wrapper.contains(event.target)) {
       this.togglePane();
     }
   }
   handlePrefChange(event) {
     const target = event.target;
-    const name = target.name,
-          checked = target.checked;
-
+    const { name, checked } = target;
     let value = checked;
     if (name === "topSitesCount") {
       value = checked ? TOP_SITES_SHOWMORE_LENGTH : TOP_SITES_DEFAULT_LENGTH;
     }
     this.props.dispatch(ac.SetPref(name, value));
   }
   handleSectionChange(event) {
     const target = event.target;
@@ -2511,25 +2302,24 @@ class PreferencesPane extends React.Comp
             React.createElement(PreferencesInput, { className: "showTopSites", prefName: "showTopSites", value: prefs.showTopSites, onChange: this.handlePrefChange,
               titleString: { id: "settings_pane_topsites_header" }, descString: { id: "settings_pane_topsites_body" } }),
             React.createElement(
               "div",
               { className: "options" },
               React.createElement(PreferencesInput, { className: "showMoreTopSites", prefName: "topSitesCount", value: prefs.topSitesCount !== TOP_SITES_DEFAULT_LENGTH, onChange: this.handlePrefChange,
                 titleString: { id: "settings_pane_topsites_options_showmore" }, labelClassName: "icon icon-topsites" })
             ),
-            sections.filter(section => !section.shouldHidePref).map((_ref) => {
-              let id = _ref.id,
-                  title = _ref.title,
-                  enabled = _ref.enabled,
-                  pref = _ref.pref;
-              return React.createElement(PreferencesInput, { key: id, className: "showSection", prefName: pref && pref.feed || id,
-                value: enabled, onChange: pref && pref.feed ? this.handlePrefChange : this.handleSectionChange,
-                titleString: pref && pref.titleString || title, descString: pref && pref.descString });
-            })
+            sections.filter(section => !section.shouldHidePref).map(({ id, title, enabled, pref }) => React.createElement(PreferencesInput, { key: id, className: "showSection", prefName: pref && pref.feed || id,
+              value: enabled, onChange: pref && pref.feed ? this.handlePrefChange : this.handleSectionChange,
+              titleString: pref && pref.titleString || title, descString: pref && pref.descString })),
+            React.createElement("hr", null),
+            React.createElement(PreferencesInput, { className: "showSnippets", prefName: "feeds.snippets",
+              value: prefs["feeds.snippets"], onChange: this.handlePrefChange,
+              titleString: { id: "settings_pane_snippets_header" },
+              descString: { id: "settings_pane_snippets_body" } })
           ),
           React.createElement(
             "section",
             { className: "actions" },
             React.createElement(
               "button",
               { className: "done", onClick: this.togglePane },
               React.createElement(FormattedMessage, { id: "settings_pane_done_button" })
@@ -2544,41 +2334,25 @@ class PreferencesPane extends React.Comp
 module.exports = connect(state => ({ Prefs: state.Prefs, Sections: state.Sections }))(injectIntl(PreferencesPane));
 module.exports.PreferencesPane = PreferencesPane;
 module.exports.PreferencesInput = PreferencesInput;
 
 /***/ }),
 /* 24 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-/* WEBPACK VAR INJECTION */(function(global) {
-
-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);
+/* WEBPACK VAR INJECTION */(function(global) {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 injectIntl = _require2.injectIntl,
-      FormattedMessage = _require2.FormattedMessage;
-
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { injectIntl, FormattedMessage } = __webpack_require__(2);
 const Card = __webpack_require__(25);
-const PlaceholderCard = Card.PlaceholderCard;
-
+const { PlaceholderCard } = Card;
 const Topics = __webpack_require__(27);
-
-var _require3 = __webpack_require__(1);
-
-const ac = _require3.actionCreators;
-
+const { actionCreators: ac } = __webpack_require__(0);
 
 const VISIBLE = "visible";
 const VISIBILITY_CHANGE_EVENT = "visibilitychange";
 const CARDS_PER_ROW = 3;
 
 class Section extends React.Component {
   constructor(props) {
     super(props);
@@ -2613,32 +2387,30 @@ class Section extends React.Component {
     return typeof message === "string" ? React.createElement(
       "span",
       null,
       message
     ) : React.createElement(FormattedMessage, message);
   }
 
   _dispatchImpressionStats() {
-    const props = this.props;
-
+    const { props } = this;
     const maxCards = 3 * props.maxRows;
     props.dispatch(ac.ImpressionStats({
       source: props.eventSource,
       tiles: props.rows.slice(0, maxCards).map(link => ({ id: link.guid })),
       incognito: props.options && props.options.personalized
     }));
   }
 
   // This sends an event when a user sees a set of new content. If content
   // changes while the page is hidden (i.e. preloaded or on a hidden tab),
   // only send the event if the page becomes visible again.
   sendImpressionStatsOrAddListener() {
-    const props = this.props;
-
+    const { props } = this;
 
     if (!props.dispatch) {
       return;
     }
 
     if (props.document.visibilityState === VISIBLE) {
       this._dispatchImpressionStats();
     } else {
@@ -2661,18 +2433,17 @@ class Section extends React.Component {
 
   componentDidMount() {
     if (this.props.rows.length) {
       this.sendImpressionStatsOrAddListener();
     }
   }
 
   componentDidUpdate(prevProps) {
-    const props = this.props;
-
+    const { props } = this;
     if (
     // Don't send impression stats for the empty state
     props.rows.length &&
     // We only want to send impression stats if the content of the cards has changed
     props.rows !== prevProps.rows) {
       this.sendImpressionStatsOrAddListener();
     }
   }
@@ -2684,32 +2455,23 @@ class Section extends React.Component {
     const remainder = items % CARDS_PER_ROW;
     if (remainder === 0) {
       return 0;
     }
     return CARDS_PER_ROW - remainder;
   }
 
   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,
-          maxRows = _props.maxRows,
-          contextMenuOptions = _props.contextMenuOptions,
-          intl = _props.intl,
-          initialized = _props.initialized;
-
+    const {
+      id, eventSource, title, icon, rows,
+      infoOption, emptyState, dispatch, maxRows,
+      contextMenuOptions, intl, initialized
+    } = this.props;
     const maxCards = CARDS_PER_ROW * maxRows;
-    const shouldShowTopics = id === "topstories" && this.props.topics && this.props.topics.length > 0 && this.props.read_more_endpoint;
+    const shouldShowTopics = id === "topstories" && this.props.topics && this.props.topics.length > 0;
 
     const infoOptionIconA11yAttrs = {
       "aria-haspopup": "true",
       "aria-controls": "info-option",
       "aria-expanded": this.state.infoActive ? "true" : "false",
       "role": "note",
       "tabIndex": 0
     };
@@ -2811,49 +2573,37 @@ class Sections extends React.Component {
     );
   }
 }
 
 module.exports = connect(state => ({ Sections: state.Sections }))(Sections);
 module.exports._unconnected = Sections;
 module.exports.SectionIntl = SectionIntl;
 module.exports._unconnectedSection = Section;
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(9)))
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
 
 /***/ }),
 /* 25 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-const React = __webpack_require__(0);
-const LinkMenu = __webpack_require__(8);
-
-var _require = __webpack_require__(2);
-
-const FormattedMessage = _require.FormattedMessage;
-
+const React = __webpack_require__(1);
+const LinkMenu = __webpack_require__(9);
+const { FormattedMessage } = __webpack_require__(2);
 const cardContextTypes = __webpack_require__(26);
-
-var _require2 = __webpack_require__(1);
-
-const ac = _require2.actionCreators,
-      at = _require2.actionTypes;
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
 
 /**
  * Card component.
  * Cards are found within a Section component and contain information about a link such
  * as preview image, page title, page description, and some context about if the page
  * was visited, bookmarked, trending etc...
  * Each Section can make an unordered list of Cards which will create one instane of
  * this class. Each card will then get a context menu which reflects the actions that
  * can be done on this Card.
  */
-
 class Card extends React.Component {
   constructor(props) {
     super(props);
     this.state = { showContextMenu: false, activeCard: null };
     this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
     this.onMenuUpdate = this.onMenuUpdate.bind(this);
     this.onLinkClick = this.onLinkClick.bind(this);
   }
@@ -2861,22 +2611,17 @@ class Card extends React.Component {
     event.preventDefault();
     this.setState({
       activeCard: this.props.index,
       showContextMenu: true
     });
   }
   onLinkClick(event) {
     event.preventDefault();
-    const altKey = event.altKey,
-          button = event.button,
-          ctrlKey = event.ctrlKey,
-          metaKey = event.metaKey,
-          shiftKey = event.shiftKey;
-
+    const { altKey, button, ctrlKey, metaKey, shiftKey } = event;
     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
@@ -2887,31 +2632,21 @@ class Card extends React.Component {
       incognito: true,
       tiles: [{ id: this.props.link.guid, pos: this.props.index }]
     }));
   }
   onMenuUpdate(showContextMenu) {
     this.setState({ showContextMenu });
   }
   render() {
-    var _props = this.props;
-    const index = _props.index,
-          link = _props.link,
-          dispatch = _props.dispatch,
-          contextMenuOptions = _props.contextMenuOptions,
-          eventSource = _props.eventSource;
-    const props = this.props;
-
+    const { index, link, dispatch, contextMenuOptions, eventSource } = this.props;
+    const { props } = this;
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
-
-    var _ref = link.type ? cardContextTypes[link.type] : {};
-
-    const icon = _ref.icon,
-          intlID = _ref.intlID;
-
+    // Display "now" as "trending" until we have new strings #3402
+    const { icon, intlID } = cardContextTypes[link.type === "now" ? "trending" : link.type] || {};
 
     return React.createElement(
       "li",
       { className: `card-outer${isContextMenuOpen ? " active" : ""}${props.placeholder ? " placeholder" : ""}` },
       React.createElement(
         "a",
         { href: link.url, onClick: !props.placeholder && this.onLinkClick },
         React.createElement(
@@ -2978,20 +2713,17 @@ Card.defaultProps = { link: {} };
 
 const PlaceholderCard = () => React.createElement(Card, { placeholder: true });
 
 module.exports = Card;
 module.exports.PlaceholderCard = PlaceholderCard;
 
 /***/ }),
 /* 26 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
+/***/ (function(module, exports) {
 
 module.exports = {
   history: {
     intlID: "type_label_visited",
     icon: "historyItem"
   },
   bookmark: {
     intlID: "type_label_bookmarked",
@@ -3006,95 +2738,71 @@ module.exports = {
     icon: "now"
   }
 };
 
 /***/ }),
 /* 27 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(2);
-
-const FormattedMessage = _require.FormattedMessage;
-
+const React = __webpack_require__(1);
+const { FormattedMessage } = __webpack_require__(2);
 
 class Topic extends React.Component {
   render() {
-    var _props = this.props;
-    const url = _props.url,
-          name = _props.name;
-
+    const { url, name } = this.props;
     return React.createElement(
       "li",
       null,
       React.createElement(
         "a",
         { key: name, className: "topic-link", href: url },
         name
       )
     );
   }
 }
 
 class Topics extends React.Component {
   render() {
-    var _props2 = this.props;
-    const topics = _props2.topics,
-          read_more_endpoint = _props2.read_more_endpoint;
-
+    const { topics, read_more_endpoint } = this.props;
     return React.createElement(
       "div",
       { className: "topic" },
       React.createElement(
         "span",
         null,
         React.createElement(FormattedMessage, { id: "pocket_read_more" })
       ),
       React.createElement(
         "ul",
         null,
         topics.map(t => React.createElement(Topic, { key: t.name, url: t.url, name: t.name }))
       ),
-      React.createElement(
+      read_more_endpoint && React.createElement(
         "a",
         { className: "topic-read-more", href: read_more_endpoint },
         React.createElement(FormattedMessage, { id: "pocket_read_even_more" })
       )
     );
   }
 }
 
 module.exports = Topics;
 module.exports._unconnected = Topics;
 module.exports.Topic = Topic;
 
 /***/ }),
 /* 28 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-/* eslint-env mozilla/frame-script */
-
-var _require = __webpack_require__(29);
+/* WEBPACK VAR INJECTION */(function(global) {/* eslint-env mozilla/frame-script */
 
-const createStore = _require.createStore,
-      combineReducers = _require.combineReducers,
-      applyMiddleware = _require.applyMiddleware;
-
-var _require2 = __webpack_require__(1);
-
-const au = _require2.actionUtils;
-
+const { createStore, combineReducers, applyMiddleware } = __webpack_require__(29);
+const { actionTypes: at, actionCreators: ac, actionUtils: au } = __webpack_require__(0);
 
 const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
 const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
 const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
 
 /**
  * A higher-order function which returns a reducer that, on MERGE_STORE action,
  * will return the action.data object merged into the previous state.
@@ -3126,73 +2834,103 @@ function mergeStateReducer(mainReducer) 
  */
 const messageMiddleware = store => next => action => {
   if (au.isSendToMain(action)) {
     sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
   }
   next(action);
 };
 
+const rehydrationMiddleware = store => next => action => {
+  if (store._didRehydrate) {
+    return next(action);
+  }
+
+  const isMergeStoreAction = action.type === MERGE_STORE_ACTION;
+  const isRehydrationRequest = action.type === at.NEW_TAB_STATE_REQUEST;
+
+  if (isRehydrationRequest) {
+    store._didRequestInitialState = true;
+    return next(action);
+  }
+
+  if (isMergeStoreAction) {
+    store._didRehydrate = true;
+    return next(action);
+  }
+
+  // If init happened after our request was made, we need to re-request
+  if (store._didRequestInitialState && action.type === at.INIT) {
+    return next(ac.SendToMain({ type: at.NEW_TAB_STATE_REQUEST }));
+  }
+
+  if (au.isBroadcastToContent(action) || au.isSendToContent(action)) {
+    // Note that actions received before didRehydrate will not be dispatched
+    // because this could negatively affect preloading and the the state
+    // will be replaced by rehydration anyway.
+    return null;
+  }
+
+  return next(action);
+};
+
 /**
  * initStore - Create a store and listen for incoming actions
  *
  * @param  {object} reducers An object containing Redux reducers
+ * @param  {object} intialState (optional) The initial state of the store, if desired
  * @return {object}          A redux store
  */
-module.exports = function initStore(reducers) {
-  const store = createStore(mergeStateReducer(combineReducers(reducers)), applyMiddleware(messageMiddleware));
+module.exports = function initStore(reducers, initialState) {
+  const store = createStore(mergeStateReducer(combineReducers(reducers)), initialState, global.addMessageListener && applyMiddleware(rehydrationMiddleware, messageMiddleware));
+
+  store._didRehydrate = false;
+  store._didRequestInitialState = false;
 
-  addMessageListener(INCOMING_MESSAGE_NAME, msg => {
-    try {
-      store.dispatch(msg.data);
-    } catch (ex) {
-      console.error("Content msg:", msg, "Dispatch error: ", ex); // eslint-disable-line no-console
-      dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`);
-    }
-  });
+  if (global.addMessageListener) {
+    global.addMessageListener(INCOMING_MESSAGE_NAME, msg => {
+      try {
+        store.dispatch(msg.data);
+      } catch (ex) {
+        console.error("Content msg:", msg, "Dispatch error: ", ex); // eslint-disable-line no-console
+        dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`);
+      }
+    });
+  }
 
   return store;
 };
 
+module.exports.rehydrationMiddleware = rehydrationMiddleware;
 module.exports.MERGE_STORE_ACTION = MERGE_STORE_ACTION;
 module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME;
 module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
 
 /***/ }),
 /* 29 */
 /***/ (function(module, exports) {
 
 module.exports = Redux;
 
 /***/ }),
 /* 30 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-
-
-var _require = __webpack_require__(1);
-
-const at = _require.actionTypes;
-
-var _require2 = __webpack_require__(6);
-
-const perfSvc = _require2.perfService;
-
+/* WEBPACK VAR INJECTION */(function(global) {const { actionTypes: at } = __webpack_require__(0);
+const { perfService: perfSvc } = __webpack_require__(7);
 
 const VISIBLE = "visible";
 const VISIBILITY_CHANGE_EVENT = "visibilitychange";
 
 module.exports = class DetectUserSessionStart {
-  constructor() {
-    let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
-
+  constructor(options = {}) {
     // Overrides for testing
-    this.sendAsyncMessage = options.sendAsyncMessage || window.sendAsyncMessage;
-    this.document = options.document || document;
+    this.sendAsyncMessage = options.sendAsyncMessage || global.sendAsyncMessage;
+    this.document = options.document || global.document;
     this._perfService = options.perfService || perfSvc;
     this._onVisibilityChange = this._onVisibilityChange.bind(this);
   }
 
   /**
    * sendEventOrAddListener - Notify immediately if the page is already visible,
    *                    or else set up a listener for when visibility changes.
    *                    This is needed for accurate session tracking for telemetry,
@@ -3236,43 +2974,40 @@ module.exports = class DetectUserSession
    */
   _onVisibilityChange() {
     if (this.document.visibilityState === VISIBLE) {
       this._sendEvent();
       this.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
     }
   }
 };
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
 
 /***/ }),
 /* 31 */
 /***/ (function(module, exports, __webpack_require__) {
 
-"use strict";
-/* WEBPACK VAR INJECTION */(function(global) {
-
-const DATABASE_NAME = "snippets_db";
+/* WEBPACK VAR INJECTION */(function(global) {const DATABASE_NAME = "snippets_db";
 const DATABASE_VERSION = 1;
 const SNIPPETS_OBJECTSTORE_NAME = "snippets";
 const SNIPPETS_UPDATE_INTERVAL_MS = 14400000; // 4 hours.
 
-var _require = __webpack_require__(1);
+const SNIPPETS_ENABLED_EVENT = "Snippets:Enabled";
+const SNIPPETS_DISABLED_EVENT = "Snippets:Disabled";
 
-const at = _require.actionTypes,
-      ac = _require.actionCreators;
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
 
 /**
  * SnippetsMap - A utility for cacheing values related to the snippet. It has
  *               the same interface as a Map, but is optionally backed by
  *               indexedDB for persistent storage.
  *               Call .connect() to open a database connection and restore any
  *               previously cached data, if necessary.
  *
  */
-
 class SnippetsMap extends Map {
   constructor(dispatch) {
     super();
     this._db = null;
     this._dispatch = dispatch;
   }
 
   set(key, value) {
@@ -3528,44 +3263,62 @@ class SnippetsProvider {
     await this._refreshSnippets();
 
     // Try showing remote snippets, falling back to defaults if necessary.
     try {
       this._showRemoteSnippets();
     } catch (e) {
       this._showDefaultSnippets(e);
     }
+
+    window.dispatchEvent(new Event(SNIPPETS_ENABLED_EVENT));
+    this.initialized = true;
+  }
+
+  uninit() {
+    window.dispatchEvent(new Event(SNIPPETS_DISABLED_EVENT));
+    this.initialized = false;
   }
 }
 
 /**
  * addSnippetsSubscriber - Creates a SnippetsProvider that Initializes
  *                         when the store has received the appropriate
  *                         Snippet data.
  *
  * @param  {obj} store   The redux store
- * @return {obj}        Returns the snippets instance and unsubscribe function
+ * @return {obj}         Returns the snippets instance and unsubscribe function
  */
 function addSnippetsSubscriber(store) {
   const snippets = new SnippetsProvider(store.dispatch);
-  const unsubscribe = store.subscribe(() => {
+
+  let initializing = false;
+
+  store.subscribe(async () => {
     const state = store.getState();
-    if (state.Snippets.initialized) {
-      if (state.Snippets.onboardingFinished) {
-        snippets.init({ appData: state.Snippets });
+    // state.Snippets.initialized:  Should snippets be initialised?
+    // snippets.initialized:        Is SnippetsProvider currently initialised?
+    if (state.Snippets.initialized && !snippets.initialized && state.Snippets.onboardingFinished) {
+      // Don't call init multiple times
+      if (!initializing) {
+        initializing = true;
+        await snippets.init({ appData: state.Snippets });
+        initializing = false;
       }
-      unsubscribe();
+    } else if (state.Snippets.initialized === false && snippets.initialized) {
+      snippets.uninit();
     }
   });
+
   // These values are returned for testing purposes
   return snippets;
 }
 
 module.exports = {
   addSnippetsSubscriber,
   SnippetsMap,
   SnippetsProvider,
   SNIPPETS_UPDATE_INTERVAL_MS
 };
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(9)))
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
 
 /***/ })
 /******/ ]);
\ No newline at end of file
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -59,17 +59,19 @@ input {
     background-image: url("assets/glyph-unpin-16.svg"); }
   .icon.icon-edit {
     background-image: url("assets/glyph-edit-16.svg"); }
   .icon.icon-pocket {
     background-image: url("assets/glyph-pocket-16.svg"); }
   .icon.icon-historyItem {
     background-image: url("assets/glyph-historyItem-16.svg"); }
   .icon.icon-trending {
-    background-image: url("assets/glyph-trending-16.svg"); }
+    background-image: url("assets/glyph-trending-16.svg");
+    transform: translateY(2px);
+    /* trending bolt is visually top heavy */ }
   .icon.icon-now {
     background-image: url("chrome://browser/skin/history.svg"); }
   .icon.icon-topsites {
     background-image: url("assets/glyph-topsites-16.svg"); }
   .icon.icon-pin-small {
     background-image: url("assets/glyph-pin-12.svg");
     background-size: 12px;
     height: 12px;
@@ -312,27 +314,36 @@ 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 {
+    .top-sites-list .top-site-outer .top-site-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: 96px;
-      background-repeat: no-repeat; }
+      background-repeat: no-repeat;
+      background-color: #F9F9FA; }
+    .top-sites-list .top-site-outer .rich-icon {
+      top: 0;
+      offset-inline-start: 0;
+      height: 100%;
+      width: 100%;
+      background-size: 96px; }
+    .top-sites-list .top-site-outer .default-icon {
+      z-index: 1;
+      top: -6px;
+      offset-inline-start: -6px;
+      height: 42px;
+      width: 42px;
+      background-size: 32px; }
     .top-sites-list .top-site-outer .title {
       font: message-box;
       height: 30px;
       line-height: 30px;
       text-align: center;
       width: 96px;
       position: relative; }
       .top-sites-list .top-site-outer .title .icon {
@@ -1012,21 +1023,20 @@ main {
     bottom: 0;
     left: 0;
     right: 0;
     color: #737373;
     font-size: 11px;
     display: flex; }
   .card-outer .card-context-icon {
     fill: rgba(12, 12, 13, 0.6);
-    font-size: 13px;
-    margin-inline-end: 6px;
-    display: block; }
+    margin-inline-end: 6px; }
   .card-outer .card-context-label {
     flex-grow: 1;
+    line-height: 16px;
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap; }
 
 .manual-migration-container {
   color: #4A4A4F;
   font-size: 13px;
   line-height: 15px;
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -1,10 +1,10 @@
 <!doctype html>
-<html lang="en-us" dir="ltr">
+<html lang="" 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="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -227,19 +227,21 @@
     "settings_pane_topsites_header": "Qabaqcıl Saytlar",
     "settings_pane_topsites_body": "Ən çox ziyarət etdiyiniz saytları görün.",
     "settings_pane_topsites_options_showmore": "İki sətir göstər",
     "settings_pane_bookmarks_header": "Son Əlfəcinlər",
     "settings_pane_bookmarks_body": "Yeni yaradılan əlfəcinlər tək bir əlverişli yerdə.",
     "settings_pane_visit_again_header": "Təkrar ziyarət et",
     "settings_pane_visit_again_body": "Firefox tarixçənizdən yadda saxlamaq və ya geri qayıtmaq istəyə biləcəyiniz hissələri göstərəcək.",
     "settings_pane_highlights_header": "Seçilmişlər",
+    "settings_pane_highlights_body2": "Son ziyarət etdiyiniz və ya əlfəcinlədiyiniz maraqlı məzmunlara rahat qayıdın.",
     "settings_pane_highlights_options_bookmarks": "Əlfəcinlər",
     "settings_pane_highlights_options_visited": "Baxılmış Saytlar",
     "settings_pane_snippets_header": "Hissələr",
+    "settings_pane_snippets_body": "Mozilladan Firefox, internet mədəniyyəti və digər yeniliklər haqqında qısa bildirişlər oxuyun.",
     "settings_pane_done_button": "Oldu",
     "edit_topsites_button_text": "Redaktə et",
     "edit_topsites_button_label": "Qabaqcıl Saytlar bölümünüzü fərdiləşdirin",
     "edit_topsites_showmore_button": "Daha çox göstər",
     "edit_topsites_showless_button": "Daha az göstər",
     "edit_topsites_done_button": "Oldu",
     "edit_topsites_pin_button": "Bu saytı sabitlə",
     "edit_topsites_unpin_button": "Bu saytı çıxart",
@@ -252,16 +254,18 @@
     "topsites_form_url_placeholder": "Ünvanı yazın və ya yapışdırın",
     "topsites_form_add_button": "Əlavə et",
     "topsites_form_save_button": "Saxla",
     "topsites_form_cancel_button": "Ləğv et",
     "topsites_form_url_validation": "Doğru ünvan tələb olunur",
     "pocket_read_more": "Məşhur Mövzular:",
     "pocket_read_even_more": "Daha çox hekayə gör",
     "pocket_feedback_header": "25 milyon nəfərin dəstəyi ilə internetin ən yaxşıları.",
+    "pocket_description": "Mozilla ailəsinin yeni üzvü olan Pocket ilə yüksək keyfiyyətli məzmunları kəşf edin.",
+    "highlights_empty_state": "İnternetdə gəzməyə başlayın, burada ziyarət edəcəyiniz və ya əlfəcinləyəcəyiniz məqalə, video və digər səhifələri göstərəcəyik.",
     "topstories_empty_state": "Hamısını oxudunuz. Yeni {provider} məqalələri üçün daha sonra təkrar yoxlayın. Gözləyə bilmirsiz? Məşhur mövzu seçərək internetdən daha çox gözəl məqalələr tapın.",
     "manual_migration_explanation2": "Firefox səyyahını digər səyyahlardan olan əlfəcin, tarixçə və parollar ilə yoxlayın.",
     "manual_migration_cancel_button": "Xeyr, Təşəkkürlər",
     "manual_migration_import_button": "İndi idxal et"
   },
   "be": {
     "newtab_page_title": "Новая картка",
     "default_label_loading": "Загрузка…",
@@ -409,19 +413,21 @@
     "settings_pane_topsites_header": "Най-посещавани",
     "settings_pane_topsites_body": "Достъп до сайтовете, които посещавате най-често.",
     "settings_pane_topsites_options_showmore": "Показване на два реда",
     "settings_pane_bookmarks_header": "Последни отметки",
     "settings_pane_bookmarks_body": "Всички нови отметки на едно място.",
     "settings_pane_visit_again_header": "Посещаване",
     "settings_pane_visit_again_body": "Firefox ще ви показва части от вашата история на разглеждане, към която бихте желали да се върнете или запомните.",
     "settings_pane_highlights_header": "Акценти",
+    "settings_pane_highlights_body2": "Намерете интересните неща, които скоро сте посетили или отметнали.",
     "settings_pane_highlights_options_bookmarks": "Отметки",
     "settings_pane_highlights_options_visited": "Посетени страници",
     "settings_pane_snippets_header": "Изрезки",
+    "settings_pane_snippets_body": "Четете кратки и радостни новини от Mozilla относно Firefox, интернет-културата и случайни мемета.",
     "settings_pane_done_button": "Готово",
     "edit_topsites_button_text": "Редактиране",
     "edit_topsites_button_label": "Настройки на най-посещаваните",
     "edit_topsites_showmore_button": "Повече",
     "edit_topsites_showless_button": "По-малко",
     "edit_topsites_done_button": "Готово",
     "edit_topsites_pin_button": "Закачане",
     "edit_topsites_unpin_button": "Премахване от закачените",
@@ -435,16 +441,17 @@
     "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_description": "Открийте висококачествено съдържание, което иначе може да пропуснете, с помощта на Pocket, вече част от Mozilla.",
+    "highlights_empty_state": "Разглеждайте и тук ще ви покажем някои от най-добрите статии, видео и други страници, които сте посетили или отметнали наскоро.",
     "topstories_empty_state": "Разгледахте всичко. Проверете по-късно за повече истории от {provider}. Нямате търпение? Изберете популярна тема, за да откриете повече истории из цялата Мрежа.",
     "manual_migration_explanation2": "Опитайте Firefox с отметките, историята и паролите от друг четец.",
     "manual_migration_cancel_button": "Не, благодаря",
     "manual_migration_import_button": "Внасяне"
   },
   "bn-BD": {
     "newtab_page_title": "নতুন ট্যাব",
     "default_label_loading": "লোড হচ্ছে…",
@@ -1050,20 +1057,20 @@
     "type_label_topic": "Thema",
     "type_label_now": "Jetzt",
     "menu_action_bookmark": "Lesezeichen",
     "menu_action_remove_bookmark": "Lesezeichen entfernen",
     "menu_action_copy_address": "Adresse kopieren",
     "menu_action_email_link": "Link per E-Mail versenden…",
     "menu_action_open_new_window": "In neuem Fenster öffnen",
     "menu_action_open_private_window": "In neuem privaten Fenster öffnen",
-    "menu_action_dismiss": "Schließen",
+    "menu_action_dismiss": "Entfernen",
     "menu_action_delete": "Aus Chronik löschen",
     "menu_action_pin": "Anheften",
-    "menu_action_unpin": "Lösen",
+    "menu_action_unpin": "Ablösen",
     "confirm_history_delete_p1": "Soll wirklich jede Instanz dieser Seite aus Ihrer Chronik gelöscht werden?",
     "confirm_history_delete_notice_p2": "Diese Aktion kann nicht rückgängig gemacht werden.",
     "menu_action_save_to_pocket": "Bei Pocket speichern",
     "search_for_something_with": "Nach {search_term} suchen mit:",
     "search_button": "Suchen",
     "search_header": "{search_engine_name}-Suche",
     "search_web_placeholder": "Das Web durchsuchen",
     "search_settings": "Sucheinstellungen ändern",
@@ -1072,18 +1079,18 @@
     "section_info_privacy_notice": "Datenschutzhinweis",
     "welcome_title": "Willkommen im neuen Tab",
     "welcome_body": "Firefox nutzt diesen Bereich, um Ihnen Ihre wichtigsten Lesezeichen, Artikel, Videos und kürzlich besuchten Seiten anzuzeigen, damit Sie diese einfach wiederfinden.",
     "welcome_label": "Auswahl Ihrer wichtigsten Seiten",
     "time_label_less_than_minute": "< 1 min",
     "time_label_minute": "{number} m",
     "time_label_hour": "{number} h",
     "time_label_day": "{number} t",
-    "settings_pane_button_label": "Neuer-Tab-Seite anpassen",
-    "settings_pane_header": "Einstellungen zum neuen Tab",
+    "settings_pane_button_label": "Einstellungen für neue Tabs anpassen",
+    "settings_pane_header": "Einstellungen für neue Tabs",
     "settings_pane_body2": "Wählen Sie aus, was auf dieser Seite angezeigt wird.",
     "settings_pane_search_header": "Suche",
     "settings_pane_search_body": "Suchen Sie aus einem neuen Tab im Internet.",
     "settings_pane_topsites_header": "Meistbesuchte Seiten",
     "settings_pane_topsites_body": "Schneller Zugriff auf Ihre meistbesuchten Websites.",
     "settings_pane_topsites_options_showmore": "Zwei Reihen anzeigen",
     "settings_pane_bookmarks_header": "Neue Lesezeichen",
     "settings_pane_bookmarks_body": "Ihre neu erstellten Lesezeichen praktisch an einem Ort.",
@@ -1103,28 +1110,28 @@
     "edit_topsites_done_button": "Fertig",
     "edit_topsites_pin_button": "Website immer in aktueller Position anzeigen",
     "edit_topsites_unpin_button": "Diese Website lösen",
     "edit_topsites_edit_button": "Diese Website bearbeiten",
     "edit_topsites_dismiss_button": "Website entfernen",
     "edit_topsites_add_button": "Hinzufügen",
     "topsites_form_add_header": "Neue meistbesuchte Seite",
     "topsites_form_edit_header": "Meistbesuchte Seite bearbeiten",
-    "topsites_form_title_placeholder": "Titel eingeben",
-    "topsites_form_url_placeholder": "Eine URL eingeben oder einfügen",
+    "topsites_form_title_placeholder": "Name eingeben",
+    "topsites_form_url_placeholder": "Eine Adresse eingeben oder einfügen",
     "topsites_form_add_button": "Hinzufügen",
     "topsites_form_save_button": "Speichern",
     "topsites_form_cancel_button": "Abbrechen",
     "topsites_form_url_validation": "Gültige URL erforderlich",
     "pocket_read_more": "Beliebte Themen:",
     "pocket_read_even_more": "Weitere Nachrichten ansehen",
     "pocket_feedback_header": "Das Beste aus dem Web, zusammengetragen von 25 Millionen Menschen.",
     "pocket_description": "Entdecken Sie qualitativ hochwertige Inhalte mithilfe von Pocket (jetzt Teil von von Mozilla), die Sie ansonsten verpassen würden.",
     "highlights_empty_state": "Surfen Sie los und wir zeigen Ihnen hier tolle Artikel, Videos und andere Seiten, die Sie kürzlich besucht oder als Lesezeichen gespeichert haben.",
-    "topstories_empty_state": "Jetzt kennen Sie die Neuigkeiten. Schauen Sie später wieder vorbei, um neue Informationen von {provider} zu erhalten. Können sie nicht warten? Wählen Sie ein beliebtes Thema und lesen Sie weitere interessante Geschichten aus dem Internet.",
+    "topstories_empty_state": "Jetzt kennen Sie die Neuigkeiten. Schauen Sie später wieder vorbei, um neue Informationen von {provider} zu erhalten. Können Sie nicht warten? Wählen Sie ein beliebtes Thema und lesen Sie weitere interessante Geschichten aus dem Internet.",
     "manual_migration_explanation2": "Probieren Sie Firefox aus und importieren Sie die Lesezeichen, Chronik und Passwörter eines anderen Browsers.",
     "manual_migration_cancel_button": "Nein, danke",
     "manual_migration_import_button": "Jetzt importieren"
   },
   "dsb": {
     "newtab_page_title": "Nowy rejtark",
     "default_label_loading": "Zacytujo se…",
     "header_top_sites": "Nejcesćej woglědane sedła",
@@ -1758,16 +1765,17 @@
     "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_highlights": "Destacados",
     "header_visit_again": "Visitar de nuevo",
     "header_bookmarks": "Marcadores recientes",
     "header_recommended_by": "Recomendado por {provider}",
     "header_bookmarks_placeholder": "Todavía no tienes ningún marcador.",
     "header_stories_from": "desde",
     "type_label_visited": "Visitados",
     "type_label_bookmarked": "En marcadores",
     "type_label_synced": "Sincronizado desde otro dispositivo",
@@ -1789,37 +1797,43 @@
     "confirm_history_delete_notice_p2": "Esta acción no se puede deshacer.",
     "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",
+    "section_info_send_feedback": "Enviar comentario",
+    "section_info_privacy_notice": "Aviso de privacidad",
     "welcome_title": "Bienvenido a la nueva pestaña",
     "welcome_body": "Firefox utilizará este espacio para mostrarte los marcadores, artículos y vídeos más relevantes y las páginas que has visitado recientemente, para que puedas acceder más rápido.",
     "welcome_label": "Identificar lo más destacado para ti",
     "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": "Personalizar la página Nueva pestaña",
     "settings_pane_header": "Preferencias de nueva pestaña",
-    "settings_pane_body": "Elige qué quieres ver al abrir una nueva pestaña",
+    "settings_pane_body2": "Elige lo quieras ver en esta página.",
     "settings_pane_search_header": "Buscar",
     "settings_pane_search_body": "Busca en la Web desde tu nueva pestaña.",
     "settings_pane_topsites_header": "Sitios populares",
     "settings_pane_topsites_body": "Accede a las páginas que más visitas.",
     "settings_pane_topsites_options_showmore": "Mostrar dos líneas",
     "settings_pane_bookmarks_header": "Marcadores recientes",
     "settings_pane_bookmarks_body": "Tus marcadores recién creados, fácilmente accesibles.",
     "settings_pane_visit_again_header": "Visitar de nuevo",
     "settings_pane_visit_again_body": "Firefox te mostrará partes de tu historial de navegación que te gustaría recordar o volver a visitar.",
-    "settings_pane_pocketstories_header": "Historias populares",
-    "settings_pane_pocketstories_body": "Pocket, que forma parte de la familia de Mozilla, te ayudará a encontrar contenido de alta calidad que puede que no encuentres de otra forma.",
+    "settings_pane_highlights_header": "Destacados",
+    "settings_pane_highlights_body2": "Vuelve a encontrar todas las cosas interesantes que hayas visitado o marcado recientemente.",
+    "settings_pane_highlights_options_bookmarks": "Marcadores",
+    "settings_pane_highlights_options_visited": "Sitios visitados",
+    "settings_pane_snippets_header": "Fragmentos de código",
+    "settings_pane_snippets_body": "Lee actualizaciones breves de Mozilla sobre Firefox, la cultura de internet y el típico meme aleatorio.",
     "settings_pane_done_button": "Hecho",
     "edit_topsites_button_text": "Editar",
     "edit_topsites_button_label": "Personalizar la sección de Sitios populares",
     "edit_topsites_showmore_button": "Mostrar más",
     "edit_topsites_showless_button": "Mostrar menos",
     "edit_topsites_done_button": "Hecho",
     "edit_topsites_pin_button": "Fijar este sitio",
     "edit_topsites_unpin_button": "Eliminar este sitio fijo",
@@ -1832,20 +1846,20 @@
     "topsites_form_url_placeholder": "Escribir o pegar una URL",
     "topsites_form_add_button": "Agregar",
     "topsites_form_save_button": "Guardar",
     "topsites_form_cancel_button": "Cancelar",
     "topsites_form_url_validation": "Se requiere una URL válida",
     "pocket_read_more": "Temas populares:",
     "pocket_read_even_more": "Ver más historias",
     "pocket_feedback_header": "Lo mejor de la web, confirmado por más de 25 millones de personas.",
-    "pocket_feedback_body": "Pocket, que forma parte de la familia de Mozilla, te ayudará a encontrar contenido de alta calidad que puede que no encuentres de otra forma.",
-    "pocket_send_feedback": "Enviar comentario",
+    "pocket_description": "Gracias a Pocket, que ahora forma parte de Mozilla, podrás descubrir contenido de alta calidad que de otra forma te perderías.",
+    "highlights_empty_state": "Empieza a navegar y nosotros te mostraremos aquí algunos de los mejores artículos, videos y otras páginas que hayas visitado recientemente o agregado a marcadores.",
     "topstories_empty_state": "Ya estás al día. Vuelve luego y busca más historias de {provider}. ¿No puedes esperar? Selecciona un tema popular y encontrás más historias alucinantes por toda la web.",
-    "manual_migration_explanation": "Prueba Firefox con tusmarcadores y sitios favoritos importados desde otro navegador.",
+    "manual_migration_explanation2": "Prueba Firefox con los marcadores, historial y contraseñas de otro navegador.",
     "manual_migration_cancel_button": "No, gracias",
     "manual_migration_import_button": "Importar ahora"
   },
   "es-MX": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Sitios favoritos",
     "header_stories": "Historias populares",
@@ -2358,17 +2372,17 @@
     "settings_pane_topsites_header": "Sites les plus visités",
     "settings_pane_topsites_body": "Accédez aux sites que vous consultez le plus.",
     "settings_pane_topsites_options_showmore": "Afficher deux lignes",
     "settings_pane_bookmarks_header": "Marque-pages récents",
     "settings_pane_bookmarks_body": "Vos nouveaux marque-pages, facilement accessibles.",
     "settings_pane_visit_again_header": "Visiter à nouveau",
     "settings_pane_visit_again_body": "Firefox affichera des extraits de votre historique de navigation dont vous pourriez vouloir vous souvenir ou que vous pourriez vouloir revisiter.",
     "settings_pane_highlights_header": "Éléments-clés",
-    "settings_pane_highlights_body2": "Retrouvez des pages inintéressantes que vous avez déjà visitées récemment ou ajoutées aux marque-pages.",
+    "settings_pane_highlights_body2": "Retrouvez des pages intéressantes que vous avez visitées récemment ou ajoutées aux marque-pages.",
     "settings_pane_highlights_options_bookmarks": "Marque-pages",
     "settings_pane_highlights_options_visited": "Sites visités",
     "settings_pane_snippets_header": "Brèves",
     "settings_pane_snippets_body": "Consultez les brèves de Mozilla à propos de Firefox, la culture Internet, mais aussi quelques mèmes Internet de temps en temps.",
     "settings_pane_done_button": "Terminé",
     "edit_topsites_button_text": "Modifier",
     "edit_topsites_button_label": "Personnaliser la section Sites les plus visités",
     "edit_topsites_showmore_button": "Afficher plus",
@@ -3002,16 +3016,17 @@
     "manual_migration_cancel_button": "Ně, dźakuju so",
     "manual_migration_import_button": "Nětko importować"
   },
   "hu": {
     "newtab_page_title": "Új lap",
     "default_label_loading": "Betöltés…",
     "header_top_sites": "Népszerű oldalak",
     "header_stories": "Népszerű történetek",
+    "header_highlights": "Kiemelések",
     "header_visit_again": "Látogasson el ismét",
     "header_bookmarks": "Friss könyvjelzők",
     "header_recommended_by": "A(z) {provider} ajánlásával",
     "header_bookmarks_placeholder": "Még nincs könyvjelzője.",
     "header_stories_from": "innen:",
     "type_label_visited": "Látogatott",
     "type_label_bookmarked": "Könyvjelzőzött",
     "type_label_synced": "Másik eszközről szinkronizálva",
@@ -3033,37 +3048,43 @@
     "confirm_history_delete_notice_p2": "Ez a művelet nem vonható vissza.",
     "menu_action_save_to_pocket": "Mentés a Pocketbe",
     "search_for_something_with": "„{search_term}” keresése ezzel:",
     "search_button": "Keresés",
     "search_header": "{search_engine_name} keresés",
     "search_web_placeholder": "Keresés a weben",
     "search_settings": "Keresési beállítások módosítása",
     "section_info_option": "Információ",
+    "section_info_send_feedback": "Visszajelzés küldése",
+    "section_info_privacy_notice": "Adatvédelmi nyilatkozat",
     "welcome_title": "Üdvözöljük az új lapon",
     "welcome_body": "A Firefox ezt a területet a leginkább releváns könyvjelzők, cikkek, videók és nemrég látogatott oldalak megjelenítésére fogja használni, így könnyedén visszatalálhat hozzájuk.",
     "welcome_label": "A kiemeléseinek azonosítása",
     "time_label_less_than_minute": "<1 p",
     "time_label_minute": "{number} p",
     "time_label_hour": "{number} ó",
     "time_label_day": "{number} n",
     "settings_pane_button_label": "Az Új lap oldal személyre szabása",
     "settings_pane_header": "Új lap beállításai",
-    "settings_pane_body": "Válassza ki, hogy mit lát, amikor megnyit egy új lapot.",
+    "settings_pane_body2": "Válassza ki, hogy mit akar látni ezen az oldalon.",
     "settings_pane_search_header": "Keresés",
     "settings_pane_search_body": "Keresés a weben az új lapon.",
     "settings_pane_topsites_header": "Népszerű oldalak",
     "settings_pane_topsites_body": "A leggyakrabban látogatott webhelyek elérése.",
     "settings_pane_topsites_options_showmore": "Két sor megjelenítése",
     "settings_pane_bookmarks_header": "Friss könyvjelzők",
     "settings_pane_bookmarks_body": "A frissen létrehozott könyvjelzői egy praktikus helyen.",
     "settings_pane_visit_again_header": "Látogasson el ismét",
     "settings_pane_visit_again_body": "A Firefox megjeleníti a böngészési előzményeinek azt a részét, amelyet lehet hogy meg szeretne jegyezni, vagy ahová vissza akar térni.",
-    "settings_pane_pocketstories_header": "Népszerű történetek",
-    "settings_pane_pocketstories_body": "A Pocket a Mozilla család tagja, segít az olyan jó minőségű tartalmak fellelésében, melyekkel egyébként nem is találkozott volna.",
+    "settings_pane_highlights_header": "Kiemelések",
+    "settings_pane_highlights_body2": "Találjon vissza azokhoz az érdekes dolgokhoz, amelyeket meglátogatott vagy könyvjelzőzött.",
+    "settings_pane_highlights_options_bookmarks": "Könyvjelzők",
+    "settings_pane_highlights_options_visited": "Látogatott helyek",
+    "settings_pane_snippets_header": "Töredékek",
+    "settings_pane_snippets_body": "Olvasson rövid és érdekes híreket a Mozillától, a Firefoxról, az internetes kultúráról, és időnként kapjon mémeket.",
     "settings_pane_done_button": "Kész",
     "edit_topsites_button_text": "Szerkesztés",
     "edit_topsites_button_label": "A Népszerű oldalak rész testreszabása",
     "edit_topsites_showmore_button": "Több megjelenítése",
     "edit_topsites_showless_button": "Kevesebb megjelenítése",
     "edit_topsites_done_button": "Kész",
     "edit_topsites_pin_button": "Webhely rögzítése",
     "edit_topsites_unpin_button": "Rögzítés feloldása",
@@ -3076,20 +3097,20 @@
     "topsites_form_url_placeholder": "Írjon vagy illesszen be egy URL-t",
     "topsites_form_add_button": "Hozzáadás",
     "topsites_form_save_button": "Mentés",
     "topsites_form_cancel_button": "Mégse",
     "topsites_form_url_validation": "Érvényes URL szükséges",
     "pocket_read_more": "Népszerű témák:",
     "pocket_read_even_more": "További történetek",
     "pocket_feedback_header": "A web legjava, több mint 25 millió ember válogatásában.",
-    "pocket_feedback_body": "A Pocket a Mozilla család tagja, segít az olyan jó minőségű tartalmak fellelésében, melyekkel egyébként nem is találkozott volna.",
-    "pocket_send_feedback": "Visszajelzés küldése",
+    "pocket_description": "Fedezzen fel olyan, magas minőségű tartalmakat, amelyek egyébként elkerülnék a figyelmét, a Pocket segítségével, amely most már a Mozilla része.",
+    "highlights_empty_state": "Kezdjen el böngészni, és itt fognak megjelenni azok a nagyszerű cikkek, videók és más lapok, amelyeket nemrég meglátogatott vagy könyvjelzőzött.",
     "topstories_empty_state": "Már felzárkózott. Nézzen vissza később a legújabb {provider} hírekért. Nem tud várni? Válasszon egy népszerű témát, hogy még több sztorit találjon a weben.",
-    "manual_migration_explanation": "Próbálja ki a Firefoxot egy másik böngészőben lévő kedvenc oldalaival és könyvjelzőivel.",
+    "manual_migration_explanation2": "Próbálja ki a Firefoxot másik böngészőből származó könyvjelzőkkel, előzményekkel és jelszavakkal.",
     "manual_migration_cancel_button": "Köszönöm, nem",
     "manual_migration_import_button": "Importálás most"
   },
   "hy-AM": {
     "newtab_page_title": "Նոր ներդիր",
     "default_label_loading": "Բեռնվում է...",
     "header_top_sites": "Լավագույն կայքեր",
     "header_highlights": "Գունանշում",
@@ -3118,16 +3139,17 @@
     "time_label_hour": "{number} ժ",
     "time_label_day": "{number} օր"
   },
   "id": {
     "newtab_page_title": "Tab Baru",
     "default_label_loading": "Memuat…",
     "header_top_sites": "Situs Teratas",
     "header_stories": "Cerita Utama",
+    "header_highlights": "Sorotan",
     "header_visit_again": "Kunjungi Lagi",
     "header_bookmarks": "Markah Terbaru",
     "header_recommended_by": "Disarankan oleh {provider}",
     "header_bookmarks_placeholder": "Anda belum memiliki markah.",
     "header_stories_from": "dari",
     "type_label_visited": "Dikunjungi",
     "type_label_bookmarked": "Dimarkahi",
     "type_label_synced": "Disinkronkan dari perangkat lain",
@@ -3149,37 +3171,43 @@
     "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",
+    "section_info_send_feedback": "Kirim Umpan Balik",
+    "section_info_privacy_notice": "Kebijakan Privasi",
     "welcome_title": "Selamat datang di tab baru",
     "welcome_body": "Firefox akan menggunakan ruang ini untuk menampilkan markah, artikel, video, dan laman yang baru-baru ini dikunjungi, yang paling relevan agar Anda bisa kembali mengunjunginya dengan mudah.",
     "welcome_label": "Mengidentifikasi Sorotan Anda",
     "time_label_less_than_minute": "<1 mnt",
     "time_label_minute": "{number} mnt",
     "time_label_hour": "{number} jam",
     "time_label_day": "{number} hr",
     "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_body2": "Pilih apa yang Anda lihat di halaman ini.",
     "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_highlights_header": "Sorotan",
+    "settings_pane_highlights_body2": "Temukan jalan kembali ke hal menarik yang baru saja Anda kunjungi atau dimarkah.",
+    "settings_pane_highlights_options_bookmarks": "Markah",
+    "settings_pane_highlights_options_visited": "Situs Terkunjungi",
+    "settings_pane_snippets_header": "Catatan Kecil",
+    "settings_pane_snippets_body": "Baca info pendek terbaru dari Mozilla tentang Firefox, budaya internet dan beberapa meme acak.",
     "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",
     "edit_topsites_pin_button": "Sematkan situs ini",
     "edit_topsites_unpin_button": "Lepaskan situs ini",
@@ -3192,20 +3220,20 @@
     "topsites_form_url_placeholder": "Ketik atau tempel URL",
     "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_description": "Temukan konten berkualitas tinggi yang mungkin Anda lewatkan dengan bantuan Pocket, yang sekarang menjadi bagian dari Mozilla.",
+    "highlights_empty_state": "Mulai menjelajah, dan kami akan menampilkan beberapa artikel bagus, video, dan halaman lain yang baru saja Anda kunjungi atau termarkah di sini.",
     "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_explanation2": "Coba Firefox dengan markah, riwayat, dan sandi dari peramban lain.",
     "manual_migration_cancel_button": "Tidak, Terima kasih",
     "manual_migration_import_button": "Impor Sekarang"
   },
   "it": {
     "newtab_page_title": "Nuova scheda",
     "default_label_loading": "Caricamento…",
     "header_top_sites": "Siti principali",
     "header_stories": "Storie principali",
@@ -3483,16 +3511,17 @@
     "manual_migration_cancel_button": "არა, გმადლობთ",
     "manual_migration_import_button": "ახლავე გადმოტანა"
   },
   "kab": {
     "newtab_page_title": "Iccer amaynut",
     "default_label_loading": "Asali…",
     "header_top_sites": "Ismal ifazen",
     "header_stories": "Tiqsiɣin ifazen",
+    "header_highlights": "Asebrureq",
     "header_visit_again": "Rzu tikelt-nniḍen",
     "header_bookmarks": "Ticraḍ n melmi kan",
     "header_recommended_by": "Iwelleh-it-id {provider}",
     "header_bookmarks_placeholder": "Ur ɣur-k ara ticraḍ yakan.",
     "header_stories_from": "seg",
     "type_label_visited": "Yettwarza",
     "type_label_bookmarked": "Yettwacreḍ",
     "type_label_synced": "Yemtawi seg ibenk-nniḍen",
@@ -3514,37 +3543,43 @@
     "confirm_history_delete_notice_p2": "Tigawt-agi ur tettuɣal ara ar deffir.",
     "menu_action_save_to_pocket": "Sekles ɣer Pocket",
     "search_for_something_with": "Nadi γef {search_term} s:",
     "search_button": "Nadi",
     "search_header": "Anadi {search_engine_name}",
     "search_web_placeholder": "Nadi di Web",
     "search_settings": "Snifel iγewwaṛen n unadi",
     "section_info_option": "Talɣut",
+    "section_info_send_feedback": "Azen tikti",
+    "section_info_privacy_notice": "Tasertit n tbaḍnit",
     "welcome_title": "Ansuf ar yiccer amaynut",
     "welcome_body": "Firefox ad iseqdec tallunt akken ad d-yesken akk ticraḍ n isebtar iwulmen, imagraden, tividyutin, akked isebtar aniɣer terziḍ melmi kan, ihi tzemreḍ ad d-uɣaleḍ ɣer-sen s wudem fessusen.",
     "welcome_label": "Asulu n iferdisen tisura",
     "time_label_less_than_minute": "<1 n tesdat",
     "time_label_minute": "{number} n tesdatin",
     "time_label_hour": "{number} n isragen",
     "time_label_day": "{number}n wussan",
     "settings_pane_button_label": "Sagen asebter n yiccer-ik amaynut",
     "settings_pane_header": "Ismenyifen n yiccer amaynut",
-    "settings_pane_body": "Fren ayen ara twaliḍ ticki teldiḍ iccer imaynut.",
+    "settings_pane_body2": "Fren ayen ad twaliḍ deg usebter-agi.",
     "settings_pane_search_header": "Nadi",
     "settings_pane_search_body": "Nadi di Web seg iccer-ik amaynut.",
     "settings_pane_topsites_header": "Ismal ifazen",
     "settings_pane_topsites_body": "Kcem ar yesmal web i trezzuḍ s waṭas.",
     "settings_pane_topsites_options_showmore": "Sken sin izirigen",
     "settings_pane_bookmarks_header": "Ticraḍ n melmi kan",
     "settings_pane_bookmarks_body": "Ticraḍ yettwarnan melmi kan deg iwen n umdiq ɣef afus.",
     "settings_pane_visit_again_header": "Rzu tikelt-nniḍen",
     "settings_pane_visit_again_body": "Firefox ad d-yesken tukkist n umazray-ik n tunigin i tzemreḍ ad twalid tikelt-nniḍen.",
-    "settings_pane_pocketstories_header": "Tiqsiɣin ifazen",
-    "settings_pane_pocketstories_body": "Pocket, aɛeggal n twaxult n Mozilla, ak-d-yefk afus ad twaliḍ agbur n tɣara meqqren i tzemred ad tzegleḍ.",
+    "settings_pane_highlights_header": "Asebrureq",
+    "settings_pane_highlights_body2": "Aff abrid-ik γer wayen i tḥemmleḍ i γef terziḍ yakan neγ tcerḍeḍ-t.",
+    "settings_pane_highlights_options_bookmarks": "Ticraḍ n isebtar",
+    "settings_pane_highlights_options_visited": "Ismal yettwarzan",
+    "settings_pane_snippets_header": "Tiwzillin",
+    "settings_pane_snippets_body": "Wali issalen n Mozilla γef Firefox, adlis internet, akked issalen nniṣen sya γer da.",
     "settings_pane_done_button": "Immed",
     "edit_topsites_button_text": "Ẓreg",
     "edit_topsites_button_label": "Sagen tigezmi n ismal ifazen",
     "edit_topsites_showmore_button": "Sken ugar",
     "edit_topsites_showless_button": "Sken qel",
     "edit_topsites_done_button": "Immed",
     "edit_topsites_pin_button": "Ṭṭef asmel-agi",
     "edit_topsites_unpin_button": "Serreḥ asmel-agi",
@@ -3557,20 +3592,20 @@
     "topsites_form_url_placeholder": "Aru neɣ sekcem tansa URL",
     "topsites_form_add_button": "Rnu",
     "topsites_form_save_button": "Sekles",
     "topsites_form_cancel_button": "Sefsex",
     "topsites_form_url_validation": "Tansa URL tameɣtut tettwasra",
     "pocket_read_more": "Isental ittwasnen aṭas:",
     "pocket_read_even_more": "Wali ugar n teqsiḍin",
     "pocket_feedback_header": "D amezwaru n Web, ittwafren sγur ugar 25 imelyan n imdanen.",
-    "pocket_feedback_body": "Pocket, aɛeggal n twaxult n Mozilla, ak-d-yefk afus ad twaliḍ agbur n tɣara meqqren i tzemred ad tzegleḍ.",
-    "pocket_send_feedback": "Azen tikti",
+    "pocket_description": "S lmendad n Pocket n Mozillan wali aqbur ifazen aṭas, s ttawil-a werǧin ad tzegleḍ taγawsa.",
+    "highlights_empty_state": "Bdu tuniginn sakin nekkni ad k-n-sken imagraden, tividyutin, akked isebtar nniḍen i γef terziḍ yakan neγ i tceṛḍeḍ dagi.",
     "topstories_empty_state": "Ulac wiyaḍ. Uɣal-d ticki s wugar n imagraden seg {provider}. Ur tebɣiḍ ara ad terǧuḍ? Fren asentel seg wid yettwasnen akken ad twaliḍ imagraden yelhan di Web.",
-    "manual_migration_explanation": "Ɛreḍ Firefox s ismal-ik inurifen akked ticraḍ seg iminig-nniḍen.",
+    "manual_migration_explanation2": "Σreḍ Firefox s ticṛaḍ n isebtar, amazray akked awalen uffiren sγur ilinigen nniḍen.",
     "manual_migration_cancel_button": "Ala, tanemmirt",
     "manual_migration_import_button": "Kter tura"
   },
   "kk": {
     "newtab_page_title": "Жаңа бет",
     "default_label_loading": "Жүктелуде…",
     "header_top_sites": "Топ сайттар",
     "header_stories": "Топ хикаялар",
@@ -3623,19 +3658,21 @@
     "settings_pane_topsites_header": "Топ сайттар",
     "settings_pane_topsites_body": "Көбірек қаралатын сайттарға қатынау.",
     "settings_pane_topsites_options_showmore": "Екі жолды көрсету",
     "settings_pane_bookmarks_header": "Соңғы бетбелгілер",
     "settings_pane_bookmarks_body": "Сіздің жаңадан жасалған бетбелгілер бір ыңғайлы жерде.",
     "settings_pane_visit_again_header": "Қайтадан шолу",
     "settings_pane_visit_again_body": "Firefox сізге есте сақтауды немесе қайта шолуды қалауыңыз мүмкін тарихыңыздың бөліктерін көрсетеді.",
     "settings_pane_highlights_header": "Ерекше жаңалықтар",
+    "settings_pane_highlights_body2": "Сіз жақында қараған немесе бетбелгілерге қосқан қызықты нәрселерге қайтатын жолды табыңыз.",
     "settings_pane_highlights_options_bookmarks": "Бетбелгілер",
     "settings_pane_highlights_options_visited": "Ашылған сайттар",
     "settings_pane_snippets_header": "Үзінділер",
+    "settings_pane_snippets_body": "Mozilla-дан Firefox және интернет мәдениеті туралы қысқа жаңалықтарды, және кездейсоқ мемдерді оқыңыз.",
     "settings_pane_done_button": "Дайын",
     "edit_topsites_button_text": "Түзету",
     "edit_topsites_button_label": "Топ сайттар санатын баптау",
     "edit_topsites_showmore_button": "Көбірек көрсету",
     "edit_topsites_showless_button": "Азырақ көрсету",
     "edit_topsites_done_button": "Дайын",
     "edit_topsites_pin_button": "Бұл сайтты жапсыру",
     "edit_topsites_unpin_button": "Бұл сайтты бекітуден алып тастау",
@@ -3648,17 +3685,20 @@
     "topsites_form_url_placeholder": "Сілтемені теріңіз немесе кірістіріңіз",
     "topsites_form_add_button": "Қосу",
     "topsites_form_save_button": "Сақтау",
     "topsites_form_cancel_button": "Бас тарту",
     "topsites_form_url_validation": "Жарамды сілтеме керек",
     "pocket_read_more": "Әйгілі тақырыптар:",
     "pocket_read_even_more": "Көбірек хикаяларды қарау",
     "pocket_feedback_header": "Интернеттің ең жақсысы, 25 миллион адаммен танылған.",
+    "pocket_description": "Ол болмаса, сіз жіберіп алатын мүмкіндігі бар жоғары сапалы құраманы Pocket көмегімен табыңыз, ол енді Mozilla-ның бөлігі болып табылады.",
+    "highlights_empty_state": "Шолуды бастаңыз, сіз жақында шолған немесе бетбелгілерге қосқан тамаша мақалалар, видеолар немесе басқа парақтардың кейбіреулері осында көрсетіледі.",
     "topstories_empty_state": "Дайын. {provider} ұсынған көбірек мақалаларды алу үшін кейінірек тексеріңіз. Күте алмайсыз ба? Интернеттен көбірек тамаша мақалаларды алу үшін әйгілі теманы таңдаңыз.",
+    "manual_migration_explanation2": "Firefox-ты басқа браузер бетбелгілері, тарихы және парольдерімен қолданып көріңіз.",
     "manual_migration_cancel_button": "Жоқ, рахмет",
     "manual_migration_import_button": "Қазір импорттау"
   },
   "km": {
     "newtab_page_title": "ផ្ទាំង​ថ្មី",
     "default_label_loading": "កំពុង​ផ្ទុក...",
     "header_top_sites": "វិបសាយ​លើ​គេ",
     "header_highlights": "ការ​រំលេច",
@@ -4371,16 +4411,17 @@
     "topsites_form_url_placeholder": "Skriv eller lim inn en URL",
     "topsites_form_add_button": "Legg til",
     "topsites_form_save_button": "Lagre",
     "topsites_form_cancel_button": "Avbryt",
     "topsites_form_url_validation": "Gyldig URL er nødvendig",
     "pocket_read_more": "Populære emner:",
     "pocket_read_even_more": "Vis flere saker",
     "pocket_feedback_header": "Det beste av nettet, kurert av over 25 millioner mennesker.",
+    "pocket_description": "Oppdag høykvalitetsinnhold som du ellers ville gå glipp av, ved hjelp av Pocket, som nå er en del av Mozilla.",
     "highlights_empty_state": "Begynn å surfe, og vi viser noen av de beste artiklene, videoer og andre sider du nylig har besøkt eller bokmerket her.",
     "topstories_empty_state": "Du har tatt igjen. Kom tilbake senere for flere topphistorier fra {provider}. Kan du ikke vente? Velg et populært emne for å finne flere gode artikler fra hele Internett.",
     "manual_migration_explanation2": "Prøv Firefox med bokmerkene, historikk og passord fra en annen nettleser.",
     "manual_migration_cancel_button": "Nei takk",
     "manual_migration_import_button": "Importer nå"
   },
   "ne-NP": {
     "newtab_page_title": "नयाँ ट्याब",
@@ -4986,17 +5027,17 @@
     "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_dismiss": "Sbittar",
     "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",
@@ -5366,31 +5407,33 @@
     "settings_pane_bookmarks_header": "Nedavni zaznamki",
     "settings_pane_bookmarks_body": "Vaši novo ustvarjeni zaznamki na enem mestu.",
     "settings_pane_visit_again_header": "Obiščite znova",
     "settings_pane_visit_again_body": "Firefox vam bo prikazoval dele zgodovine brskanja, ki bi se jih morda želeli spomniti ali se nanje vrniti.",
     "settings_pane_highlights_header": "Poudarki",
     "settings_pane_highlights_body2": "Najdite pot nazaj do zanimivih strani, ki ste jih nedavno obiskali ali dodali med zaznamke.",
     "settings_pane_highlights_options_bookmarks": "Zaznamki",
     "settings_pane_highlights_options_visited": "Obiskane strani",
+    "settings_pane_snippets_header": "Izrezki",
+    "settings_pane_snippets_body": "Spremljajte kratke novice o Mozilli in Firefoxu, kulturi interneta in si občasno oglejte kak meme.",
     "settings_pane_done_button": "Končano",
     "edit_topsites_button_text": "Uredi",
     "edit_topsites_button_label": "Prilagodite odsek Glavne strani",
     "edit_topsites_showmore_button": "Prikaži več",
     "edit_topsites_showless_button": "Prikaži manj",
     "edit_topsites_done_button": "Končano",
     "edit_topsites_pin_button": "Pripni to stran",
     "edit_topsites_unpin_button": "Odpni to stran",
     "edit_topsites_edit_button": "Uredi to stran",
     "edit_topsites_dismiss_button": "Odstrani to stran",
     "edit_topsites_add_button": "Dodaj",
     "topsites_form_add_header": "Nova glavna stran",
     "topsites_form_edit_header": "Uredi glavno stran",
-    "topsites_form_title_placeholder": "Vnesite naslov",
-    "topsites_form_url_placeholder": "Vnesite ali prilepite URL",
+    "topsites_form_title_placeholder": "Vnesite ime",
+    "topsites_form_url_placeholder": "Vnesite ali prilepite spletni naslov",
     "topsites_form_add_button": "Dodaj",
     "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_description": "Odkrijte kakovostno vsebino, ki bi jo sicer spregledali, s pomočjo Pocketa (zdaj dela Mozille).",
@@ -5720,16 +5763,17 @@
     "manual_migration_cancel_button": "பரவாயில்லை",
     "manual_migration_import_button": "இப்போது இறக்கு"
   },
   "te": {
     "newtab_page_title": "కొత్త ట్యాబు",
     "default_label_loading": "వస్తోంది…",
     "header_top_sites": "మేటి సైట్లు",
     "header_stories": "ముఖ్య కథనాలు",
+    "header_highlights": "విశేషాలు",
     "header_visit_again": "మళ్లీ సందర్శించండి",
     "header_bookmarks": "ఇటీవలి ఇష్టాంశములు",
     "header_recommended_by": "{provider}చే సిఫార్సు చేయబడినది",
     "header_bookmarks_placeholder": "మీకు ఇంకా ఎటువంటి ఇష్టాంశాలు లేవు.",
     "header_stories_from": "నుండి",
     "type_label_visited": "సందర్శించినవి",
     "type_label_bookmarked": "ఇష్టాంశము చేయబడినది",
     "type_label_synced": "మరో పరికరం నుంచి సమకాలీకరించి తెచ్చుకున్నవి",
@@ -5751,37 +5795,40 @@
     "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": "సమాచారం",
+    "section_info_send_feedback": "అభిప్రాయాన్ని పంపండి",
+    "section_info_privacy_notice": "గోప్యతా విధానం",
     "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": "మీ కొత్త ట్యాబు పేజీని మలచుకోండి",
     "settings_pane_header": "కొత్త ట్యాబు అభిరుచులు",
-    "settings_pane_body": "మీరు కొత్త ట్యాబు తెరిచినప్పుడు ఏం చూడాలో ఎంచుకోండి.",
+    "settings_pane_body2": "మీరు ఈ పేజీలో చూసేదాన్ని ఎంచుకోండి.",
     "settings_pane_search_header": "వెతకడం",
     "settings_pane_search_body": "కొత్త ట్యాబు నుండే జాలంలో వెతకండి.",
     "settings_pane_topsites_header": "మేటి సైట్లు",
     "settings_pane_topsites_body": "మీరు ఎక్కువగా చూసే వెబ్‌సైట్లను చూడండి.",
     "settings_pane_topsites_options_showmore": "రెండు వరుసలు చూపించు",
     "settings_pane_bookmarks_header": "ఇటీవలి బుక్మార్క్లు",
     "settings_pane_bookmarks_body": "ఒక సులభ స్థానంలో మీ క్రొత్తగా సృష్టించిన బుక్మార్క్లు.",
     "settings_pane_visit_again_header": "మళ్లీ సందర్శించండి",
     "settings_pane_visit_again_body": "మీరు బ్రౌజింగ్ చరిత్రలో గుర్తుంచుకోవాల్సిన  లేదా తిరిగి పొందవలసిన భాగాలను చూపిస్తుంది.",
-    "settings_pane_pocketstories_header": "ముఖ్య కథనాలు",
-    "settings_pane_pocketstories_body": "Mozilla కుటుంబం యొక్క Pocket, మీరు కనుగొనలేకపోయే అధిక-నాణ్యత విషయముకి మిమ్మల్ని అనుసంధానించడానికి సహాయపడుతుంది.",
+    "settings_pane_highlights_header": "విశేషాలు",
+    "settings_pane_highlights_options_bookmarks": "ఇష్టాంశాలు",
+    "settings_pane_highlights_options_visited": "చూసిన సైటులు",
     "settings_pane_done_button": "పూర్తయింది",
     "edit_topsites_button_text": "మార్చు",
     "edit_topsites_button_label": "మీ మేటి సైట్ల విభాగాన్ని మలచుకోండి",
     "edit_topsites_showmore_button": "ఇంకా చూపించు",
     "edit_topsites_showless_button": "కొన్నే చూపించు",
     "edit_topsites_done_button": "పూర్తయింది",
     "edit_topsites_pin_button": "ఈ సైటును ఇక్కడ గుచ్చు",
     "edit_topsites_unpin_button": "ఈ సైటుకి పిన్నుని తీసివేయండి",
@@ -5794,20 +5841,17 @@
     "topsites_form_url_placeholder": "URL ను టైప్ చేయండి లేదా అతికించండి",
     "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": "Mozilla కుటుంబం యొక్క Pocket, మీరు కనుగొనలేకపోయే అధిక-నాణ్యత విషయముకి మిమ్మల్ని అనుసంధానించడానికి సహాయపడుతుంది.",
-    "pocket_send_feedback": "అభిప్రాయాన్ని పంపండి",
     "topstories_empty_state": "మీరు పట్టుబడ్డారు. {provider} నుండి మరింత అగ్ర కథనాల కోసం తరువాత తనిఖీ చేయండి. వేచి ఉండలేరా? జాలములోని అంతటి నుండి మరింత గొప్ప కథనాలను కనుగొనడానికి ప్రసిద్ధ అంశం ఎంచుకోండి.",
-    "manual_migration_explanation": "మరొక విహరణి నుండి మీకు ఇష్టమైన సైట్లు మరియు ఇష్టంశాలతో Firefox ను ప్రయత్నించండి.",
     "manual_migration_cancel_button": "అడిగినందుకు ధన్యవాదాలు, వద్దు",
     "manual_migration_import_button": "ఇప్పుడే దిగుమతి చేయండి"
   },
   "th": {
     "newtab_page_title": "แท็บใหม่",
     "default_label_loading": "กำลังโหลด…",
     "header_top_sites": "ไซต์เด่น",
     "header_stories": "เรื่องราวเด่น",
@@ -5860,16 +5904,17 @@
     "settings_pane_topsites_header": "ไซต์เด่น",
     "settings_pane_topsites_body": "เข้าถึงเว็บไซต์ที่คุณเยี่ยมชมมากที่สุด",
     "settings_pane_topsites_options_showmore": "แสดงสองแถว",
     "settings_pane_bookmarks_header": "ที่คั่นหน้าเมื่อเร็ว ๆ นี้",
     "settings_pane_bookmarks_body": "ที่คั่นหน้าที่สร้างใหม่ของคุณในตำแหน่งที่ตั้งเดียวที่สะดวก",
     "settings_pane_visit_again_header": "เยี่ยมชมอีกครั้ง",
     "settings_pane_highlights_header": "รายการเด่น",
     "settings_pane_highlights_options_bookmarks": "ที่คั่นหน้า",
+    "settings_pane_highlights_options_visited": "ไซต์ที่เยี่ยมชมแล้ว",
     "settings_pane_done_button": "เสร็จสิ้น",
     "edit_topsites_button_text": "แก้ไข",
     "edit_topsites_button_label": "ปรับแต่งส่วนไซต์เด่นของคุณ",
     "edit_topsites_showmore_button": "แสดงเพิ่มเติม",
     "edit_topsites_showless_button": "แสดงน้อยลง",
     "edit_topsites_done_button": "เสร็จสิ้น",
     "edit_topsites_pin_button": "ปักหมุดไซต์นี้",
     "edit_topsites_unpin_button": "ถอนหมุดไซต์นี้",
@@ -5882,16 +5927,17 @@
     "topsites_form_url_placeholder": "พิมพ์หรือวาง URL",
     "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 ล้านคน",
+    "manual_migration_explanation2": "ลอง Firefox ด้วยที่คั่นหน้า, ประวัติ และรหัสผ่านจากเบราว์เซอร์อื่น",
     "manual_migration_cancel_button": "ไม่ ขอบคุณ",
     "manual_migration_import_button": "นำเข้าตอนนี้"
   },
   "tl": {
     "newtab_page_title": "Bagong Tab",
     "default_label_loading": "Pagkarga…",
     "header_top_sites": "Tuktok na mga Site",
     "header_highlights": "Highlights",
@@ -6289,18 +6335,18 @@
     "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_open_new_window": "新建窗口打开",
+    "menu_action_open_private_window": "新建隐私浏览窗口打开",
     "menu_action_dismiss": "隐藏",
     "menu_action_delete": "从历史记录中删除",
     "menu_action_pin": "固定",
     "menu_action_unpin": "取消固定",
     "confirm_history_delete_p1": "确定删除此页面在您的历史记录中的所有记录?",
     "confirm_history_delete_notice_p2": "此操作不能撤销。",
     "menu_action_save_to_pocket": "保存到 Pocket",
     "search_for_something_with": "搜索 {search_term},使用:",
--- a/browser/extensions/activity-stream/install.rdf.in
+++ b/browser/extensions/activity-stream/install.rdf.in
@@ -3,17 +3,17 @@
 #filter substitution
 
 <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>activity-stream@mozilla.org</em:id>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
-    <em:version>2017.09.08.0882-3dbf720c</em:version>
+    <em:version>2017.09.11.1306-373d9fc</em:version>
     <em:name>Activity Stream</em:name>
     <em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -72,16 +72,20 @@ const PREFS_CONFIG = new Map([
   ["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
   }],
+  ["prerender", {
+    title: "Use the prerendered version of activity-stream.html. This is set automatically by PrefsFeed.jsm.",
+    value: true
+  }],
   ["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
   }],
--- a/browser/extensions/activity-stream/lib/HighlightsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/HighlightsFeed.jsm
@@ -5,23 +5,25 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 const {shortURL} = Cu.import("resource://activity-stream/lib/ShortURL.jsm", {});
 const {SectionsManager} = Cu.import("resource://activity-stream/lib/SectionsManager.jsm", {});
+const {TOP_SITES_SHOWMORE_LENGTH} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
 const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 
 const HIGHLIGHTS_MAX_LENGTH = 9;
 const HIGHLIGHTS_UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
+const MANY_EXTRA_LENGTH = HIGHLIGHTS_MAX_LENGTH * 5 + TOP_SITES_SHOWMORE_LENGTH;
 const SECTION_ID = "highlights";
 
 this.HighlightsFeed = class HighlightsFeed {
   constructor() {
     this.highlightsLastUpdated = 0;
     this.highlights = [];
     this.dedupe = new Dedupe(this._dedupeKey);
   }
@@ -38,29 +40,50 @@ this.HighlightsFeed = class HighlightsFe
     SectionsManager.enableSection(SECTION_ID);
   }
 
   uninit() {
     SectionsManager.disableSection(SECTION_ID);
   }
 
   async fetchHighlights(broadcast = false) {
-    this.highlights = await NewTabUtils.activityStreamLinks.getHighlights();
-    for (let highlight of this.highlights) {
-      highlight.hostname = shortURL(Object.assign({}, highlight, {url: highlight.url}));
-      highlight.image = highlight.preview_image_url;
-      if (highlight.bookmarkGuid) {
-        highlight.type = "bookmark";
+    // Request more than the expected length to allow for items being removed by
+    // deduping against Top Sites or multiple history from the same domain, etc.
+    const manyPages = await NewTabUtils.activityStreamLinks.getHighlights({numItems: MANY_EXTRA_LENGTH});
+
+    // Remove any Highlights that are in Top Sites already
+    const deduped = this.dedupe.group(this.store.getState().TopSites.rows, manyPages)[1];
+
+    // Keep all "bookmark"s and at most one (most recent) "history" per host
+    this.highlights = [];
+    const hosts = new Set();
+    for (const page of deduped) {
+      const hostname = shortURL(page);
+      // Skip this history page if we already something from the same host
+      if (page.type === "history" && hosts.has(hostname)) {
+        continue;
+      }
+
+      // We want the page, so update various fields for UI
+      Object.assign(page, {
+        hostname,
+        image: page.preview_image_url,
+        type: page.bookmarkGuid ? "bookmark" : page.type
+      });
+
+      // Add the "bookmark" or not-skipped "history"
+      this.highlights.push(page);
+      hosts.add(hostname);
+
+      // Skip the rest if we have enough items
+      if (this.highlights.length === HIGHLIGHTS_MAX_LENGTH) {
+        break;
       }
     }
 
-    // Remove any Highlights that are in Top Sites already
-    const deduped = this.dedupe.group(this.store.getState().TopSites.rows, this.highlights);
-    this.highlights = deduped[1];
-
     SectionsManager.updateSection(SECTION_ID, {rows: this.highlights}, this.highlightsLastUpdated === 0 || broadcast);
     this.highlightsLastUpdated = Date.now();
   }
 
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
--- a/browser/extensions/activity-stream/lib/NewTabInit.jsm
+++ b/browser/extensions/activity-stream/lib/NewTabInit.jsm
@@ -7,20 +7,38 @@ const {utils: Cu} = Components;
 
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 /**
  * NewTabInit - A placeholder for now. This will send a copy of the state to all
  *              newly opened tabs.
  */
 this.NewTabInit = class NewTabInit {
+  constructor() {
+    this._queue = new Set();
+  }
+  reply(target) {
+    const action = {type: at.NEW_TAB_INITIAL_STATE, data: this.store.getState()};
+    this.store.dispatch(ac.SendToContent(action, target));
+  }
   onAction(action) {
-    let newAction;
     switch (action.type) {
-      case at.NEW_TAB_LOAD:
-        newAction = {type: at.NEW_TAB_INITIAL_STATE, data: this.store.getState()};
-        this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
+      case at.NEW_TAB_STATE_REQUEST:
+        // If localization hasn't been loaded yet, we should wait for it.
+        if (!this.store.getState().App.strings) {
+          this._queue.add(action.meta.fromTarget);
+          return;
+        }
+        this.reply(action.meta.fromTarget);
+        break;
+      case at.LOCALE_UPDATED:
+        // If the queue is full because we were waiting for strings,
+        // dispatch them now.
+        if (this._queue.size > 0 && this.store.getState().App.strings) {
+          this._queue.forEach(target => this.reply(target));
+          this._queue.clear();
+        }
         break;
     }
   }
 };
 
 this.EXPORTED_SYMBOLS = ["NewTabInit"];
--- a/browser/extensions/activity-stream/lib/PrefsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PrefsFeed.jsm
@@ -2,38 +2,62 @@
  * 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;
 
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
+const {PrerenderData} = Cu.import("resource://activity-stream/common/PrerenderData.jsm", {});
 
 this.PrefsFeed = class PrefsFeed {
   constructor(prefMap) {
     this._prefMap = prefMap;
     this._prefs = new Prefs();
   }
+
+  // If the any prefs are set to something other than what the prerendered version
+  // of AS expects, we can't use it.
+  _setPrerenderPref() {
+    for (const prefName of PrerenderData.invalidatingPrefs) {
+      if (this._prefs.get(prefName) !== PrerenderData.initialPrefs[prefName]) {
+        this._prefs.set("prerender", false);
+        return;
+      }
+    }
+    this._prefs.set("prerender", true);
+  }
+
+  _checkPrerender(name) {
+    if (PrerenderData.invalidatingPrefs.includes(name)) {
+      this._setPrerenderPref();
+    }
+  }
+
   onPrefChanged(name, value) {
     if (this._prefMap.has(name)) {
       this.store.dispatch(ac.BroadcastToContent({type: at.PREF_CHANGED, data: {name, value}}));
     }
+    this._checkPrerender(name, value);
   }
+
   init() {
     this._prefs.observeBranch(this);
 
     // Get the initial value of each activity stream pref
     const values = {};
     for (const name of this._prefMap.keys()) {
       values[name] = this._prefs.get(name);
     }
 
     // Set the initial state of all prefs in redux
     this.store.dispatch(ac.BroadcastToContent({type: at.PREFS_INITIAL_VALUES, data: values}));
+
+    this._setPrerenderPref();
   }
   removeListeners() {
     this._prefs.ignoreBranch(this);
   }
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
--- a/browser/extensions/activity-stream/lib/SectionsManager.jsm
+++ b/browser/extensions/activity-stream/lib/SectionsManager.jsm
@@ -43,17 +43,17 @@ const BUILT_IN_SECTIONS = {
       titleString: {id: "settings_pane_highlights_header"},
       descString: {id: "settings_pane_highlights_body2"}
     },
     shouldHidePref:  false,
     eventSource: "HIGHLIGHTS",
     icon: "highlights",
     title: {id: "header_highlights"},
     maxRows: 3,
-    availableContextMenuOptions: ["CheckBookmark", "SaveToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
+    availableContextMenuOptions: ["CheckBookmark", "SaveToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
     emptyState: {
       message: {id: "highlights_empty_state"},
       icon: "highlights"
     },
     order: 1
   })
 };
 
--- a/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
@@ -116,17 +116,17 @@ this.SnippetsFeed = class SnippetsFeed {
   }
 
   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);
     Services.obs.removeObserver(this, SEARCH_ENGINE_OBSERVER_TOPIC);
-    this.store.dispatch({type: at.SNIPPETS_RESET});
+    this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_RESET}));
   }
 
   showFirefoxAccounts(browser) {
     // We want to replace the current tab.
     browser.loadURI("about:accounts?action=signup&entrypoint=snippets");
   }
 
   onAction(action) {
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -16,16 +16,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Screenshots",
   "resource://activity-stream/lib/Screenshots.jsm");
 
 const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
 const DEFAULT_SITES_PREF = "default.sites";
 const DEFAULT_TOP_SITES = [];
 const FRECENCY_THRESHOLD = 100; // 1 visit (skip first-run/one-time pages)
+const MIN_FAVICON_SIZE = 96;
 
 this.TopSitesFeed = class TopSitesFeed {
   constructor() {
     this.lastUpdated = 0;
     this._tippyTopProvider = new TippyTopProvider();
     this.dedupe = new Dedupe(this._dedupeKey);
   }
   _dedupeKey(site) {
@@ -90,23 +91,23 @@ this.TopSitesFeed = class TopSitesFeed {
     // 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 tippy top icon or screenshot for every item
+    // Now, get a tippy top icon, a rich icon, or screenshot for every item
     for (let link of links) {
       if (!link) { continue; }
 
-      // Check for tippy top icon.
+      // Check for tippy top icon or a rich icon.
       link = this._tippyTopProvider.processSite(link);
-      if (link.tippyTopIcon) { continue; }
+      if (link.tippyTopIcon || link.faviconSize >= MIN_FAVICON_SIZE) { continue; }
 
       // If no tippy top, then we get a screenshot.
       if (currentScreenshots[link.url]) {
         link.screenshot = currentScreenshots[link.url];
       } else {
         this.getScreenshot(link.url);
       }
     }
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/activity-stream-prerender.test.jsx
@@ -0,0 +1,70 @@
+const prerender = require("content-src/activity-stream-prerender");
+const {prerenderStore} = prerender;
+const {PrerenderData} = require("common/PrerenderData.jsm");
+
+describe("prerenderStore", () => {
+  it("should create a store", () => {
+    const store = prerenderStore();
+
+    assert.isFunction(store.getState);
+  });
+  it("should start uninitialized", () => {
+    const store = prerenderStore();
+
+    const state = store.getState();
+    assert.equal(state.App.initialized, false);
+  });
+  it("should set the right locale, strings, and text direction", () => {
+    const strings = {foo: "foo"};
+
+    const store = prerenderStore("en-FOO", strings);
+
+    const state = store.getState();
+    assert.equal(state.App.locale, "en-FOO");
+    assert.equal(state.App.strings, strings);
+    assert.equal(state.App.textDirection, "ltr");
+  });
+  it("should add the right initial prefs", () => {
+    const store = prerenderStore();
+
+    const state = store.getState();
+    assert.equal(state.Prefs.values, PrerenderData.initialPrefs);
+  });
+  it("should add TopStories as the first section", () => {
+    const store = prerenderStore();
+
+    const state = store.getState();
+    // TopStories
+    const firstSection = state.Sections[0];
+    assert.equal(firstSection.id, "topstories");
+    // it should start uninitialized
+    assert.equal(firstSection.initialized, false);
+  });
+});
+
+describe("prerender", () => {
+  it("should set the locale and get the right strings of whatever is passed in", () => {
+    const {store} = prerender("en-US");
+
+    const state = store.getState();
+    assert.equal(state.App.locale, "en-US");
+    assert.equal(state.App.strings.newtab_page_title, "New Tab");
+  });
+  it("should throw if an unknown locale is passed in", () => {
+    assert.throws(() => prerender("en-FOO"));
+  });
+  it("should set the locale to en-PRERENDER and have empty strings if no locale is passed in", () => {
+    const {store} = prerender();
+
+    const state = store.getState();
+    assert.equal(state.App.locale, "en-PRERENDER");
+    assert.equal(state.App.strings.newtab_page_title, " ");
+  });
+  // # TODO: Remove when #3370 is resolved.
+  it("should render a real English string for search_web_placeholder", () => {
+    const {store} = prerender();
+
+    const state = store.getState();
+    assert.equal(state.App.strings.search_web_placeholder, "Search the Web");
+  });
+});
--- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -21,22 +21,30 @@ describe("Reducers", () => {
 
       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", () => {
+    it("should set locale, strings and text direction on LOCALE_UPDATE", () => {
       const strings = {};
       const action = {type: "LOCALE_UPDATED", data: {locale: "zh-CN", strings}};
       const nextState = App(undefined, action);
       assert.propertyVal(nextState, "locale", "zh-CN");
       assert.propertyVal(nextState, "strings", strings);
+      assert.propertyVal(nextState, "textDirection", "ltr");
+    });
+    it("should set rtl text direction for RTL locales", () => {
+      const action = {type: "LOCALE_UPDATED", data: {locale: "ar"}};
+
+      const nextState = App(undefined, action);
+
+      assert.propertyVal(nextState, "textDirection", "rtl");
     });
   });
   describe("TopSites", () => {
     it("should return the initial state", () => {
       const nextState = TopSites(undefined, {type: "FOO"});
       assert.equal(nextState, INITIAL_STATE.TopSites);
     });
     it("should add top sites on TOP_SITES_UPDATED", () => {
--- a/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
@@ -25,17 +25,17 @@ describe("Top Sites Feed", () => {
     fakeNewTabUtils = {activityStreamLinks: {getHighlights: sandbox.spy(() => Promise.resolve(links))}};
     sectionsManagerStub = {
       onceInitialized: sinon.stub().callsFake(callback => callback()),
       enableSection: sinon.spy(),
       disableSection: sinon.spy(),
       updateSection: sinon.spy(),
       sections: new Map([["highlights", {}]])
     };
-    shortURLStub = sinon.stub().callsFake(site => site.url);
+    shortURLStub = sinon.stub().callsFake(site => site.url.match(/\/([^/]+)/)[1]);
     globals.set("NewTabUtils", fakeNewTabUtils);
     ({HighlightsFeed, HIGHLIGHTS_UPDATE_TIME, SECTION_ID} = injector({
       "lib/ShortURL.jsm": {shortURL: shortURLStub},
       "lib/SectionsManager.jsm": {SectionsManager: sectionsManagerStub},
       "common/Dedupe.jsm": {Dedupe}
     }));
     feed = new HighlightsFeed();
     feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill(null).map((v, i) => ({url: `http://www.topsite${i}.com`}))}}; }};
@@ -67,30 +67,54 @@ describe("Top Sites Feed", () => {
 
       assert.notCalled(feed.fetchHighlights);
     });
   });
   describe("#fetchHighlights", () => {
     it("should add hostname and image to each link", async () => {
       links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
       await feed.fetchHighlights();
-      assert.equal(feed.highlights[0].hostname, links[0].url);
+      assert.equal(feed.highlights[0].hostname, "mozilla.org");
       assert.equal(feed.highlights[0].image, links[0].preview_image_url);
     });
     it("should not include any links already in Top Sites", async () => {
       links = [
         {url: "https://mozilla.org"},
         {url: "http://www.topsite0.com"},
         {url: "http://www.topsite1.com"},
         {url: "http://www.topsite2.com"}
       ];
       await feed.fetchHighlights();
       assert.equal(feed.highlights.length, 1);
       assert.deepEqual(feed.highlights[0], links[0]);
     });
+    it("should not include history of same hostname as a bookmark", async () => {
+      links = [
+        {url: "https://site.com/bookmark", type: "bookmark"},
+        {url: "https://site.com/history", type: "history"}
+      ];
+
+      await feed.fetchHighlights();
+
+      assert.equal(feed.highlights.length, 1);
+      assert.deepEqual(feed.highlights[0], links[0]);
+    });
+    it("should take the first history of a hostname", async () => {
+      links = [
+        {url: "https://site.com/first", type: "history"},
+        {url: "https://site.com/second", type: "history"},
+        {url: "https://other", type: "history"}
+      ];
+
+      await feed.fetchHighlights();
+
+      assert.equal(feed.highlights.length, 2);
+      assert.deepEqual(feed.highlights[0], links[0]);
+      assert.deepEqual(feed.highlights[1], links[2]);
+    });
     it("should set type to bookmark if there is a bookmarkGuid", async () => {
       links = [{url: "https://mozilla.org", type: "history", bookmarkGuid: "1234567890"}];
       await feed.fetchHighlights();
       assert.equal(feed.highlights[0].type, "bookmark");
     });
   });
   describe("#uninit", () => {
     it("should disable its section", () => {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/NewTabInit.test.js
@@ -0,0 +1,62 @@
+const {NewTabInit} = require("lib/NewTabInit.jsm");
+const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
+
+describe("NewTabInit", () => {
+  let instance;
+  let store;
+  let STATE;
+  beforeEach(() => {
+    STATE = {};
+    store = {getState: sinon.stub().returns(STATE), dispatch: sinon.stub()};
+    instance = new NewTabInit();
+    instance.store = store;
+  });
+  it("should reply with a copy of the state immediately if localization is ready", () => {
+    STATE.App = {strings: {}};
+
+    instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, 123));
+
+    const resp = ac.SendToContent({type: at.NEW_TAB_INITIAL_STATE, data: STATE}, 123);
+    assert.calledWith(store.dispatch, resp);
+  });
+  it("should not reply immediately if localization is not ready", () => {
+    STATE.App = {strings: null};
+
+    instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, 123));
+
+    assert.notCalled(store.dispatch);
+  });
+  it("should dispatch responses for queued targets when LOCALE_UPDATED is received", () => {
+    STATE.App = {strings: null};
+
+    // Send requests before strings are ready
+    instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "foo"));
+    instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "bar"));
+    instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "baz"));
+    assert.notCalled(store.dispatch);
+
+    // Update strings
+    STATE.App = {strings: {}};
+    instance.onAction({type: at.LOCALE_UPDATED});
+
+    assert.calledThrice(store.dispatch);
+    const action = {type: at.NEW_TAB_INITIAL_STATE, data: STATE};
+    assert.calledWith(store.dispatch, ac.SendToContent(action, "foo"));
+    assert.calledWith(store.dispatch, ac.SendToContent(action, "bar"));
+    assert.calledWith(store.dispatch, ac.SendToContent(action, "baz"));
+  });
+  it("should clear targets from the queue once they have been sent", () => {
+    STATE.App = {strings: null};
+    instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "foo"));
+    instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "bar"));
+    instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "baz"));
+
+    STATE.App = {strings: {}};
+    instance.onAction({type: at.LOCALE_UPDATED});
+    assert.calledThrice(store.dispatch);
+
+    store.dispatch.reset();
+    instance.onAction({type: at.LOCALE_UPDATED});
+    assert.notCalled(store.dispatch);
+  });
+});
--- a/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js
@@ -1,21 +1,25 @@
 const {PrefsFeed} = require("lib/PrefsFeed.jsm");
 const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
+const {PrerenderData} = require("common/PrerenderData.jsm");
+const {initialPrefs} = PrerenderData;
 
-const FAKE_PREFS = new Map([["foo", {value: 1}], ["bar", {value: 2}]]);
+const PRERENDER_PREF_NAME = "prerender";
 
 describe("PrefsFeed", () => {
   let feed;
+  let FAKE_PREFS;
   beforeEach(() => {
+    FAKE_PREFS = new Map([["foo", 1], ["bar", 2]]);
     feed = new PrefsFeed(FAKE_PREFS);
     feed.store = {dispatch: sinon.spy()};
     feed._prefs = {
-      get: sinon.spy(item => FAKE_PREFS.get(item).value),
-      set: sinon.spy(),
+      get: sinon.spy(item => FAKE_PREFS.get(item)),
+      set: sinon.spy((name, value) => FAKE_PREFS.set(name, value)),
       observe: sinon.spy(),
       observeBranch: sinon.spy(),
       ignore: sinon.spy(),
       ignoreBranch: sinon.spy()
     };
   });
   it("should set a pref when a SET_PREF action is received", () => {
     feed.onAction(ac.SetPref("foo", 2));
@@ -36,9 +40,47 @@ describe("PrefsFeed", () => {
     feed.onAction({type: at.UNINIT});
     assert.calledOnce(feed._prefs.ignoreBranch);
     assert.calledWith(feed._prefs.ignoreBranch, feed);
   });
   it("should send a PREF_CHANGED action when onPrefChanged is called", () => {
     feed.onPrefChanged("foo", 2);
     assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({type: at.PREF_CHANGED, data: {name: "foo", value: 2}}));
   });
+  describe("INIT prerendering", () => {
+    it("should set a prerender pref on init", () => {
+      feed.onAction({type: at.INIT});
+      assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME);
+    });
+    it("should set prerender pref to true if prefs match initial values", () => {
+      Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
+      feed.onAction({type: at.INIT});
+      assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, true);
+    });
+    it("should set prerender pref to false if a pref does not match its initial value", () => {
+      Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
+      FAKE_PREFS.set("feeds.section.topstories", false);
+      feed.onAction({type: at.INIT});
+      assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, false);
+    });
+  });
+  describe("onPrefChanged prerendering", () => {
+    it("should not change the prerender pref if the pref is not included in invalidatingPrefs", () => {
+      feed.onPrefChanged("foo123", true);
+      assert.notCalled(feed._prefs.set);
+    });
+    it("should set the prerender pref to false if a pref in invalidatingPrefs is changed from its original value", () => {
+      Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
+
+      feed._prefs.set("feeds.section.topstories", false);
+      feed.onPrefChanged("feeds.section.topstories", false);
+      assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, false);
+    });
+    it("should set the prerender pref back to true if the invalidatingPrefs are changed back to their original values", () => {
+      Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
+      FAKE_PREFS.set("feeds.section.topstories", false);
+
+      feed._prefs.set("feeds.section.topstories", true);
+      feed.onPrefChanged("feeds.section.topstories", true);
+      assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, true);
+    });
+  });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
@@ -1,10 +1,10 @@
 const {SnippetsFeed} = require("lib/SnippetsFeed.jsm");
-const {actionTypes: at} = require("common/Actions.jsm");
+const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
 const {GlobalOverrider} = require("test/unit/utils");
 
 const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
 const searchData = {searchEngineIdentifier: "google", engines: ["searchEngine-google", "searchEngine-bing"]};
 
 let overrider = new GlobalOverrider();
 
 describe("SnippetsFeed", () => {
@@ -73,23 +73,23 @@ describe("SnippetsFeed", () => {
   it("should call .uninit on an UNINIT action", () => {
     const feed = new SnippetsFeed();
     sandbox.stub(feed, "uninit");
     feed.store = {dispatch: sandbox.stub()};
 
     feed.onAction({type: at.UNINIT});
     assert.calledOnce(feed.uninit);
   });
-  it("should dispatch a SNIPPETS_RESET on uninit", () => {
+  it("should broadcast a SNIPPETS_RESET on uninit", () => {
     const feed = new SnippetsFeed();
     feed.store = {dispatch: sandbox.stub()};
 
     feed.uninit();
 
-    assert.calledWith(feed.store.dispatch, {type: at.SNIPPETS_RESET});
+    assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({type: at.SNIPPETS_RESET}));
   });
   it("should dispatch an update event when the Search observer is called", async () => {
     const feed = new SnippetsFeed();
     feed.store = {dispatch: sandbox.stub()};
     sandbox.stub(feed, "getSelectedSearchEngine")
       .returns(Promise.resolve(searchData));
 
     await feed.observe(null, "browser-search-engine-modified");
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -290,16 +290,32 @@ describe("Top Sites Feed", () => {
         site.tippyTopIcon = "icon.png";
         site.backgroundColor = "#fff";
         return site;
       };
       await feed.refresh(action);
       assert.calledOnce(feed.store.dispatch);
       assert.notCalled(feed.getScreenshot);
     });
+    it("should skip getting screenshot if there is an icon of size greater than 96x96 and no tippy top", async () => {
+      sandbox.stub(feed, "getScreenshot");
+      feed.getLinksWithDefaults = () => [{
+        url: "foo.com",
+        favicon: "data:foo",
+        faviconSize: 196
+      }];
+      feed._tippyTopProvider.processSite = site => {
+        site.tippyTopIcon = null;
+        site.backgroundColor = null;
+        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);
     });
   });
--- a/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
@@ -1,50 +1,111 @@
 const initStore = require("content-src/lib/init-store");
+const {MERGE_STORE_ACTION, rehydrationMiddleware} = initStore;
 const {GlobalOverrider, addNumberReducer} = require("test/unit/utils");
-const {actionCreators: ac} = require("common/Actions.jsm");
+const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
 
 describe("initStore", () => {
   let globals;
   let store;
   beforeEach(() => {
     globals = new GlobalOverrider();
     globals.set("sendAsyncMessage", globals.sandbox.spy());
     globals.set("addMessageListener", globals.sandbox.spy());
     store = initStore({number: addNumberReducer});
   });
   afterEach(() => globals.restore());
   it("should create a store with the provided reducers", () => {
     assert.ok(store);
     assert.property(store.getState(), "number");
   });
-  it("should add a listener for incoming actions", () => {
+  it("should add a listener that dispatches actions", () => {
     assert.calledWith(global.addMessageListener, initStore.INCOMING_MESSAGE_NAME);
-    const callback = global.addMessageListener.firstCall.args[1];
+    const listener = global.addMessageListener.firstCall.args[1];
     globals.sandbox.spy(store, "dispatch");
     const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
-    callback(message);
+
+    listener(message);
+
     assert.calledWith(store.dispatch, message.data);
   });
+  it("should not throw if addMessageListener is not defined", () => {
+    // Note: this is being set/restored by GlobalOverrider
+    delete global.addMessageListener;
+
+    assert.doesNotThrow(() => initStore({number: addNumberReducer}));
+  });
+  it("should initialize with an initial state if provided as the second argument", () => {
+    store = initStore({number: addNumberReducer}, {number: 42});
+
+    assert.equal(store.getState().number, 42);
+  });
   it("should log errors from failed messages", () => {
     const callback = global.addMessageListener.firstCall.args[1];
     globals.sandbox.stub(global.console, "error");
     globals.sandbox.stub(store, "dispatch").throws(Error("failed"));
 
-    const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
+    const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: MERGE_STORE_ACTION}};
     callback(message);
 
     assert.calledOnce(global.console.error);
   });
   it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => {
     store.dispatch({type: initStore.MERGE_STORE_ACTION, data: {number: 42}});
     assert.deepEqual(store.getState(), {number: 42});
   });
-  it("should send out SendToMain ations", () => {
+  it("should send out SendToMain actions", () => {
     const action = ac.SendToMain({type: "FOO"});
     store.dispatch(action);
     assert.calledWith(global.sendAsyncMessage, initStore.OUTGOING_MESSAGE_NAME, action);
   });
-  it("should not send out other types of ations", () => {
+  it("should not send out other types of actions", () => {
     store.dispatch({type: "FOO"});
     assert.notCalled(global.sendAsyncMessage);
   });
+  describe("rehydrationMiddleware", () => {
+    it("should allow NEW_TAB_STATE_REQUEST to go through", () => {
+      const action = ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST});
+      const next = sinon.spy();
+      rehydrationMiddleware(store)(next)(action);
+      assert.calledWith(next, action);
+    });
+    it("should dispatch an additional NEW_TAB_STATE_REQUEST if INIT was received after a request", () => {
+      const requestAction = ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST});
+      const next = sinon.spy();
+
+      rehydrationMiddleware(store)(next)(requestAction);
+
+      next.reset();
+      rehydrationMiddleware(store)(next)({type: at.INIT});
+      assert.calledWith(next, requestAction);
+    });
+    it("should allow MERGE_STORE_ACTION to go through", () => {
+      const action = {type: MERGE_STORE_ACTION};
+      const next = sinon.spy();
+      rehydrationMiddleware(store)(next)(action);
+      assert.calledWith(next, action);
+    });
+    it("should not allow actions from main to go through before MERGE_STORE_ACTION was received", () => {
+      const next = sinon.spy();
+
+      rehydrationMiddleware(store)(next)(ac.BroadcastToContent({type: "FOO"}));
+      rehydrationMiddleware(store)(next)(ac.SendToContent({type: "FOO"}, 123));
+
+      assert.notCalled(next);
+    });
+    it("should allow all local actions to go through", () => {
+      const action = {type: "FOO"};
+      const next = sinon.spy();
+      rehydrationMiddleware(store)(next)(action);
+      assert.calledWith(next, action);
+    });
+    it("should allow actions from main to go through after MERGE_STORE_ACTION has been received", () => {
+      const next = sinon.spy();
+      rehydrationMiddleware(store)(next)({type: MERGE_STORE_ACTION});
+      next.reset();
+
+      const action = ac.SendToContent({type: "FOO"}, 123);
+      rehydrationMiddleware(store)(next)(action);
+      assert.calledWith(next, action);
+    });
+  });
 });