Bug 1559536 - Add disabled checkboxes, reenabled stories and bug fixes to Activity Stream r=r1cky
authorEd Lee <edilee@mozilla.com>
Tue, 18 Jun 2019 18:33:22 +0000
changeset 479117 0dff9f803849e1e95d097fb2a41167f56c2d22c7
parent 479116 de209d87f58fc054e51e8661896d9ad1335a040c
child 479118 93ccef16281453771066ba9196e285a8ceb32dbf
push id36169
push usercbrindusan@mozilla.com
push dateTue, 18 Jun 2019 21:46:19 +0000
treeherdermozilla-central@0dff9f803849 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersr1cky
bugs1559536
milestone69.0a1
first release with
nightly linux32
0dff9f803849 / 69.0a1 / 20190618214619 / files
nightly linux64
0dff9f803849 / 69.0a1 / 20190618214619 / files
nightly mac
0dff9f803849 / 69.0a1 / 20190618214619 / files
nightly win32
0dff9f803849 / 69.0a1 / 20190618214619 / files
nightly win64
0dff9f803849 / 69.0a1 / 20190618214619 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1559536 - Add disabled checkboxes, reenabled stories and bug fixes to Activity Stream r=r1cky Differential Revision: https://phabricator.services.mozilla.com/D35109
browser/components/newtab/.eslintrc.js
browser/components/newtab/.prettierrc
browser/components/newtab/bin/strings-import.js
browser/components/newtab/content-src/asrouter/asrouter-content.jsx
browser/components/newtab/content-src/asrouter/components/RichText/RichText.jsx
browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json
browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx
browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json
browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx
browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss
browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
browser/components/newtab/content-src/components/Sections/Sections.jsx
browser/components/newtab/content-src/lib/link-menu-options.js
browser/components/newtab/content-src/styles/_activity-stream.scss
browser/components/newtab/css/activity-stream-linux.css
browser/components/newtab/css/activity-stream-mac.css
browser/components/newtab/css/activity-stream-windows.css
browser/components/newtab/data/content/activity-stream.bundle.js
browser/components/newtab/karma.mc.config.js
browser/components/newtab/lib/AboutPreferences.jsm
browser/components/newtab/lib/ActivityStream.jsm
browser/components/newtab/lib/CFRPageActions.jsm
browser/components/newtab/lib/DiscoveryStreamFeed.jsm
browser/components/newtab/lib/PlacesFeed.jsm
browser/components/newtab/lib/RecipeExecutor.jsm
browser/components/newtab/lib/TopStoriesFeed.jsm
browser/components/newtab/locales-src/kab/strings.properties
browser/components/newtab/locales-src/ro/strings.properties
browser/components/newtab/package-lock.json
browser/components/newtab/package.json
browser/components/newtab/prerendered/locales/kab/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ro/activity-stream-strings.js
browser/components/newtab/test/browser/browser_activity_stream_strings.js
browser/components/newtab/test/browser/browser_highlights_section.js
browser/components/newtab/test/browser/head.js
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Hero.test.jsx
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/List.test.jsx
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopSites.test.jsx
browser/components/newtab/test/unit/lib/AboutPreferences.test.js
browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
browser/components/newtab/test/unit/lib/TopStoriesFeed.test.js
browser/components/newtab/test/unit/unit-entry.js
browser/components/newtab/yamscripts.yml
--- a/browser/components/newtab/.eslintrc.js
+++ b/browser/components/newtab/.eslintrc.js
@@ -10,16 +10,17 @@ module.exports = {
   },
   "env": {
     "node": true
   },
   "plugins": [
     "import", // require("eslint-plugin-import")
     "react", // require("eslint-plugin-react")
     "jsx-a11y", // require("eslint-plugin-jsx-a11y")
+    "prettier", // require("eslint-plugin-prettier")
 
     // Temporarily disabled since they aren't vendored into in mozilla central yet
     // "react-hooks", // require("react-hooks")
     "fetch-options", // require("eslint-plugin-fetch-options")
   ],
   "settings": {
     "react": {
       "version": "16.2.0"
@@ -27,17 +28,20 @@ module.exports = {
   },
   "extends": [
 
     "eslint:recommended",
     "plugin:jsx-a11y/recommended", // require("eslint-plugin-jsx-a11y")
     "plugin:mozilla/recommended", // require("eslint-plugin-mozilla")
     "plugin:mozilla/browser-test",
     "plugin:mozilla/mochitest-test",
-    "plugin:mozilla/xpcshell-test"
+    "plugin:mozilla/xpcshell-test",
+    "prettier",
+    "prettier/react",
+    "plugin:prettier/recommended",
   ],
   "globals": {
     // Remove this when m-c updates their eslint: See https://github.com/mozilla/activity-stream/pull/4219
     "RPMSendAsyncMessage": true,
     "NewTabPagePreloading": true,
   },
   "overrides": [
     {
@@ -66,125 +70,101 @@ module.exports = {
     }
   ],
   "rules": {
     // "react-hooks/rules-of-hooks": 2,
 
     "fetch-options/no-fetch-credentials": 2,
 
     "react/jsx-boolean-value": [2, "always"],
-    "react/jsx-closing-bracket-location": [2, "after-props"],
-    "react/jsx-curly-spacing": [2, "never"],
-    "react/jsx-equals-spacing": [2, "never"],
     "react/jsx-key": 2,
     "react/jsx-no-bind": 2,
     "react/jsx-no-comment-textnodes": 2,
     "react/jsx-no-duplicate-props": 2,
     "react/jsx-no-target-blank": 2,
     "react/jsx-no-undef": 2,
     "react/jsx-pascal-case": 2,
-    "react/jsx-tag-spacing": 2,
     "react/jsx-uses-react": 2,
     "react/jsx-uses-vars": 2,
-    "react/jsx-wrap-multilines": 2,
     "react/no-access-state-in-setstate": 2,
     "react/no-danger": 2,
     "react/no-deprecated": 2,
     "react/no-did-mount-set-state": 2,
     "react/no-did-update-set-state": 2,
     "react/no-direct-mutation-state": 2,
     "react/no-is-mounted": 2,
     "react/no-unknown-property": 2,
     "react/require-render-return": 2,
-    "react/self-closing-comp": 2,
 
     "accessor-pairs": [2, {"setWithoutGet": true, "getWithoutSet": false}],
-    "array-bracket-newline": [2, "consistent"],
-    "array-bracket-spacing": [2, "never"],
     "array-callback-return": 2,
     "array-element-newline": 0,
-    "arrow-body-style": [2, "as-needed"],
-    "arrow-parens": [2, "as-needed"],
     "block-scoped-var": 2,
     "callback-return": 0,
     "camelcase": 0,
     "capitalized-comments": 0,
     "class-methods-use-this": 0,
     "consistent-this": [2, "use-bind"],
     "curly": [2, "all"],
     "default-case": 0,
-    "dot-location": [2, "property"],
     "eqeqeq": 2,
     "for-direction": 2,
     "func-name-matching": 2,
     "func-names": 0,
     "func-style": 0,
     "function-paren-newline": 0,
     "getter-return": 2,
     "global-require": 0,
     "guard-for-in": 2,
     "handle-callback-err": 2,
     "id-blacklist": 0,
     "id-length": 0,
     "id-match": 0,
     "implicit-arrow-linebreak": 0,
     // XXX Switch back to indent once mozilla-central has decided what it is using.
     "indent": 0,
-    "indent-legacy": ["error", 2, {"SwitchCase": 1}],
     "init-declarations": 0,
-    "jsx-quotes": [2, "prefer-double"],
     "line-comment-position": 0,
-    "lines-around-comment": ["error", {
-      "allowClassStart": true,
-      "allowObjectStart": true,
-      "beforeBlockComment": true
-    }],
     "lines-between-class-members": 2,
     "max-depth": [2, 4],
     "max-len": 0,
     "max-lines": 0,
     "max-nested-callbacks": [2, 4],
     "max-params": [2, 6],
     "max-statements": [2, 50],
     "max-statements-per-line": [2, {"max": 2}],
     "multiline-comment-style": 0,
     "multiline-ternary": 0,
     "new-cap": [2, {"newIsCap": true, "capIsNew": false}],
-    "new-parens": 2,
     "newline-after-var": 0,
     "newline-before-return": 0,
-    "newline-per-chained-call": [2, {"ignoreChainWithDepth": 3}],
     "no-alert": 2,
     "no-await-in-loop": 0,
     "no-bitwise": 0,
     "no-buffer-constructor": 2,
     "no-catch-shadow": 2,
-    "no-confusing-arrow": [2, {"allowParens": true}],
     "no-console": 1,
     "no-continue": 0,
     "no-div-regex": 2,
     "no-duplicate-imports": 2,
     "no-empty-function": 0,
     "no-eq-null": 2,
     "no-extend-native": 2,
     "no-extra-label": 2,
     "no-extra-parens": 0,
-    "no-floating-decimal": 2,
     "no-implicit-coercion": [2, {"allow": ["!!"]}],
     "no-implicit-globals": 2,
     "no-inline-comments": 0,
     "no-invalid-this": 0,
     "no-label-var": 2,
     "no-loop-func": 2,
     "no-magic-numbers": 0,
-    "no-mixed-operators": [2, {"allowSamePrecedence": true, "groups": [["&", "|", "^", "~", "<<", ">>", ">>>"], ["==", "!=", "===", "!==", ">", ">=", "<", "<="], ["&&", "||"], ["in", "instanceof"]]}],
     "no-mixed-requires": 2,
     "no-multi-assign": 2,
     "no-multi-str": 2,
-    "no-multiple-empty-lines": [2, {"max": 1, "maxBOF": 0, "maxEOF": 0}],
     "no-negated-condition": 0,
     "no-negated-in-lhs": 2,
     "no-new": 2,
     "no-new-func": 2,
     "no-new-require": 2,
     "no-octal-escape": 2,
     "no-param-reassign": 2,
     "no-path-concat": 2,
@@ -196,66 +176,47 @@ module.exports = {
     "no-restricted-globals": 0,
     "no-restricted-imports": 0,
     "no-restricted-modules": 0,
     "no-restricted-properties": 0,
     "no-restricted-syntax": 0,
     "no-return-assign": [2, "except-parens"],
     "no-script-url": 2,
     "no-shadow": 2,
-    "no-spaced-func": 2,
     "no-sync": 0,
     "no-template-curly-in-string": 2,
     "no-ternary": 0,
     "no-undef-init": 2,
     "no-undefined": 0,
     "no-underscore-dangle": 0,
     "no-unmodified-loop-condition": 2,
     "no-unused-expressions": 2,
     "no-use-before-define": 2,
     "no-useless-computed-key": 2,
     "no-useless-constructor": 2,
     "no-useless-rename": 2,
     "no-var": 2,
     "no-void": 2,
     "no-warning-comments": 0, // TODO: Change to `1`?
-    "nonblock-statement-body-position": 2,
-    "object-curly-newline": [2, {"multiline": true, "consistent": true}],
-    "object-curly-spacing": [2, "never"],
-    "object-property-newline": [2, {"allowMultiplePropertiesPerLine": true}],
     "one-var": [2, "never"],
-    "one-var-declaration-per-line": [2, "initializations"],
     "operator-assignment": [2, "always"],
-    "operator-linebreak": [2, "after"],
     "padding-line-between-statements": 0,
-    "prefer-arrow-callback": ["error", {"allowNamedFunctions": true}],
     "prefer-const": 0, // TODO: Change to `1`?
     "prefer-destructuring": [2, {"AssignmentExpression": {"array": true}, "VariableDeclarator": {"array": true, "object": true}}],
     "prefer-numeric-literals": 2,
     "prefer-promise-reject-errors": 2,
     "prefer-reflect": 0,
     "prefer-rest-params": 2,
     "prefer-spread": 2,
     "prefer-template": 2,
-    "quote-props": [2, "consistent"],
     "radix": [2, "always"],
     "require-await": 2,
     "require-jsdoc": 0,
-    "semi-spacing": [2, {"before": false, "after": true}],
-    "semi-style": 2,
-    "sort-imports": [2, {"ignoreCase": true}],
     "sort-keys": 0,
     "sort-vars": 2,
-    "space-in-parens": [2, "never"],
     "strict": 0,
-    "switch-colon-spacing": 2,
     "symbol-description": 2,
-    "template-curly-spacing": [2, "never"],
-    "template-tag-spacing": 2,
-    "unicode-bom": [2, "never"],
     "valid-jsdoc": [0, {"requireReturn": false, "requireParamDescription": false, "requireReturnDescription": false}],
     "vars-on-top": 2,
-    "wrap-iife": [2, "inside"],
     "wrap-regex": 0,
-    "yield-star-spacing": [2, "after"],
     "yoda": [2, "never"]
   }
 };
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/.prettierrc
@@ -0,0 +1,5 @@
+{
+  "printWidth": 80,
+  "tabWidth": 2,
+  "trailingComma": "es5"
+}
--- a/browser/components/newtab/bin/strings-import.js
+++ b/browser/components/newtab/bin/strings-import.js
@@ -34,17 +34,18 @@ async function getLocales() {
   return locales;
 }
 
 // Pick a different string if the desired one is missing
 const transvision = {};
 async function cherryPickString(locale) {
   const getTransvision = async string => {
     if (!transvision[string]) {
-      const response = await fetch(`https://transvision.mozfr.org/api/v1/entity/gecko_strings/?id=${string}`); // eslint-disable-line fetch-options/no-fetch-credentials
+      // eslint-disable-next-line fetch-options/no-fetch-credentials
+      const response = await fetch(`https://transvision.mozfr.org/api/v1/entity/gecko_strings/?id=${string}`);
       transvision[string] = response.ok ? await response.json() : {};
     }
     return transvision[string];
   };
   const expectedKey = "section_menu_action_add_search_engine";
   const expected = await getTransvision(`browser/chrome/browser/activity-stream/newtab.properties:${expectedKey}`);
   const target = await getTransvision("browser/chrome/browser/search.properties:searchAddFoundEngine2");
   return !expected[locale] && target[locale] ? `${expectedKey}=${target[locale]}\n` : "";
--- a/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
+++ b/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
@@ -293,17 +293,17 @@ export class ASRouterUISurface extends R
             onReady={this.triggerOnboarding}
             onBlock={this.onDismissById(message.id)}
             dispatch={this.props.dispatch} />
         </IntlProvider>
       );
     } else if (message.template === "return_to_amo_overlay") {
       global.document.body.classList.add("amo");
       return (
-        <LocalizationProvider messages={generateBundles({"amo_html": message.content.text})}>
+        <LocalizationProvider bundles={generateBundles({"amo_html": message.content.text})}>
           <ReturnToAMO
             {...message}
             UISurface="NEWTAB_OVERLAY"
             onReady={this.triggerOnboarding}
             onBlock={this.onDismissById(message.id)}
             onAction={ASRouterUtils.executeAction}
             sendUserActionTelemetry={this.sendUserActionTelemetry} />
         </LocalizationProvider>
--- a/browser/components/newtab/content-src/asrouter/components/RichText/RichText.jsx
+++ b/browser/components/newtab/content-src/asrouter/components/RichText/RichText.jsx
@@ -19,18 +19,21 @@ const ALLOWED_TAGS = {
  */
 export function convertLinks(links, sendClick, doNotAutoBlock, openNewWindow = false) {
   if (links) {
     return Object.keys(links).reduce((acc, linkTag) => {
       const {action} = links[linkTag];
       // Setting the value to false will not include the attribute in the anchor
       const url = action ? false : safeURI(links[linkTag].url);
 
-      acc[linkTag] = (<a href={url} // eslint-disable-line jsx-a11y/anchor-has-content
-        // eslint was getting a false positive caused by the dynamic injection of content.
+      acc[linkTag] = (
+        // eslint was getting a false positive caused by the dynamic injection
+        // of content.
+        // eslint-disable-next-line jsx-a11y/anchor-has-content
+        <a href={url}
         target={openNewWindow ? "_blank" : ""}
         data-metric={links[linkTag].metric}
         data-action={action}
         data-args={links[linkTag].args}
         data-do_not_autoblock={doNotAutoBlock}
         onClick={sendClick} />);
       return acc;
     }, {});
--- a/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json
@@ -75,16 +75,21 @@
     "icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
     "icon_dark_theme": {
       "type": "string",
       "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
     },
+    "icon_alt_text": {
+      "type": "string",
+      "description": "Alt text for accessibility",
+      "default": ""
+    },
     "title": {
       "allOf": [
         {"$ref": "#/definitions/plainText"},
         {"description": "Snippet title displayed before snippet text"}
       ]
     },
     "title_icon": {
       "type": "string",
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
@@ -1,15 +1,15 @@
 import React from "react";
 import {RichText} from "../../components/RichText/RichText";
 import {safeURI} from "../../template-utils";
 import {SnippetBase} from "../../components/SnippetBase/SnippetBase";
 
 const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
-// Alt text if available; in the future this should come from the server. See bug 1551711
+// Alt text placeholder in case the prop from the server isn't available
 const ICON_ALT_TEXT = "";
 
 export class SimpleBelowSearchSnippet extends React.PureComponent {
   renderText() {
     const {props} = this;
     return (<RichText text={props.content.text}
       customElements={this.props.customElements}
       localization_id="text"
@@ -21,17 +21,17 @@ export class SimpleBelowSearchSnippet ex
     const {props} = this;
     let className = "SimpleBelowSearchSnippet";
 
     if (props.className) {
       className += ` ${props.className}`;
     }
 
     return (<SnippetBase {...props} className={className} textStyle={this.props.textStyle}>
-      <img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-light-theme" alt={ICON_ALT_TEXT} />
-      <img src={safeURI(props.content.icon_dark_theme || props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-dark-theme" alt={ICON_ALT_TEXT} />
+      <img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-light-theme" alt={props.content.icon_alt_text || ICON_ALT_TEXT} />
+      <img src={safeURI(props.content.icon_dark_theme || props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-dark-theme" alt={props.content.icon_alt_text || ICON_ALT_TEXT} />
       <div>
         <p className="body">{this.renderText()}</p>
         {this.props.extraContent}
       </div>
     </SnippetBase>);
   }
 }
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
@@ -24,16 +24,21 @@
     "icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
     "icon_dark_theme": {
       "type": "string",
       "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
     },
+    "icon_alt_text": {
+      "type": "string",
+      "description": "Alt text describing icon for screen readers",
+      "default": ""
+    },
     "block_button_text": {
       "type": "string",
       "description": "Tooltip text used for dismiss button.",
       "default": "Remove this"
     },
     "do_not_autoblock": {
       "type": "boolean",
       "description": "Used to prevent blocking the snippet after the CTA link has been clicked"
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
@@ -1,17 +1,17 @@
 import {Button} from "../../components/Button/Button";
 import {ConditionalWrapper} from "../../components/ConditionalWrapper/ConditionalWrapper";
 import React from "react";
 import {RichText} from "../../components/RichText/RichText";
 import {safeURI} from "../../template-utils";
 import {SnippetBase} from "../../components/SnippetBase/SnippetBase";
 
 const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
-// Alt text if available; in the future this should come from the server. See bug 1551711
+// Alt text placeholder in case the prop from the server isn't available
 const ICON_ALT_TEXT = "";
 
 export class SimpleSnippet extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onButtonClick = this.onButtonClick.bind(this);
   }
 
@@ -126,18 +126,18 @@ export class SimpleSnippet extends React
     }
     if (sectionHeader) {
       className += " has-section-header";
     }
 
     return (<SnippetBase {...props} className={className} textStyle={this.props.textStyle}>
       {sectionHeader}
       <ConditionalWrapper condition={sectionHeader} wrap={this.wrapSnippetContent}>
-        <img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-light-theme" alt={ICON_ALT_TEXT} />
-        <img src={safeURI(props.content.icon_dark_theme || props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-dark-theme" alt={ICON_ALT_TEXT} />
+        <img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-light-theme" alt={props.content.icon_alt_text || ICON_ALT_TEXT} />
+        <img src={safeURI(props.content.icon_dark_theme || props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-dark-theme" alt={props.content.icon_alt_text || ICON_ALT_TEXT} />
         <div>
           {this.renderTitle()} <p className="body">{this.renderText()}</p>
           {this.props.extraContent}
         </div>
         {<div>{this.renderButton()}</div>}
       </ConditionalWrapper>
     </SnippetBase>);
   }
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
@@ -34,24 +34,34 @@
     "icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
     "icon_dark_theme": {
       "type": "string",
       "description": "Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."
     },
+    "icon_alt_text": {
+      "type": "string",
+      "description": "Alt text describing icon for screen readers",
+      "default": ""
+    },
     "title_icon": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
     },
     "title_icon_dark_theme": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
     },
+    "title_icon_alt_text": {
+      "type": "string",
+      "description": "Alt text describing title icon for screen readers",
+      "default": ""
+    },
     "button_action": {
       "type": "string",
       "description": "The type of action the button should trigger."
     },
     "button_url": {
       "allOf": [
         {"$ref": "#/definitions/link_url"},
         {"description": "A url, button_label links to this"}
--- a/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx
@@ -1,16 +1,16 @@
 import {Button} from "../../components/Button/Button";
 import React from "react";
 import {RichText} from "../../components/RichText/RichText";
 import {safeURI} from "../../template-utils";
 import {SimpleSnippet} from "../SimpleSnippet/SimpleSnippet";
 import {SnippetBase} from "../../components/SnippetBase/SnippetBase";
 
-// Alt text if available; in the future this should come from the server. See bug 1551711
+// Alt text placeholder in case the prop from the server isn't available
 const ICON_ALT_TEXT = "";
 
 export class SubmitFormSnippet extends React.PureComponent {
   constructor(props) {
     super(props);
     this.expandSnippet = this.expandSnippet.bind(this);
     this.handleSubmit = this.handleSubmit.bind(this);
     this.handleSubmitAttempt = this.handleSubmitAttempt.bind(this);
@@ -61,17 +61,18 @@ export class SubmitFormSnippet extends R
 
     if (json && json.status === "ok") {
       this.setState({signupSuccess: true, signupSubmitted: true});
       if (!this.props.content.do_not_autoblock) {
         this.props.onBlock({preventDismiss: true});
       }
       this.props.sendUserActionTelemetry({event: "CLICK_BUTTON", value: "subscribe-success", id: "NEWTAB_FOOTER_BAR_CONTENT"});
     } else {
-      console.error("There was a problem submitting the form", json || "[No JSON response]"); // eslint-disable-line no-console
+      // eslint-disable-next-line no-console
+      console.error("There was a problem submitting the form", json || "[No JSON response]");
       this.setState({signupSuccess: false, signupSubmitted: true});
       this.props.sendUserActionTelemetry({event: "CLICK_BUTTON", value: "subscribe-error", id: "NEWTAB_FOOTER_BAR_CONTENT"});
     }
 
     this.setState({disableForm: false});
   }
 
   expandSnippet() {
@@ -163,18 +164,18 @@ export class SubmitFormSnippet extends R
   }
 
   renderSignupView() {
     const {content} = this.props;
     const containerClass = `SubmitFormSnippet ${this.props.className}`;
     return (<SnippetBase {...this.props} className={containerClass} footerDismiss={true}>
         {content.scene2_icon ?
           <div className="scene2Icon">
-            <img src={safeURI(content.scene2_icon)} className="icon-light-theme" alt={ICON_ALT_TEXT} />
-            <img src={safeURI(content.scene2_icon_dark_theme || content.scene2_icon)} className="icon-dark-theme" alt={ICON_ALT_TEXT} />
+            <img src={safeURI(content.scene2_icon)} className="icon-light-theme" alt={content.scene2_icon_alt_text || ICON_ALT_TEXT} />
+            <img src={safeURI(content.scene2_icon_dark_theme || content.scene2_icon)} className="icon-dark-theme" alt={content.scene2_icon_alt_text || ICON_ALT_TEXT} />
           </div> : null}
         <div className="message">
           <p>
             {content.scene2_title && <h3 className="scene2Title">{content.scene2_title}</h3>}
             {" "}
             {content.scene2_text && <RichText scene2_text={content.scene2_text} localization_id="scene2_text" />}
           </p>
         </div>
--- a/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json
@@ -54,24 +54,34 @@
     "scene1_icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
     "scene1_icon_dark_theme": {
       "type": "string",
       "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
     },
+    "scene1_icon_alt_text": {
+      "type": "string",
+      "description": "Alt text describing scene1 icon for screen readers",
+      "default": ""
+    },
     "scene1_title_icon": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
     },
     "scene1_title_icon_dark_theme": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
     },
+    "scene1_title_icon_alt_text": {
+      "type": "string",
+      "description": "Alt text describing scene1 title icon for screen readers",
+      "default": ""
+    },
     "form_action": {
       "type": "string",
       "description": "Endpoint to submit form data."
     },
     "success_title": {
       "type": "string",
       "description": "(send to device) Title shown before text on successful registration."
     },
@@ -110,16 +120,21 @@
     "scene2_icon": {
       "type": "string",
       "description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."
     },
     "scene2_icon_dark_theme": {
       "type": "string",
       "description": "(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred."
     },
+    "scene2_icon_alt_text": {
+      "type": "string",
+      "description": "Alt text describing scene2 icon for screen readers",
+      "default": ""
+    },
     "scene2_newsletter": {
       "type": "string",
       "description": "Newsletter/basket id user is subscribing to. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/. Default 'mozilla-foundation'."
     },
     "hidden_inputs": {
       "type": "object",
       "description": "Each entry represents a hidden input, key is used as value for the name property."
     },
--- a/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
+++ b/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
@@ -467,18 +467,21 @@ export class ASRouterAdminInner extends 
       {messagesToShow.map(msg => this.renderMessageItem(msg))}
     </tbody></table>);
   }
 
   renderMessageFilter() {
     if (!this.state.providers) {
       return null;
     }
-    // eslint-disable-next-line jsx-a11y/no-onchange
-    return (<p>Show messages from <select value={this.state.messageFilter} onChange={this.onChangeMessageFilter}>
+    return (<p>
+      {/* eslint-disable-next-line prettier/prettier */}
+      Show messages from{" "}
+      {/* eslint-disable-next-line jsx-a11y/no-onchange */}
+      <select value={this.state.messageFilter} onChange={this.onChangeMessageFilter}>
       <option value="all">all providers</option>
       {this.state.providers.map(provider => (<option key={provider.id} value={provider.id}>{provider.id}</option>))}
     </select></p>);
   }
 
   renderTableHead() {
     return (<thead>
       <tr className="message-item">
--- a/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx
+++ b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx
@@ -33,18 +33,19 @@ export class ContextMenu extends React.P
     // Eat all clicks on the context menu so they don't bubble up to window.
     // This prevents the context menu from closing when clicking disabled items
     // or the separators.
     event.stopPropagation();
   }
 
   render() {
     // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper.
-    // eslint-disable-next-line jsx-a11y/interactive-supports-focus
-    return (<span role="menu" className="context-menu" onClick={this.onClick} onKeyDown={this.onClick} >
+    return (
+      // eslint-disable-next-line jsx-a11y/interactive-supports-focus
+      <span role="menu" className="context-menu" onClick={this.onClick} onKeyDown={this.onClick} >
       <ul className="context-menu-list">
         {this.props.options.map((option, i) => (option.type === "separator" ?
           (<li key={i} className="separator" />) :
           (option.type !== "empty" && <ContextMenuItem key={i} option={option} hideContext={this.hideContext} tabIndex="0" />)
         ))}
       </ul>
     </span>);
   }
@@ -91,16 +92,19 @@ export class ContextMenuItem extends Rea
       case "ArrowDown":
         event.preventDefault();
         this.focusSibling(event.target, event.key);
         break;
       case "Enter":
         this.props.hideContext();
         option.onClick();
         break;
+      case "Escape":
+        this.props.hideContext();
+        break;
     }
   }
 
   render() {
     const {option} = this.props;
     return (
       <li role="menuitem" className="context-menu-item" >
         <button className={option.disabled ? "disabled" : ""} tabIndex="0" onClick={this.onClick} onKeyDown={this.onKeyDown}>
--- a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
@@ -1,14 +1,15 @@
 import {actionCreators as ac} from "common/Actions.jsm";
 import {CardGrid} from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid";
 import {CollapsibleSection} from "content-src/components/CollapsibleSection/CollapsibleSection";
 import {connect} from "react-redux";
 import {DSMessage} from "content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage";
 import {Hero} from "content-src/components/DiscoveryStreamComponents/Hero/Hero";
+import {Highlights} from "content-src/components/DiscoveryStreamComponents/Highlights/Highlights";
 import {HorizontalRule} from "content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule";
 import {List} from "content-src/components/DiscoveryStreamComponents/List/List";
 import {Navigation} from "content-src/components/DiscoveryStreamComponents/Navigation/Navigation";
 import React from "react";
 import {SectionTitle} from "content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle";
 import {selectLayoutRender} from "content-src/lib/selectLayoutRender";
 import {TopSites} from "content-src/components/DiscoveryStreamComponents/TopSites/TopSites";
 
@@ -84,16 +85,18 @@ export class _DiscoveryStreamBase extend
           }
         });
       });
     });
   }
 
   renderComponent(component, embedWidth) {
     switch (component.type) {
+      case "Highlights":
+        return (<Highlights />);
       case "TopSites":
         return (<TopSites header={component.header} />);
       case "Message":
         return (
           <DSMessage
             title={component.header && component.header.title}
             subtitle={component.header && component.header.subtitle}
             link_text={component.header && component.header.link_text}
@@ -240,16 +243,20 @@ export class _DiscoveryStreamBase extend
               id: message.header.link_text,
             },
           }}
           privacyNoticeURL={topStories.privacyNoticeURL}
           showPrefName={topStories.pref.feed}
           title={message.header.title}>
           {this.renderLayout(layoutRender)}
         </CollapsibleSection>}
+        {this.renderLayout([{
+          width: 12,
+          components: [{type: "Highlights"}],
+        }])}
       </React.Fragment>
     );
   }
 
   renderLayout(layoutRender) {
     const styles = [];
     return (
       <div className="discovery-stream ds-layout">
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
@@ -21,17 +21,17 @@ export class DSCard extends React.PureCo
       }));
 
       this.props.dispatch(ac.ImpressionStats({
         source: this.props.type.toUpperCase(),
         click: 0,
         tiles: [{
           id: this.props.id,
           pos: this.props.pos,
-          ...(this.props.shim ? {shim: this.props.shim} : {}),
+          ...(this.props.shim && this.props.shim.click ? {shim: this.props.shim.click} : {}),
         }],
       }));
     }
   }
 
   render() {
     return (
       <div className={`ds-card${this.props.placeholder ? " placeholder" : ""}`}>
@@ -53,29 +53,30 @@ export class DSCard extends React.PureCo
               <p className="context">{this.props.context}</p>
             )}
           </div>
           <ImpressionStats
             campaignId={this.props.campaignId}
             rows={[{
               id: this.props.id,
               pos: this.props.pos,
-              ...(this.props.shim ? {shim: this.props.shim} : {}),
+              ...(this.props.shim && this.props.shim.impression ? {shim: this.props.shim.impression} : {}),
             }]}
             dispatch={this.props.dispatch}
             source={this.props.type} />
         </SafeAnchor>
         {!this.props.placeholder && <DSLinkMenu
           id={this.props.id}
           index={this.props.pos}
           dispatch={this.props.dispatch}
           intl={this.props.intl}
           url={this.props.url}
           title={this.props.title}
           source={this.props.source}
           type={this.props.type}
           pocket_id={this.props.pocket_id}
+          shim={this.props.shim}
           bookmarkGuid={this.props.bookmarkGuid} />}
       </div>
     );
   }
 }
 export const PlaceholderDSCard = props => <DSCard placeholder={true} />;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
@@ -4,16 +4,17 @@ import React from "react";
 
 export class _DSLinkMenu extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
       activeCard: null,
       showContextMenu: false,
     };
+    this.windowObj = this.props.windowObj || window; // Added to support unit tests
     this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
     this.onMenuUpdate = this.onMenuUpdate.bind(this);
     this.onMenuShow = this.onMenuShow.bind(this);
     this.contextMenuButtonRef = React.createRef();
   }
 
   onMenuButtonClick(event) {
     event.preventDefault();
@@ -28,17 +29,17 @@ export class _DSLinkMenu extends React.P
       const dsLinkMenuHostDiv = this.contextMenuButtonRef.current.parentElement;
       dsLinkMenuHostDiv.parentElement.classList.remove("active", "last-item");
     }
     this.setState({showContextMenu});
   }
 
   onMenuShow() {
     const dsLinkMenuHostDiv = this.contextMenuButtonRef.current.parentElement;
-    if (window.scrollMaxX > 0) {
+    if (this.windowObj.scrollMaxX > 0) {
       dsLinkMenuHostDiv.parentElement.classList.add("last-item");
     }
     dsLinkMenuHostDiv.parentElement.classList.add("active");
   }
 
   render() {
     const {index, dispatch} = this.props;
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
@@ -67,16 +68,17 @@ export class _DSLinkMenu extends React.P
           shouldSendImpressionStats={true}
           site={{
             referrer: "https://getpocket.com/recommendations",
             title: this.props.title,
             type: this.props.type,
             url: this.props.url,
             guid: this.props.id,
             pocket_id: this.props.pocket_id,
+            shim: this.props.shim,
             bookmarkGuid: this.props.bookmarkGuid,
           }} />
       }
     </div>);
   }
 }
 
 export const DSLinkMenu = injectIntl(_DSLinkMenu);
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
@@ -23,17 +23,17 @@ export class Hero extends React.PureComp
       }));
 
       this.props.dispatch(ac.ImpressionStats({
         source: this.props.type.toUpperCase(),
         click: 0,
         tiles: [{
           id: this.heroRec.id,
           pos: this.heroRec.pos,
-          ...(this.heroRec.shim ? {shim: this.heroRec.shim} : {}),
+          ...(this.heroRec.shim && this.heroRec.shim.click ? {shim: this.heroRec.shim.click} : {}),
         }],
       }));
     }
   }
 
   renderHero() {
     let [heroRec, ...otherRecs] = this.props.data.recommendations.slice(0, this.props.items);
     this.heroRec = heroRec;
@@ -91,31 +91,32 @@ export class Hero extends React.PureComp
                 <p className="excerpt clamp">{heroRec.excerpt}</p>
               </div>
             </div>
             <ImpressionStats
               campaignId={heroRec.campaignId}
               rows={[{
                 id: heroRec.id,
                 pos: heroRec.pos,
-                ...(heroRec.shim ? {shim: heroRec.shim} : {}),
+                ...(heroRec.shim && heroRec.shim.impression ? {shim: heroRec.shim.impression} : {}),
               }]}
               dispatch={this.props.dispatch}
               source={this.props.type} />
           </SafeAnchor>
           <DSLinkMenu
             id={heroRec.id}
             index={heroRec.pos}
             dispatch={this.props.dispatch}
             intl={this.props.intl}
             url={heroRec.url}
             title={heroRec.title}
             source={heroRec.domain}
             type={this.props.type}
             pocket_id={heroRec.pocket_id}
+            shim={heroRec.shim}
             bookmarkGuid={heroRec.bookmarkGuid} />
         </div>
       );
     }
 
     let list = (
       <List
         recStartingPoint={1}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx
@@ -0,0 +1,20 @@
+import {connect} from "react-redux";
+import React from "react";
+import {SectionIntl} from "content-src/components/Sections/Sections";
+
+export class _Highlights extends React.PureComponent {
+  render() {
+    const section = this.props.Sections.find(s => s.id === "highlights");
+    if (!section || !section.enabled) {
+      return null;
+    }
+
+    return (
+      <div className="ds-highlights sections-list">
+        <SectionIntl {...section} isFixed={true} />
+      </div>
+    );
+  }
+}
+
+export const Highlights = connect(state => ({Sections: state.Sections}))(_Highlights);
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss
@@ -0,0 +1,34 @@
+.ds-highlights {
+  .section {
+    margin: 0 (-$section-horizontal-padding);
+
+    .section-list {
+      grid-gap: var(--gridRowGap);
+      grid-template-columns: repeat(4, 1fr);
+
+      .card-outer {
+        $line-height: 20px;
+        height: 175px;
+
+        .card-host-name {
+          font-size: 13px;
+          line-height: $line-height;
+          margin-bottom: 2px;
+          padding-bottom: 0;
+          text-transform: unset; // sass-lint:disable-line no-disallowed-properties
+        }
+
+        .card-title {
+          font-size: 14px;
+          font-weight: 600;
+          line-height: $line-height;
+          max-height: $line-height;
+        }
+      }
+    }
+  }
+
+  .hide-for-narrow {
+    display: block;
+  }
+}
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
@@ -27,17 +27,17 @@ export class ListItem extends React.Pure
       }));
 
       this.props.dispatch(ac.ImpressionStats({
         source: this.props.type.toUpperCase(),
         click: 0,
         tiles: [{
           id: this.props.id,
           pos: this.props.pos,
-          ...(this.props.shim ? {shim: this.props.shim} : {}),
+          ...(this.props.shim && this.props.shim.click ? {shim: this.props.shim.click} : {}),
         }],
       }));
     }
   }
 
   render() {
     return (
       <li className={`ds-list-item${this.props.placeholder ? " placeholder" : ""}`} >
@@ -62,31 +62,32 @@ export class ListItem extends React.Pure
             </p>
           </div>
           <DSImage extraClassNames="ds-list-image" source={this.props.image_src} rawSource={this.props.raw_image_src} />
           <ImpressionStats
             campaignId={this.props.campaignId}
             rows={[{
               id: this.props.id,
               pos: this.props.pos,
-              ...(this.props.shim ? {shim: this.props.shim} : {}),
+              ...(this.props.shim && this.props.shim.impression ? {shim: this.props.shim.impression} : {}),
             }]}
             dispatch={this.props.dispatch}
             source={this.props.type} />
         </SafeAnchor>
         {!this.props.placeholder && <DSLinkMenu
           id={this.props.id}
           index={this.props.pos}
           dispatch={this.props.dispatch}
           intl={this.props.intl}
           url={this.props.url}
           title={this.props.title}
           source={this.props.source}
           type={this.props.type}
           pocket_id={this.props.pocket_id}
+          shim={this.props.shim}
           bookmarkGuid={this.props.bookmarkGuid} />}
       </li>
     );
   }
 }
 
 export const PlaceholderListItem = props => <ListItem placeholder={true} />;
 
--- a/browser/components/newtab/content-src/components/Sections/Sections.jsx
+++ b/browser/components/newtab/content-src/components/Sections/Sections.jsx
@@ -221,16 +221,17 @@ export class Section extends React.PureC
       <CollapsibleSection className={sectionClassName} icon={icon}
         title={title}
         id={id}
         eventSource={eventSource}
         collapsed={this.props.pref.collapsed}
         showPrefName={(pref && pref.feed) || id}
         privacyNoticeURL={privacyNoticeURL}
         Prefs={this.props.Prefs}
+        isFixed={this.props.isFixed}
         isFirst={isFirst}
         isLast={isLast}
         learnMore={learnMore}
         dispatch={this.props.dispatch}
         isWebExtension={this.props.isWebExtension}>
 
         {!shouldShowEmptyState && (<ul className="section-list" style={{padding: 0}}>
           {cards}
--- a/browser/components/newtab/content-src/lib/link-menu-options.js
+++ b/browser/components/newtab/content-src/lib/link-menu-options.js
@@ -67,17 +67,21 @@ export const LinkMenuOptions = {
     icon: "dismiss",
     action: ac.AlsoToMain({
       type: at.BLOCK_URL,
       data: {url: site.open_url || site.url, pocket_id: site.pocket_id},
     }),
     impression: ac.ImpressionStats({
       source: eventSource,
       block: 0,
-      tiles: [{id: site.guid, pos: index}],
+      tiles: [{
+        id: site.guid,
+        pos: index,
+        ...(site.shim && site.shim.delete ? {shim: site.shim.delete} : {}),
+      }],
     }),
     userEvent: "BLOCK",
   }),
 
   // This is an option for web extentions which will result in remove items from
   // memory and notify the web extenion, rather than using the built-in block list.
   WebExtDismiss: (site, index, eventSource) => ({
     id: "menu_action_webext_dismiss",
@@ -178,17 +182,21 @@ export const LinkMenuOptions = {
     icon: "pocket-save",
     action: ac.AlsoToMain({
       type: at.SAVE_TO_POCKET,
       data: {site: {url: site.url, title: site.title}},
     }),
     impression: ac.ImpressionStats({
       source: eventSource,
       pocket: 0,
-      tiles: [{id: site.guid, pos: index}],
+      tiles: [{
+        id: site.guid,
+        pos: index,
+        ...(site.shim && site.shim.save ? {shim: site.shim.save} : {}),
+      }],
     }),
     userEvent: "SAVE_TO_POCKET",
   }),
   DeleteFromPocket: site => ({
     id: "menu_action_delete_pocket",
     icon: "pocket-delete",
     action: ac.AlsoToMain({
       type: at.DELETE_FROM_POCKET,
--- a/browser/components/newtab/content-src/styles/_activity-stream.scss
+++ b/browser/components/newtab/content-src/styles/_activity-stream.scss
@@ -156,16 +156,17 @@ input {
 @import '../components/ASRouterAdmin/ASRouterAdmin';
 @import '../components/PocketLoggedInCta/PocketLoggedInCta';
 @import '../components/MoreRecommendations/MoreRecommendations';
 @import '../components/DiscoveryStreamBase/DiscoveryStreamBase';
 
 // Discovery Stream Components
 @import '../components/DiscoveryStreamComponents/CardGrid/CardGrid';
 @import '../components/DiscoveryStreamComponents/Hero/Hero';
+@import '../components/DiscoveryStreamComponents/Highlights/Highlights';
 @import '../components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule';
 @import '../components/DiscoveryStreamComponents/List/List';
 @import '../components/DiscoveryStreamComponents/Navigation/Navigation';
 @import '../components/DiscoveryStreamComponents/SectionTitle/SectionTitle';
 @import '../components/DiscoveryStreamComponents/TopSites/TopSites';
 @import '../components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu';
 @import '../components/DiscoveryStreamComponents/DSCard/DSCard';
 @import '../components/DiscoveryStreamComponents/DSImage/DSImage';
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -2218,16 +2218,38 @@ main {
         [lwt-newtab-brighttext] .ds-column-9 .ds-hero .cards .ds-card .title, [lwt-newtab-brighttext]
         .ds-column-10 .ds-hero .cards .ds-card .title, [lwt-newtab-brighttext]
         .ds-column-11 .ds-hero .cards .ds-card .title, [lwt-newtab-brighttext]
         .ds-column-12 .ds-hero .cards .ds-card .title {
           color: #FFF; }
   .ds-hero.empty {
     grid-template-columns: auto; }
 
+.ds-highlights .section {
+  margin: 0 -25px; }
+  .ds-highlights .section .section-list {
+    grid-gap: var(--gridRowGap);
+    grid-template-columns: repeat(4, 1fr); }
+    .ds-highlights .section .section-list .card-outer {
+      height: 175px; }
+      .ds-highlights .section .section-list .card-outer .card-host-name {
+        font-size: 13px;
+        line-height: 20px;
+        margin-bottom: 2px;
+        padding-bottom: 0;
+        text-transform: unset; }
+      .ds-highlights .section .section-list .card-outer .card-title {
+        font-size: 14px;
+        font-weight: 600;
+        line-height: 20px;
+        max-height: 20px; }
+
+.ds-highlights .hide-for-narrow {
+  display: block; }
+
 .ds-hr {
   border: 0;
   border-top: 1px solid #D7D7DB;
   height: 0; }
   [lwt-newtab-brighttext] .ds-hr {
     border-top: 1px solid #4A4A4F; }
 
 .ds-list {
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -2221,16 +2221,38 @@ main {
         [lwt-newtab-brighttext] .ds-column-9 .ds-hero .cards .ds-card .title, [lwt-newtab-brighttext]
         .ds-column-10 .ds-hero .cards .ds-card .title, [lwt-newtab-brighttext]
         .ds-column-11 .ds-hero .cards .ds-card .title, [lwt-newtab-brighttext]
         .ds-column-12 .ds-hero .cards .ds-card .title {
           color: #FFF; }
   .ds-hero.empty {
     grid-template-columns: auto; }
 
+.ds-highlights .section {
+  margin: 0 -25px; }
+  .ds-highlights .section .section-list {
+    grid-gap: var(--gridRowGap);
+    grid-template-columns: repeat(4, 1fr); }
+    .ds-highlights .section .section-list .card-outer {
+      height: 175px; }
+      .ds-highlights .section .section-list .card-outer .card-host-name {
+        font-size: 13px;
+        line-height: 20px;
+        margin-bottom: 2px;
+        padding-bottom: 0;
+        text-transform: unset; }
+      .ds-highlights .section .section-list .card-outer .card-title {
+        font-size: 14px;
+        font-weight: 600;
+        line-height: 20px;
+        max-height: 20px; }
+
+.ds-highlights .hide-for-narrow {
+  display: block; }
+
 .ds-hr {
   border: 0;
   border-top: 1px solid #D7D7DB;
   height: 0; }
   [lwt-newtab-brighttext] .ds-hr {
     border-top: 1px solid #4A4A4F; }
 
 .ds-list {
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -2218,16 +2218,38 @@ main {
         [lwt-newtab-brighttext] .ds-column-9 .ds-hero .cards .ds-card .title, [lwt-newtab-brighttext]
         .ds-column-10 .ds-hero .cards .ds-card .title, [lwt-newtab-brighttext]
         .ds-column-11 .ds-hero .cards .ds-card .title, [lwt-newtab-brighttext]
         .ds-column-12 .ds-hero .cards .ds-card .title {
           color: #FFF; }
   .ds-hero.empty {
     grid-template-columns: auto; }
 
+.ds-highlights .section {
+  margin: 0 -25px; }
+  .ds-highlights .section .section-list {
+    grid-gap: var(--gridRowGap);
+    grid-template-columns: repeat(4, 1fr); }
+    .ds-highlights .section .section-list .card-outer {
+      height: 175px; }
+      .ds-highlights .section .section-list .card-outer .card-host-name {
+        font-size: 13px;
+        line-height: 20px;
+        margin-bottom: 2px;
+        padding-bottom: 0;
+        text-transform: unset; }
+      .ds-highlights .section .section-list .card-outer .card-title {
+        font-size: 14px;
+        font-weight: 600;
+        line-height: 20px;
+        max-height: 20px; }
+
+.ds-highlights .hide-for-narrow {
+  display: block; }
+
 .ds-hr {
   border: 0;
   border-top: 1px solid #D7D7DB;
   height: 0; }
   [lwt-newtab-brighttext] .ds-hr {
     border-top: 1px solid #4A4A4F; }
 
 .ds-list {
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -562,18 +562,18 @@ var actionUtils = {
 /* harmony import */ var _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6);
 /* harmony import */ var content_src_components_ConfirmDialog_ConfirmDialog__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(28);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_5__);
 /* harmony import */ var content_src_components_DiscoveryStreamBase_DiscoveryStreamBase__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(51);
 /* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(34);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(10);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_8__);
-/* harmony import */ var content_src_components_Search_Search__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(45);
-/* harmony import */ var content_src_components_Sections_Sections__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(46);
+/* harmony import */ var content_src_components_Search_Search__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(49);
+/* harmony import */ var content_src_components_Sections_Sections__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(38);
 function _extends() { _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; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
 
@@ -1312,20 +1312,19 @@ class ASRouterAdminInner extends react__
 
     const messagesToShow = this.state.messageFilter === "all" ? this.state.messages : this.state.messages.filter(message => message.provider === this.state.messageFilter);
     return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("table", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tbody", null, messagesToShow.map(msg => this.renderMessageItem(msg))));
   }
 
   renderMessageFilter() {
     if (!this.state.providers) {
       return null;
-    } // eslint-disable-next-line jsx-a11y/no-onchange
-
-
-    return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", null, "Show messages from ", react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("select", {
+    }
+
+    return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", null, "Show messages from", " ", react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("select", {
       value: this.state.messageFilter,
       onChange: this.onChangeMessageFilter
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("option", {
       value: "all"
     }, "all providers"), this.state.providers.map(provider => react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("option", {
       key: provider.id,
       value: provider.id
     }, provider.id))));
@@ -2164,17 +2163,17 @@ class ASRouterUISurface extends react__W
       }, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(_templates_StartupOverlay_StartupOverlay__WEBPACK_IMPORTED_MODULE_12__["StartupOverlay"], {
         onReady: this.triggerOnboarding,
         onBlock: this.onDismissById(message.id),
         dispatch: this.props.dispatch
       }));
     } else if (message.template === "return_to_amo_overlay") {
       global.document.body.classList.add("amo");
       return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(fluent_react__WEBPACK_IMPORTED_MODULE_5__["LocalizationProvider"], {
-        messages: Object(_rich_text_strings__WEBPACK_IMPORTED_MODULE_3__["generateBundles"])({
+        bundles: Object(_rich_text_strings__WEBPACK_IMPORTED_MODULE_3__["generateBundles"])({
           "amo_html": message.content.text
         })
       }, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(_templates_ReturnToAMO_ReturnToAMO__WEBPACK_IMPORTED_MODULE_10__["ReturnToAMO"], _extends({}, message, {
         UISurface: "NEWTAB_OVERLAY",
         onReady: this.triggerOnboarding,
         onBlock: this.onDismissById(message.id),
         onAction: ASRouterUtils.executeAction,
         sendUserActionTelemetry: this.sendUserActionTelemetry
@@ -2838,20 +2837,21 @@ const ALLOWED_TAGS = {
 function convertLinks(links, sendClick, doNotAutoBlock, openNewWindow = false) {
   if (links) {
     return Object.keys(links).reduce((acc, linkTag) => {
       const {
         action
       } = links[linkTag]; // Setting the value to false will not include the attribute in the anchor
 
       const url = action ? false : Object(_template_utils__WEBPACK_IMPORTED_MODULE_3__["safeURI"])(links[linkTag].url);
-      acc[linkTag] = react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
-        href: url // eslint-disable-line jsx-a11y/anchor-has-content
-        // eslint was getting a false positive caused by the dynamic injection of content.
-        ,
+      acc[linkTag] = // eslint was getting a false positive caused by the dynamic injection
+      // of content.
+      // eslint-disable-next-line jsx-a11y/anchor-has-content
+      react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
+        href: url,
         target: openNewWindow ? "_blank" : "",
         "data-metric": links[linkTag].metric,
         "data-action": action,
         "data-args": links[linkTag].args,
         "data-do_not_autoblock": doNotAutoBlock,
         onClick: sendClick
       });
       return acc;
@@ -2897,23 +2897,23 @@ function safeURI(url) {
 
   return isAllowed ? url : "";
 }
 
 /***/ }),
 /* 19 */
 /***/ (function(module) {
 
-module.exports = {"title":"EOYSnippet","description":"Fundraising Snippet","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"donation_form_url":{"type":"string","description":"Url to the donation form."},"currency_code":{"type":"string","description":"The code for the currency. Examle gbp, cad, usd.","default":"usd"},"locale":{"type":"string","description":"String for the locale code.","default":"en-US"},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"text_color":{"type":"string","description":"Modify the text message color"},"background_color":{"type":"string","description":"Snippet background color."},"highlight_color":{"type":"string","description":"Paragraph em highlight color."},"donation_amount_first":{"type":"number","description":"First button amount."},"donation_amount_second":{"type":"number","description":"Second button amount."},"donation_amount_third":{"type":"number","description":"Third button amount."},"donation_amount_fourth":{"type":"number","description":"Fourth button amount."},"selected_button":{"type":"string","description":"Default donation_amount_second. Donation amount button that's selected by default.","default":"donation_amount_second"},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button."},"monthly_checkbox_label_text":{"type":"string","description":"Label text for monthly checkbox.","default":"Make my donation monthly"},"test":{"type":"string","description":"Different styles for the snippet. Options are bold and takeover."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}}},"additionalProperties":false,"required":["text","donation_form_url","donation_amount_first","donation_amount_second","donation_amount_third","donation_amount_fourth","button_label","currency_code"],"dependencies":{"button_color":["button_label"],"button_background_color":["button_label"]}};
+module.exports = {"title":"EOYSnippet","description":"Fundraising Snippet","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"donation_form_url":{"type":"string","description":"Url to the donation form."},"currency_code":{"type":"string","description":"The code for the currency. Examle gbp, cad, usd.","default":"usd"},"locale":{"type":"string","description":"String for the locale code.","default":"en-US"},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"text_color":{"type":"string","description":"Modify the text message color"},"background_color":{"type":"string","description":"Snippet background color."},"highlight_color":{"type":"string","description":"Paragraph em highlight color."},"donation_amount_first":{"type":"number","description":"First button amount."},"donation_amount_second":{"type":"number","description":"Second button amount."},"donation_amount_third":{"type":"number","description":"Third button amount."},"donation_amount_fourth":{"type":"number","description":"Fourth button amount."},"selected_button":{"type":"string","description":"Default donation_amount_second. Donation amount button that's selected by default.","default":"donation_amount_second"},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"icon_alt_text":{"type":"string","description":"Alt text for accessibility","default":""},"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button."},"monthly_checkbox_label_text":{"type":"string","description":"Label text for monthly checkbox.","default":"Make my donation monthly"},"test":{"type":"string","description":"Different styles for the snippet. Options are bold and takeover."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}}},"additionalProperties":false,"required":["text","donation_form_url","donation_amount_first","donation_amount_second","donation_amount_third","donation_amount_fourth","button_label","currency_code"],"dependencies":{"button_color":["button_label"],"button_background_color":["button_label"]}};
 
 /***/ }),
 /* 20 */
 /***/ (function(module) {
 
-module.exports = {"title":"SimpleSnippet","description":"A simple template with an icon, text, and optional button.","version":"1.1.1","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"button_action":{"type":"string","description":"The type of action the button should trigger."},"button_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, button_label links to this"}]},"button_action_args":{"type":"string","description":"Additional parameters for button action, example which specific menu the button should open"},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button.","default":"Remove this"},"tall":{"type":"boolean","description":"To be used by fundraising only, increases height to roughly 120px. Defaults to false."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}},"section_title_icon":{"type":"string","description":"Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_icon_dark_theme":{"type":"string","description":"Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_text":{"type":"string","description":"Section title text. section_title_icon must also be specified to display."},"section_title_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, section_title_text links to this"}]}},"additionalProperties":false,"required":["text"],"dependencies":{"button_action":["button_label"],"button_url":["button_label"],"button_color":["button_label"],"button_background_color":["button_label"],"section_title_url":["section_title_text"]}};
+module.exports = {"title":"SimpleSnippet","description":"A simple template with an icon, text, and optional button.","version":"1.1.1","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."},"icon_alt_text":{"type":"string","description":"Alt text describing icon for screen readers","default":""},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_alt_text":{"type":"string","description":"Alt text describing title icon for screen readers","default":""},"button_action":{"type":"string","description":"The type of action the button should trigger."},"button_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, button_label links to this"}]},"button_action_args":{"type":"string","description":"Additional parameters for button action, example which specific menu the button should open"},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button.","default":"Remove this"},"tall":{"type":"boolean","description":"To be used by fundraising only, increases height to roughly 120px. Defaults to false."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}},"section_title_icon":{"type":"string","description":"Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_icon_dark_theme":{"type":"string","description":"Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_text":{"type":"string","description":"Section title text. section_title_icon must also be specified to display."},"section_title_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, section_title_text links to this"}]}},"additionalProperties":false,"required":["text"],"dependencies":{"button_action":["button_label"],"button_url":["button_label"],"button_color":["button_label"],"button_background_color":["button_label"],"section_title_url":["section_title_text"]}};
 
 /***/ }),
 /* 21 */
 /***/ (function(module) {
 
 module.exports = {"title":"FXASignupSnippet","description":"A snippet template for FxA sign up/sign in","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"number","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
 
 /***/ }),
@@ -3980,33 +3980,34 @@ class ContextMenu extends react__WEBPACK
     // Eat all clicks on the context menu so they don't bubble up to window.
     // This prevents the context menu from closing when clicking disabled items
     // or the separators.
     event.stopPropagation();
   }
 
   render() {
     // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper.
-    // eslint-disable-next-line jsx-a11y/interactive-supports-focus
-    return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", {
-      role: "menu",
-      className: "context-menu",
-      onClick: this.onClick,
-      onKeyDown: this.onClick
-    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("ul", {
-      className: "context-menu-list"
-    }, this.props.options.map((option, i) => option.type === "separator" ? react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("li", {
-      key: i,
-      className: "separator"
-    }) : option.type !== "empty" && react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(ContextMenuItem, {
-      key: i,
-      option: option,
-      hideContext: this.hideContext,
-      tabIndex: "0"
-    }))));
+    return (// eslint-disable-next-line jsx-a11y/interactive-supports-focus
+      react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", {
+        role: "menu",
+        className: "context-menu",
+        onClick: this.onClick,
+        onKeyDown: this.onClick
+      }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("ul", {
+        className: "context-menu-list"
+      }, this.props.options.map((option, i) => option.type === "separator" ? react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("li", {
+        key: i,
+        className: "separator"
+      }) : option.type !== "empty" && react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(ContextMenuItem, {
+        key: i,
+        option: option,
+        hideContext: this.hideContext,
+        tabIndex: "0"
+      }))))
+    );
   }
 
 }
 class ContextMenuItem extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
@@ -4054,16 +4055,20 @@ class ContextMenuItem extends react__WEB
         event.preventDefault();
         this.focusSibling(event.target, event.key);
         break;
 
       case "Enter":
         this.props.hideContext();
         option.onClick();
         break;
+
+      case "Escape":
+        this.props.hideContext();
+        break;
     }
   }
 
   render() {
     const {
       option
     } = this.props;
     return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("li", {
@@ -4179,17 +4184,20 @@ const LinkMenuOptions = {
         pocket_id: site.pocket_id
       }
     }),
     impression: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ImpressionStats({
       source: eventSource,
       block: 0,
       tiles: [{
         id: site.guid,
-        pos: index
+        pos: index,
+        ...(site.shim && site.shim.delete ? {
+          shim: site.shim.delete
+        } : {})
       }]
     }),
     userEvent: "BLOCK"
   }),
   // This is an option for web extentions which will result in remove items from
   // memory and notify the web extenion, rather than using the built-in block list.
   WebExtDismiss: (site, index, eventSource) => ({
     id: "menu_action_webext_dismiss",
@@ -4326,17 +4334,20 @@ const LinkMenuOptions = {
         }
       }
     }),
     impression: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ImpressionStats({
       source: eventSource,
       pocket: 0,
       tiles: [{
         id: site.guid,
-        pos: index
+        pos: index,
+        ...(site.shim && site.shim.save ? {
+          shim: site.shim.save
+        } : {})
       }]
     }),
     userEvent: "SAVE_TO_POCKET"
   }),
   DeleteFromPocket: site => ({
     id: "menu_action_delete_pocket",
     icon: "pocket-delete",
     action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
@@ -5243,257 +5254,446 @@ const SectionMenuOptions = {
 };
 
 /***/ }),
 /* 38 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
-/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_TopSites", function() { return _TopSites; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSites", function() { return TopSites; });
+/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Section", function() { return Section; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionIntl", function() { return SectionIntl; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Sections", function() { return _Sections; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Sections", function() { return Sections; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(39);
-/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(33);
-/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(40);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(25);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4);
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_5__);
-/* harmony import */ var _asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(14);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(10);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_7__);
-/* harmony import */ var _SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(42);
-/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(55);
-/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(57);
-/* harmony import */ var _TopSite__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(43);
+/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(56);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_2__);
+/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(33);
+/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(40);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_5__);
+/* harmony import */ var content_src_components_MoreRecommendations_MoreRecommendations__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(42);
+/* harmony import */ var content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(43);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(10);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_8__);
+/* harmony import */ var content_src_components_Topics_Topics__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(44);
+/* harmony import */ var content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(45);
 function _extends() { _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; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
 
 
 
 
 
 
-
-
-function topSiteIconType(link) {
-  if (link.customScreenshotURL) {
-    return "custom_screenshot";
-  }
-
-  if (link.tippyTopIcon || link.faviconRef === "tippytop") {
-    return "tippytop";
-  }
-
-  if (link.faviconSize >= _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["MIN_RICH_FAVICON_SIZE"]) {
-    return "rich_icon";
-  }
-
-  if (link.screenshot && link.faviconSize >= _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["MIN_CORNER_FAVICON_SIZE"]) {
-    return "screenshot_with_icon";
-  }
-
-  if (link.screenshot) {
-    return "screenshot";
-  }
-
-  return "no_image";
-}
-/**
- * Iterates through TopSites and counts types of images.
- * @param acc Accumulator for reducer.
- * @param topsite Entry in TopSites.
- */
-
-
-function countTopSitesIconsTypes(topSites) {
-  const countTopSitesTypes = (acc, link) => {
-    acc[topSiteIconType(link)]++;
-    return acc;
-  };
-
-  return topSites.reduce(countTopSitesTypes, {
-    "custom_screenshot": 0,
-    "screenshot_with_icon": 0,
-    "screenshot": 0,
-    "tippytop": 0,
-    "rich_icon": 0,
-    "no_image": 0
-  });
-}
-
-class _TopSites extends react__WEBPACK_IMPORTED_MODULE_7___default.a.PureComponent {
-  constructor(props) {
-    super(props);
-    this.onEditFormClose = this.onEditFormClose.bind(this);
-    this.onSearchShortcutsFormClose = this.onSearchShortcutsFormClose.bind(this);
-  }
-  /**
-   * Dispatch session statistics about the quality of TopSites icons and pinned count.
-   */
-
-
-  _dispatchTopSitesStats() {
-    const topSites = this._getVisibleTopSites();
-
-    const topSitesIconsStats = countTopSitesIconsTypes(topSites);
-    const topSitesPinned = topSites.filter(site => !!site.isPinned).length;
-    const searchShortcuts = topSites.filter(site => !!site.searchTopSite).length; // Dispatch telemetry event with the count of TopSites images types.
-
-    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
-      type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SAVE_SESSION_PERF_DATA,
-      data: {
-        topsites_icon_stats: topSitesIconsStats,
-        topsites_pinned: topSitesPinned,
-        topsites_search_shortcuts: searchShortcuts
-      }
-    }));
-  }
-  /**
-   * Return the TopSites that are visible based on prefs and window width.
-   */
-
-
-  _getVisibleTopSites() {
-    // We hide 2 sites per row when not in the wide layout.
-    let sitesPerRow = common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_9__["TOP_SITES_MAX_SITES_PER_ROW"]; // $break-point-widest = 1072px (from _variables.scss)
-
-    if (!global.matchMedia(`(min-width: 1072px)`).matches) {
-      sitesPerRow -= 2;
-    }
-
-    return this.props.TopSites.rows.slice(0, this.props.TopSitesRows * sitesPerRow);
-  }
-
-  componentDidUpdate() {
-    this._dispatchTopSitesStats();
+const VISIBLE = "visible";
+const VISIBILITY_CHANGE_EVENT = "visibilitychange";
+const CARDS_PER_ROW_DEFAULT = 3;
+const CARDS_PER_ROW_COMPACT_WIDE = 4;
+
+function getFormattedMessage(message) {
+  return typeof message === "string" ? react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("span", null, message) : react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_2__["FormattedMessage"], message);
+}
+
+class Section extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
+  get numRows() {
+    const {
+      rowsPref,
+      maxRows,
+      Prefs
+    } = this.props;
+    return rowsPref ? Prefs.values[rowsPref] : maxRows;
+  }
+
+  _dispatchImpressionStats() {
+    const {
+      props
+    } = this;
+    let cardsPerRow = CARDS_PER_ROW_DEFAULT;
+
+    if (props.compactCards && global.matchMedia(`(min-width: 1072px)`).matches) {
+      // If the section has compact cards and the viewport is wide enough, we show
+      // 4 columns instead of 3.
+      // $break-point-widest = 1072px (from _variables.scss)
+      cardsPerRow = CARDS_PER_ROW_COMPACT_WIDE;
+    }
+
+    const maxCards = cardsPerRow * this.numRows;
+    const cards = props.rows.slice(0, maxCards);
+
+    if (this.needsImpressionStats(cards)) {
+      props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ImpressionStats({
+        source: props.eventSource,
+        tiles: cards.map(link => ({
+          id: link.guid
+        }))
+      }));
+      this.impressionCardGuids = cards.map(link => link.guid);
+    }
+  } // 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;
+
+    if (!props.shouldSendImpressionStats || !props.dispatch) {
+      return;
+    }
+
+    if (props.document.visibilityState === VISIBLE) {
+      this._dispatchImpressionStats();
+    } else {
+      // We should only ever send the latest impression stats ping, so remove any
+      // older listeners.
+      if (this._onVisibilityChange) {
+        props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+      } // When the page becomes visible, send the impression stats ping if the section isn't collapsed.
+
+
+      this._onVisibilityChange = () => {
+        if (props.document.visibilityState === VISIBLE) {
+          if (!this.props.pref.collapsed) {
+            this._dispatchImpressionStats();
+          }
+
+          props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+        }
+      };
+
+      props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+    }
+  }
+
+  componentWillMount() {
+    this.sendNewTabRehydrated(this.props.initialized);
   }
 
   componentDidMount() {
-    this._dispatchTopSitesStats();
-  }
-
-  onEditFormClose() {
-    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
-      source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["TOP_SITES_SOURCE"],
-      event: "TOP_SITES_EDIT_CLOSE"
-    }));
-    this.props.dispatch({
-      type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_CANCEL_EDIT
-    });
-  }
-
-  onSearchShortcutsFormClose() {
-    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
-      source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["TOP_SITES_SOURCE"],
-      event: "SEARCH_EDIT_CLOSE"
-    }));
-    this.props.dispatch({
-      type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL
-    });
-  }
-
-  render() {
+    if (this.props.rows.length && !this.props.pref.collapsed) {
+      this.sendImpressionStatsOrAddListener();
+    }
+  }
+
+  componentDidUpdate(prevProps) {
     const {
       props
     } = this;
-    const {
-      editForm,
-      showSearchShortcutsForm
-    } = props.TopSites;
-    const extraMenuOptions = ["AddTopSite"];
-
-    if (props.Prefs.values["improvesearch.topSiteSearchShortcuts"]) {
-      extraMenuOptions.push("AddSearchShortcut");
-    }
-
-    return react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__["ComponentPerfTimer"], {
-      id: "topsites",
-      initialized: props.TopSites.initialized,
-      dispatch: props.dispatch
-    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__["CollapsibleSection"], {
-      className: "top-sites",
-      icon: "topsites",
-      id: "topsites",
-      title: this.props.title || {
-        id: "header_top_sites"
-      },
-      extraMenuOptions: extraMenuOptions,
-      showPrefName: "feeds.topsites",
-      eventSource: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["TOP_SITES_SOURCE"],
-      collapsed: props.TopSites.pref ? props.TopSites.pref.collapsed : undefined,
-      isFixed: props.isFixed,
-      isFirst: props.isFirst,
-      isLast: props.isLast,
-      dispatch: props.dispatch
-    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_TopSite__WEBPACK_IMPORTED_MODULE_11__["TopSiteList"], {
-      TopSites: props.TopSites,
-      TopSitesRows: props.TopSitesRows,
-      dispatch: props.dispatch,
-      intl: props.intl,
-      topSiteIconType: topSiteIconType
-    }), react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("div", {
-      className: "edit-topsites-wrapper"
-    }, editForm && react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("div", {
-      className: "edit-topsites"
-    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_6__["ModalOverlayWrapper"], {
-      unstyled: true,
-      onClose: this.onEditFormClose,
-      innerClassName: "modal"
-    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_TopSiteForm__WEBPACK_IMPORTED_MODULE_10__["TopSiteForm"], _extends({
-      site: props.TopSites.rows[editForm.index],
-      onClose: this.onEditFormClose,
+    const isCollapsed = props.pref.collapsed;
+    const wasCollapsed = prevProps.pref.collapsed;
+
+    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
+    // and the section is not collapsed...
+    props.rows !== prevProps.rows && !isCollapsed || // or if we are expanding a section that was collapsed.
+    wasCollapsed && !isCollapsed)) {
+      this.sendImpressionStatsOrAddListener();
+    }
+  }
+
+  componentWillUpdate(nextProps) {
+    this.sendNewTabRehydrated(nextProps.initialized);
+  }
+
+  componentWillUnmount() {
+    if (this._onVisibilityChange) {
+      this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+    }
+  }
+
+  needsImpressionStats(cards) {
+    if (!this.impressionCardGuids || this.impressionCardGuids.length !== cards.length) {
+      return true;
+    }
+
+    for (let i = 0; i < cards.length; i++) {
+      if (cards[i].guid !== this.impressionCardGuids[i]) {
+        return true;
+      }
+    }
+
+    return false;
+  } // The NEW_TAB_REHYDRATED event is used to inform feeds that their
+  // data has been consumed e.g. for counting the number of tabs that
+  // have rendered that data.
+
+
+  sendNewTabRehydrated(initialized) {
+    if (initialized && !this.renderNotified) {
+      this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
+        type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].NEW_TAB_REHYDRATED,
+        data: {}
+      }));
+      this.renderNotified = true;
+    }
+  }
+
+  render() {
+    const {
+      id,
+      eventSource,
+      title,
+      icon,
+      rows,
+      Pocket,
+      topics,
+      emptyState,
+      dispatch,
+      compactCards,
+      read_more_endpoint,
+      contextMenuOptions,
+      initialized,
+      learnMore,
+      pref,
+      privacyNoticeURL,
+      isFirst,
+      isLast
+    } = this.props;
+    const waitingForSpoc = id === "topstories" && this.props.Pocket.waitingForSpoc;
+    const maxCardsPerRow = compactCards ? CARDS_PER_ROW_COMPACT_WIDE : CARDS_PER_ROW_DEFAULT;
+    const {
+      numRows
+    } = this;
+    const maxCards = maxCardsPerRow * numRows;
+    const maxCardsOnNarrow = CARDS_PER_ROW_DEFAULT * numRows;
+    const {
+      pocketCta,
+      isUserLoggedIn
+    } = Pocket || {};
+    const {
+      useCta
+    } = pocketCta || {}; // Don't display anything until we have a definitve result from Pocket,
+    // to avoid a flash of logged out state while we render.
+
+    const isPocketLoggedInDefined = isUserLoggedIn === true || isUserLoggedIn === false;
+    const hasTopics = topics && topics.length > 0;
+    const shouldShowPocketCta = id === "topstories" && useCta && isUserLoggedIn === false; // Show topics only for top stories and if it has loaded with topics.
+    // The classs .top-stories-bottom-container ensures content doesn't shift as things load.
+
+    const shouldShowTopics = id === "topstories" && hasTopics && (useCta && isUserLoggedIn === true || !useCta && isPocketLoggedInDefined); // We use topics to determine language support for read more.
+
+    const shouldShowReadMore = read_more_endpoint && hasTopics;
+    const realRows = rows.slice(0, maxCards); // The empty state should only be shown after we have initialized and there is no content.
+    // Otherwise, we should show placeholders.
+
+    const shouldShowEmptyState = initialized && !rows.length;
+    const cards = [];
+
+    if (!shouldShowEmptyState) {
+      for (let i = 0; i < maxCards; i++) {
+        const link = realRows[i]; // On narrow viewports, we only show 3 cards per row. We'll mark the rest as
+        // .hide-for-narrow to hide in CSS via @media query.
+
+        const className = i >= maxCardsOnNarrow ? "hide-for-narrow" : "";
+        let usePlaceholder = !link; // If we are in the third card and waiting for spoc,
+        // use the placeholder.
+
+        if (!usePlaceholder && i === 2 && waitingForSpoc) {
+          usePlaceholder = true;
+        }
+
+        cards.push(!usePlaceholder ? react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__["Card"], {
+          key: i,
+          index: i,
+          className: className,
+          dispatch: dispatch,
+          link: link,
+          contextMenuOptions: contextMenuOptions,
+          eventSource: eventSource,
+          shouldSendImpressionStats: this.props.shouldSendImpressionStats,
+          isWebExtension: this.props.isWebExtension
+        }) : react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__["PlaceholderCard"], {
+          key: i,
+          className: className
+        }));
+      }
+    }
+
+    const sectionClassName = ["section", compactCards ? "compact-cards" : "normal-cards"].join(" "); // <Section> <-- React component
+    // <section> <-- HTML5 element
+
+    return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_4__["ComponentPerfTimer"], this.props, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_3__["CollapsibleSection"], {
+      className: sectionClassName,
+      icon: icon,
+      title: title,
+      id: id,
+      eventSource: eventSource,
+      collapsed: this.props.pref.collapsed,
+      showPrefName: pref && pref.feed || id,
+      privacyNoticeURL: privacyNoticeURL,
+      Prefs: this.props.Prefs,
+      isFixed: this.props.isFixed,
+      isFirst: isFirst,
+      isLast: isLast,
+      learnMore: learnMore,
       dispatch: this.props.dispatch,
-      intl: this.props.intl
-    }, editForm)))), showSearchShortcutsForm && react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("div", {
-      className: "edit-search-shortcuts"
-    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_6__["ModalOverlayWrapper"], {
-      unstyled: true,
-      onClose: this.onSearchShortcutsFormClose,
-      innerClassName: "modal"
-    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_8__["SearchShortcutsForm"], {
-      TopSites: props.TopSites,
-      onClose: this.onSearchShortcutsFormClose,
-      dispatch: this.props.dispatch
-    }))))));
-  }
-
-}
-const TopSites = Object(react_redux__WEBPACK_IMPORTED_MODULE_4__["connect"])(state => ({
-  TopSites: state.TopSites,
+      isWebExtension: this.props.isWebExtension
+    }, !shouldShowEmptyState && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("ul", {
+      className: "section-list",
+      style: {
+        padding: 0
+      }
+    }, cards), shouldShowEmptyState && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
+      className: "section-empty-state"
+    }, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
+      className: "empty-state"
+    }, emptyState.icon && emptyState.icon.startsWith("moz-extension://") ? react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("span", {
+      className: "empty-state-icon icon",
+      style: {
+        "background-image": `url('${emptyState.icon}')`
+      }
+    }) : react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("span", {
+      className: `empty-state-icon icon icon-${emptyState.icon}`
+    }), react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("p", {
+      className: "empty-state-message"
+    }, getFormattedMessage(emptyState.message)))), id === "topstories" && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
+      className: "top-stories-bottom-container"
+    }, shouldShowTopics && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
+      className: "wrapper-topics"
+    }, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Topics_Topics__WEBPACK_IMPORTED_MODULE_9__["Topics"], {
+      topics: this.props.topics
+    })), shouldShowPocketCta && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
+      className: "wrapper-cta"
+    }, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__["PocketLoggedInCta"], null)), react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
+      className: "wrapper-more-recommendations"
+    }, shouldShowReadMore && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_MoreRecommendations_MoreRecommendations__WEBPACK_IMPORTED_MODULE_6__["MoreRecommendations"], {
+      read_more_endpoint: read_more_endpoint
+    })))));
+  }
+
+}
+Section.defaultProps = {
+  document: global.document,
+  rows: [],
+  emptyState: {},
+  pref: {},
+  title: ""
+};
+const SectionIntl = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({
   Prefs: state.Prefs,
-  TopSitesRows: state.Prefs.values.topSitesRows
-}))(Object(react_intl__WEBPACK_IMPORTED_MODULE_5__["injectIntl"])(_TopSites));
+  Pocket: state.Pocket
+}))(Object(react_intl__WEBPACK_IMPORTED_MODULE_2__["injectIntl"])(Section));
+class _Sections extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
+  renderSections() {
+    const sections = [];
+    const enabledSections = this.props.Sections.filter(section => section.enabled);
+    const {
+      sectionOrder,
+      "feeds.topsites": showTopSites
+    } = this.props.Prefs.values; // Enabled sections doesn't include Top Sites, so we add it if enabled.
+
+    const expectedCount = enabledSections.length + ~~showTopSites;
+
+    for (const sectionId of sectionOrder.split(",")) {
+      const commonProps = {
+        key: sectionId,
+        isFirst: sections.length === 0,
+        isLast: sections.length === expectedCount - 1
+      };
+
+      if (sectionId === "topsites" && showTopSites) {
+        sections.push(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__["TopSites"], commonProps));
+      } else {
+        const section = enabledSections.find(s => s.id === sectionId);
+
+        if (section) {
+          sections.push(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(SectionIntl, _extends({}, section, commonProps)));
+        }
+      }
+    }
+
+    return sections;
+  }
+
+  render() {
+    return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
+      className: "sections-list"
+    }, this.renderSections());
+  }
+
+}
+const Sections = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({
+  Sections: state.Sections,
+  Prefs: state.Prefs
+}))(_Sections);
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
 /* 39 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_SOURCE", function() { return TOP_SITES_SOURCE; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_CONTEXT_MENU_OPTIONS", function() { return TOP_SITES_CONTEXT_MENU_OPTIONS; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS", function() { return TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MIN_RICH_FAVICON_SIZE", function() { return MIN_RICH_FAVICON_SIZE; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MIN_CORNER_FAVICON_SIZE", function() { return MIN_CORNER_FAVICON_SIZE; });
-const TOP_SITES_SOURCE = "TOP_SITES";
-const TOP_SITES_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"]; // the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite
-
-const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "BlockUrl"]; // minimum size necessary to show a rich icon instead of a screenshot
-
-const MIN_RICH_FAVICON_SIZE = 96; // minimum size necessary to show any icon in the top left corner with a screenshot
-
-const MIN_CORNER_FAVICON_SIZE = 16;
+/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ScreenshotUtils", function() { return ScreenshotUtils; });
+/**
+ * List of helper functions for screenshot-based images.
+ *
+ * There are two kinds of images:
+ * 1. Remote Image: This is the image from the main process and it refers to
+ *    the image in the React props. This can either be an object with the `data`
+ *    and `path` properties, if it is a blob, or a string, if it is a normal image.
+ * 2. Local Image: This is the image object in the content process and it refers
+ *    to the image *object* in the React component's state. All local image
+ *    objects have the `url` property, and an additional property `path`, if they
+ *    are blobs.
+ */
+const ScreenshotUtils = {
+  isBlob(isLocal, image) {
+    return !!(image && image.path && (!isLocal && image.data || isLocal && image.url));
+  },
+
+  // This should always be called with a remote image and not a local image.
+  createLocalImageObject(remoteImage) {
+    if (!remoteImage) {
+      return null;
+    }
+
+    if (this.isBlob(false, remoteImage)) {
+      return {
+        url: global.URL.createObjectURL(remoteImage.data),
+        path: remoteImage.path
+      };
+    }
+
+    return {
+      url: remoteImage
+    };
+  },
+
+  // Revokes the object URL of the image if the local image is a blob.
+  // This should always be called with a local image and not a remote image.
+  maybeRevokeBlobObjectURL(localImage) {
+    if (this.isBlob(true, localImage)) {
+      global.URL.revokeObjectURL(localImage.url);
+    }
+  },
+
+  // Checks if remoteImage and localImage are the same.
+  isRemoteImageLocal(localImage, remoteImage) {
+    // Both remoteImage and localImage are present.
+    if (remoteImage && localImage) {
+      return this.isBlob(false, remoteImage) ? localImage.path === remoteImage.path : localImage.url === remoteImage;
+    } // This will only handle the remaining three possible outcomes.
+    // (i.e. everything except when both image and localImage are present)
+
+
+    return !remoteImage && !localImage;
+  }
+
+};
+/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
 /* 40 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ComponentPerfTimer", function() { return ComponentPerfTimer; });
@@ -5797,24 +5997,393 @@ function _PerfService(options) {
 var perfService = new _PerfService();
 
 /***/ }),
 /* 42 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MoreRecommendations", function() { return MoreRecommendations; });
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(10);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
+
+
+class MoreRecommendations extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
+  render() {
+    const {
+      read_more_endpoint
+    } = this.props;
+
+    if (read_more_endpoint) {
+      return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
+        className: "more-recommendations",
+        href: read_more_endpoint
+      }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], {
+        id: "pocket_more_reccommendations"
+      }));
+    }
+
+    return null;
+  }
+
+}
+
+/***/ }),
+/* 43 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_PocketLoggedInCta", function() { return _PocketLoggedInCta; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PocketLoggedInCta", function() { return PocketLoggedInCta; });
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(25);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
+
+
+
+class _PocketLoggedInCta extends react__WEBPACK_IMPORTED_MODULE_2___default.a.PureComponent {
+  render() {
+    const {
+      pocketCta
+    } = this.props.Pocket;
+    return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("span", {
+      className: "pocket-logged-in-cta"
+    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("a", {
+      className: "pocket-cta-button",
+      href: pocketCta.ctaUrl ? pocketCta.ctaUrl : "https://getpocket.com/"
+    }, pocketCta.ctaButton ? pocketCta.ctaButton : react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], {
+      id: "pocket_cta_button"
+    })), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("a", {
+      href: pocketCta.ctaUrl ? pocketCta.ctaUrl : "https://getpocket.com/"
+    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("span", {
+      className: "cta-text"
+    }, pocketCta.ctaText ? pocketCta.ctaText : react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], {
+      id: "pocket_cta_text"
+    }))));
+  }
+
+}
+const PocketLoggedInCta = Object(react_redux__WEBPACK_IMPORTED_MODULE_0__["connect"])(state => ({
+  Pocket: state.Pocket
+}))(_PocketLoggedInCta);
+
+/***/ }),
+/* 44 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Topic", function() { return Topic; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Topics", function() { return Topics; });
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(10);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
+
+
+class Topic extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
+  render() {
+    const {
+      url,
+      name
+    } = this.props;
+    return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("li", null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
+      key: name,
+      href: url
+    }, name));
+  }
+
+}
+class Topics extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
+  render() {
+    const {
+      topics
+    } = this.props;
+    return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", {
+      className: "topics"
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], {
+      id: "pocket_read_more"
+    })), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("ul", null, topics && topics.map(t => react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(Topic, {
+      key: t.name,
+      url: t.url,
+      name: t.name
+    }))));
+  }
+
+}
+
+/***/ }),
+/* 45 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_TopSites", function() { return _TopSites; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSites", function() { return TopSites; });
+/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
+/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(46);
+/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(33);
+/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(40);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(25);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_5__);
+/* harmony import */ var _asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(14);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(10);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_7__);
+/* harmony import */ var _SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(47);
+/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(55);
+/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(57);
+/* harmony import */ var _TopSite__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(48);
+function _extends() { _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; }; return _extends.apply(this, arguments); }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+function topSiteIconType(link) {
+  if (link.customScreenshotURL) {
+    return "custom_screenshot";
+  }
+
+  if (link.tippyTopIcon || link.faviconRef === "tippytop") {
+    return "tippytop";
+  }
+
+  if (link.faviconSize >= _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["MIN_RICH_FAVICON_SIZE"]) {
+    return "rich_icon";
+  }
+
+  if (link.screenshot && link.faviconSize >= _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["MIN_CORNER_FAVICON_SIZE"]) {
+    return "screenshot_with_icon";
+  }
+
+  if (link.screenshot) {
+    return "screenshot";
+  }
+
+  return "no_image";
+}
+/**
+ * Iterates through TopSites and counts types of images.
+ * @param acc Accumulator for reducer.
+ * @param topsite Entry in TopSites.
+ */
+
+
+function countTopSitesIconsTypes(topSites) {
+  const countTopSitesTypes = (acc, link) => {
+    acc[topSiteIconType(link)]++;
+    return acc;
+  };
+
+  return topSites.reduce(countTopSitesTypes, {
+    "custom_screenshot": 0,
+    "screenshot_with_icon": 0,
+    "screenshot": 0,
+    "tippytop": 0,
+    "rich_icon": 0,
+    "no_image": 0
+  });
+}
+
+class _TopSites extends react__WEBPACK_IMPORTED_MODULE_7___default.a.PureComponent {
+  constructor(props) {
+    super(props);
+    this.onEditFormClose = this.onEditFormClose.bind(this);
+    this.onSearchShortcutsFormClose = this.onSearchShortcutsFormClose.bind(this);
+  }
+  /**
+   * Dispatch session statistics about the quality of TopSites icons and pinned count.
+   */
+
+
+  _dispatchTopSitesStats() {
+    const topSites = this._getVisibleTopSites();
+
+    const topSitesIconsStats = countTopSitesIconsTypes(topSites);
+    const topSitesPinned = topSites.filter(site => !!site.isPinned).length;
+    const searchShortcuts = topSites.filter(site => !!site.searchTopSite).length; // Dispatch telemetry event with the count of TopSites images types.
+
+    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
+      type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SAVE_SESSION_PERF_DATA,
+      data: {
+        topsites_icon_stats: topSitesIconsStats,
+        topsites_pinned: topSitesPinned,
+        topsites_search_shortcuts: searchShortcuts
+      }
+    }));
+  }
+  /**
+   * Return the TopSites that are visible based on prefs and window width.
+   */
+
+
+  _getVisibleTopSites() {
+    // We hide 2 sites per row when not in the wide layout.
+    let sitesPerRow = common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_9__["TOP_SITES_MAX_SITES_PER_ROW"]; // $break-point-widest = 1072px (from _variables.scss)
+
+    if (!global.matchMedia(`(min-width: 1072px)`).matches) {
+      sitesPerRow -= 2;
+    }
+
+    return this.props.TopSites.rows.slice(0, this.props.TopSitesRows * sitesPerRow);
+  }
+
+  componentDidUpdate() {
+    this._dispatchTopSitesStats();
+  }
+
+  componentDidMount() {
+    this._dispatchTopSitesStats();
+  }
+
+  onEditFormClose() {
+    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
+      source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["TOP_SITES_SOURCE"],
+      event: "TOP_SITES_EDIT_CLOSE"
+    }));
+    this.props.dispatch({
+      type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_CANCEL_EDIT
+    });
+  }
+
+  onSearchShortcutsFormClose() {
+    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
+      source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["TOP_SITES_SOURCE"],
+      event: "SEARCH_EDIT_CLOSE"
+    }));
+    this.props.dispatch({
+      type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL
+    });
+  }
+
+  render() {
+    const {
+      props
+    } = this;
+    const {
+      editForm,
+      showSearchShortcutsForm
+    } = props.TopSites;
+    const extraMenuOptions = ["AddTopSite"];
+
+    if (props.Prefs.values["improvesearch.topSiteSearchShortcuts"]) {
+      extraMenuOptions.push("AddSearchShortcut");
+    }
+
+    return react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__["ComponentPerfTimer"], {
+      id: "topsites",
+      initialized: props.TopSites.initialized,
+      dispatch: props.dispatch
+    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__["CollapsibleSection"], {
+      className: "top-sites",
+      icon: "topsites",
+      id: "topsites",
+      title: this.props.title || {
+        id: "header_top_sites"
+      },
+      extraMenuOptions: extraMenuOptions,
+      showPrefName: "feeds.topsites",
+      eventSource: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["TOP_SITES_SOURCE"],
+      collapsed: props.TopSites.pref ? props.TopSites.pref.collapsed : undefined,
+      isFixed: props.isFixed,
+      isFirst: props.isFirst,
+      isLast: props.isLast,
+      dispatch: props.dispatch
+    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_TopSite__WEBPACK_IMPORTED_MODULE_11__["TopSiteList"], {
+      TopSites: props.TopSites,
+      TopSitesRows: props.TopSitesRows,
+      dispatch: props.dispatch,
+      intl: props.intl,
+      topSiteIconType: topSiteIconType
+    }), react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("div", {
+      className: "edit-topsites-wrapper"
+    }, editForm && react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("div", {
+      className: "edit-topsites"
+    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_6__["ModalOverlayWrapper"], {
+      unstyled: true,
+      onClose: this.onEditFormClose,
+      innerClassName: "modal"
+    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_TopSiteForm__WEBPACK_IMPORTED_MODULE_10__["TopSiteForm"], _extends({
+      site: props.TopSites.rows[editForm.index],
+      onClose: this.onEditFormClose,
+      dispatch: this.props.dispatch,
+      intl: this.props.intl
+    }, editForm)))), showSearchShortcutsForm && react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("div", {
+      className: "edit-search-shortcuts"
+    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_6__["ModalOverlayWrapper"], {
+      unstyled: true,
+      onClose: this.onSearchShortcutsFormClose,
+      innerClassName: "modal"
+    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_8__["SearchShortcutsForm"], {
+      TopSites: props.TopSites,
+      onClose: this.onSearchShortcutsFormClose,
+      dispatch: this.props.dispatch
+    }))))));
+  }
+
+}
+const TopSites = Object(react_redux__WEBPACK_IMPORTED_MODULE_4__["connect"])(state => ({
+  TopSites: state.TopSites,
+  Prefs: state.Prefs,
+  TopSitesRows: state.Prefs.values.topSitesRows
+}))(Object(react_intl__WEBPACK_IMPORTED_MODULE_5__["injectIntl"])(_TopSites));
+/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
+
+/***/ }),
+/* 46 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_SOURCE", function() { return TOP_SITES_SOURCE; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_CONTEXT_MENU_OPTIONS", function() { return TOP_SITES_CONTEXT_MENU_OPTIONS; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS", function() { return TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MIN_RICH_FAVICON_SIZE", function() { return MIN_RICH_FAVICON_SIZE; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MIN_CORNER_FAVICON_SIZE", function() { return MIN_CORNER_FAVICON_SIZE; });
+const TOP_SITES_SOURCE = "TOP_SITES";
+const TOP_SITES_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"]; // the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite
+
+const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "BlockUrl"]; // minimum size necessary to show a rich icon instead of a screenshot
+
+const MIN_RICH_FAVICON_SIZE = 96; // minimum size necessary to show any icon in the top left corner with a screenshot
+
+const MIN_CORNER_FAVICON_SIZE = 16;
+
+/***/ }),
+/* 47 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SelectableSearchShortcut", function() { return SelectableSearchShortcut; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SearchShortcutsForm", function() { return SearchShortcutsForm; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
 /* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
-/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(39);
+/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(46);
 
 
 
 
 class SelectableSearchShortcut extends react__WEBPACK_IMPORTED_MODULE_2___default.a.PureComponent {
   render() {
     const {
       shortcut,
@@ -5986,34 +6555,34 @@ class SearchShortcutsForm extends react_
     }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], {
       id: "topsites_form_save_button"
     }))));
   }
 
 }
 
 /***/ }),
-/* 43 */
+/* 48 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteLink", function() { return TopSiteLink; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSite", function() { return TopSite; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSitePlaceholder", function() { return TopSitePlaceholder; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_TopSiteList", function() { return _TopSiteList; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteList", function() { return TopSiteList; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
 /* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
-/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(39);
+/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(46);
 /* harmony import */ var content_src_components_LinkMenu_LinkMenu__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(29);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(10);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
-/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(44);
+/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(39);
 /* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(55);
 function _extends() { _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; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
@@ -6647,82 +7216,17 @@ class _TopSiteList extends react__WEBPAC
       className: `top-sites-list${this.state.draggedSite ? " dnd-active" : ""}`
     }, topSitesUI);
   }
 
 }
 const TopSiteList = Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(_TopSiteList);
 
 /***/ }),
-/* 44 */
-/***/ (function(module, __webpack_exports__, __webpack_require__) {
-
-"use strict";
-__webpack_require__.r(__webpack_exports__);
-/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ScreenshotUtils", function() { return ScreenshotUtils; });
-/**
- * List of helper functions for screenshot-based images.
- *
- * There are two kinds of images:
- * 1. Remote Image: This is the image from the main process and it refers to
- *    the image in the React props. This can either be an object with the `data`
- *    and `path` properties, if it is a blob, or a string, if it is a normal image.
- * 2. Local Image: This is the image object in the content process and it refers
- *    to the image *object* in the React component's state. All local image
- *    objects have the `url` property, and an additional property `path`, if they
- *    are blobs.
- */
-const ScreenshotUtils = {
-  isBlob(isLocal, image) {
-    return !!(image && image.path && (!isLocal && image.data || isLocal && image.url));
-  },
-
-  // This should always be called with a remote image and not a local image.
-  createLocalImageObject(remoteImage) {
-    if (!remoteImage) {
-      return null;
-    }
-
-    if (this.isBlob(false, remoteImage)) {
-      return {
-        url: global.URL.createObjectURL(remoteImage.data),
-        path: remoteImage.path
-      };
-    }
-
-    return {
-      url: remoteImage
-    };
-  },
-
-  // Revokes the object URL of the image if the local image is a blob.
-  // This should always be called with a local image and not a remote image.
-  maybeRevokeBlobObjectURL(localImage) {
-    if (this.isBlob(true, localImage)) {
-      global.URL.revokeObjectURL(localImage.url);
-    }
-  },
-
-  // Checks if remoteImage and localImage are the same.
-  isRemoteImageLocal(localImage, remoteImage) {
-    // Both remoteImage and localImage are present.
-    if (remoteImage && localImage) {
-      return this.isBlob(false, remoteImage) ? localImage.path === remoteImage.path : localImage.url === remoteImage;
-    } // This will only handle the remaining three possible outcomes.
-    // (i.e. everything except when both image and localImage are present)
-
-
-    return !remoteImage && !localImage;
-  }
-
-};
-/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
-
-/***/ }),
-/* 45 */
+/* 49 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Search", function() { return _Search; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Search", function() { return Search; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
@@ -6920,508 +7424,16 @@ class _Search extends react__WEBPACK_IMP
       ref: this.onInputMount
     })));
   }
 
 }
 const Search = Object(react_redux__WEBPACK_IMPORTED_MODULE_2__["connect"])()(Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(_Search));
 
 /***/ }),
-/* 46 */
-/***/ (function(module, __webpack_exports__, __webpack_require__) {
-
-"use strict";
-__webpack_require__.r(__webpack_exports__);
-/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Section", function() { return Section; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionIntl", function() { return SectionIntl; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Sections", function() { return _Sections; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Sections", function() { return Sections; });
-/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(56);
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_2__);
-/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(33);
-/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(40);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_5__);
-/* harmony import */ var content_src_components_MoreRecommendations_MoreRecommendations__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(47);
-/* harmony import */ var content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(48);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(10);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_8__);
-/* harmony import */ var content_src_components_Topics_Topics__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(49);
-/* harmony import */ var content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(38);
-function _extends() { _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; }; return _extends.apply(this, arguments); }
-
-
-
-
-
-
-
-
-
-
-
-
-const VISIBLE = "visible";
-const VISIBILITY_CHANGE_EVENT = "visibilitychange";
-const CARDS_PER_ROW_DEFAULT = 3;
-const CARDS_PER_ROW_COMPACT_WIDE = 4;
-
-function getFormattedMessage(message) {
-  return typeof message === "string" ? react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("span", null, message) : react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_2__["FormattedMessage"], message);
-}
-
-class Section extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
-  get numRows() {
-    const {
-      rowsPref,
-      maxRows,
-      Prefs
-    } = this.props;
-    return rowsPref ? Prefs.values[rowsPref] : maxRows;
-  }
-
-  _dispatchImpressionStats() {
-    const {
-      props
-    } = this;
-    let cardsPerRow = CARDS_PER_ROW_DEFAULT;
-
-    if (props.compactCards && global.matchMedia(`(min-width: 1072px)`).matches) {
-      // If the section has compact cards and the viewport is wide enough, we show
-      // 4 columns instead of 3.
-      // $break-point-widest = 1072px (from _variables.scss)
-      cardsPerRow = CARDS_PER_ROW_COMPACT_WIDE;
-    }
-
-    const maxCards = cardsPerRow * this.numRows;
-    const cards = props.rows.slice(0, maxCards);
-
-    if (this.needsImpressionStats(cards)) {
-      props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ImpressionStats({
-        source: props.eventSource,
-        tiles: cards.map(link => ({
-          id: link.guid
-        }))
-      }));
-      this.impressionCardGuids = cards.map(link => link.guid);
-    }
-  } // 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;
-
-    if (!props.shouldSendImpressionStats || !props.dispatch) {
-      return;
-    }
-
-    if (props.document.visibilityState === VISIBLE) {
-      this._dispatchImpressionStats();
-    } else {
-      // We should only ever send the latest impression stats ping, so remove any
-      // older listeners.
-      if (this._onVisibilityChange) {
-        props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
-      } // When the page becomes visible, send the impression stats ping if the section isn't collapsed.
-
-
-      this._onVisibilityChange = () => {
-        if (props.document.visibilityState === VISIBLE) {
-          if (!this.props.pref.collapsed) {
-            this._dispatchImpressionStats();
-          }
-
-          props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
-        }
-      };
-
-      props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
-    }
-  }
-
-  componentWillMount() {
-    this.sendNewTabRehydrated(this.props.initialized);
-  }
-
-  componentDidMount() {
-    if (this.props.rows.length && !this.props.pref.collapsed) {
-      this.sendImpressionStatsOrAddListener();
-    }
-  }
-
-  componentDidUpdate(prevProps) {
-    const {
-      props
-    } = this;
-    const isCollapsed = props.pref.collapsed;
-    const wasCollapsed = prevProps.pref.collapsed;
-
-    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
-    // and the section is not collapsed...
-    props.rows !== prevProps.rows && !isCollapsed || // or if we are expanding a section that was collapsed.
-    wasCollapsed && !isCollapsed)) {
-      this.sendImpressionStatsOrAddListener();
-    }
-  }
-
-  componentWillUpdate(nextProps) {
-    this.sendNewTabRehydrated(nextProps.initialized);
-  }
-
-  componentWillUnmount() {
-    if (this._onVisibilityChange) {
-      this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
-    }
-  }
-
-  needsImpressionStats(cards) {
-    if (!this.impressionCardGuids || this.impressionCardGuids.length !== cards.length) {
-      return true;
-    }
-
-    for (let i = 0; i < cards.length; i++) {
-      if (cards[i].guid !== this.impressionCardGuids[i]) {
-        return true;
-      }
-    }
-
-    return false;
-  } // The NEW_TAB_REHYDRATED event is used to inform feeds that their
-  // data has been consumed e.g. for counting the number of tabs that
-  // have rendered that data.
-
-
-  sendNewTabRehydrated(initialized) {
-    if (initialized && !this.renderNotified) {
-      this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
-        type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].NEW_TAB_REHYDRATED,
-        data: {}
-      }));
-      this.renderNotified = true;
-    }
-  }
-
-  render() {
-    const {
-      id,
-      eventSource,
-      title,
-      icon,
-      rows,
-      Pocket,
-      topics,
-      emptyState,
-      dispatch,
-      compactCards,
-      read_more_endpoint,
-      contextMenuOptions,
-      initialized,
-      learnMore,
-      pref,
-      privacyNoticeURL,
-      isFirst,
-      isLast
-    } = this.props;
-    const waitingForSpoc = id === "topstories" && this.props.Pocket.waitingForSpoc;
-    const maxCardsPerRow = compactCards ? CARDS_PER_ROW_COMPACT_WIDE : CARDS_PER_ROW_DEFAULT;
-    const {
-      numRows
-    } = this;
-    const maxCards = maxCardsPerRow * numRows;
-    const maxCardsOnNarrow = CARDS_PER_ROW_DEFAULT * numRows;
-    const {
-      pocketCta,
-      isUserLoggedIn
-    } = Pocket || {};
-    const {
-      useCta
-    } = pocketCta || {}; // Don't display anything until we have a definitve result from Pocket,
-    // to avoid a flash of logged out state while we render.
-
-    const isPocketLoggedInDefined = isUserLoggedIn === true || isUserLoggedIn === false;
-    const hasTopics = topics && topics.length > 0;
-    const shouldShowPocketCta = id === "topstories" && useCta && isUserLoggedIn === false; // Show topics only for top stories and if it has loaded with topics.
-    // The classs .top-stories-bottom-container ensures content doesn't shift as things load.
-
-    const shouldShowTopics = id === "topstories" && hasTopics && (useCta && isUserLoggedIn === true || !useCta && isPocketLoggedInDefined); // We use topics to determine language support for read more.
-
-    const shouldShowReadMore = read_more_endpoint && hasTopics;
-    const realRows = rows.slice(0, maxCards); // The empty state should only be shown after we have initialized and there is no content.
-    // Otherwise, we should show placeholders.
-
-    const shouldShowEmptyState = initialized && !rows.length;
-    const cards = [];
-
-    if (!shouldShowEmptyState) {
-      for (let i = 0; i < maxCards; i++) {
-        const link = realRows[i]; // On narrow viewports, we only show 3 cards per row. We'll mark the rest as
-        // .hide-for-narrow to hide in CSS via @media query.
-
-        const className = i >= maxCardsOnNarrow ? "hide-for-narrow" : "";
-        let usePlaceholder = !link; // If we are in the third card and waiting for spoc,
-        // use the placeholder.
-
-        if (!usePlaceholder && i === 2 && waitingForSpoc) {
-          usePlaceholder = true;
-        }
-
-        cards.push(!usePlaceholder ? react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__["Card"], {
-          key: i,
-          index: i,
-          className: className,
-          dispatch: dispatch,
-          link: link,
-          contextMenuOptions: contextMenuOptions,
-          eventSource: eventSource,
-          shouldSendImpressionStats: this.props.shouldSendImpressionStats,
-          isWebExtension: this.props.isWebExtension
-        }) : react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__["PlaceholderCard"], {
-          key: i,
-          className: className
-        }));
-      }
-    }
-
-    const sectionClassName = ["section", compactCards ? "compact-cards" : "normal-cards"].join(" "); // <Section> <-- React component
-    // <section> <-- HTML5 element
-
-    return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_4__["ComponentPerfTimer"], this.props, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_3__["CollapsibleSection"], {
-      className: sectionClassName,
-      icon: icon,
-      title: title,
-      id: id,
-      eventSource: eventSource,
-      collapsed: this.props.pref.collapsed,
-      showPrefName: pref && pref.feed || id,
-      privacyNoticeURL: privacyNoticeURL,
-      Prefs: this.props.Prefs,
-      isFirst: isFirst,
-      isLast: isLast,
-      learnMore: learnMore,
-      dispatch: this.props.dispatch,
-      isWebExtension: this.props.isWebExtension
-    }, !shouldShowEmptyState && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("ul", {
-      className: "section-list",
-      style: {
-        padding: 0
-      }
-    }, cards), shouldShowEmptyState && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
-      className: "section-empty-state"
-    }, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
-      className: "empty-state"
-    }, emptyState.icon && emptyState.icon.startsWith("moz-extension://") ? react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("span", {
-      className: "empty-state-icon icon",
-      style: {
-        "background-image": `url('${emptyState.icon}')`
-      }
-    }) : react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("span", {
-      className: `empty-state-icon icon icon-${emptyState.icon}`
-    }), react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("p", {
-      className: "empty-state-message"
-    }, getFormattedMessage(emptyState.message)))), id === "topstories" && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
-      className: "top-stories-bottom-container"
-    }, shouldShowTopics && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
-      className: "wrapper-topics"
-    }, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Topics_Topics__WEBPACK_IMPORTED_MODULE_9__["Topics"], {
-      topics: this.props.topics
-    })), shouldShowPocketCta && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
-      className: "wrapper-cta"
-    }, react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__["PocketLoggedInCta"], null)), react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
-      className: "wrapper-more-recommendations"
-    }, shouldShowReadMore && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_MoreRecommendations_MoreRecommendations__WEBPACK_IMPORTED_MODULE_6__["MoreRecommendations"], {
-      read_more_endpoint: read_more_endpoint
-    })))));
-  }
-
-}
-Section.defaultProps = {
-  document: global.document,
-  rows: [],
-  emptyState: {},
-  pref: {},
-  title: ""
-};
-const SectionIntl = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({
-  Prefs: state.Prefs,
-  Pocket: state.Pocket
-}))(Object(react_intl__WEBPACK_IMPORTED_MODULE_2__["injectIntl"])(Section));
-class _Sections extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
-  renderSections() {
-    const sections = [];
-    const enabledSections = this.props.Sections.filter(section => section.enabled);
-    const {
-      sectionOrder,
-      "feeds.topsites": showTopSites
-    } = this.props.Prefs.values; // Enabled sections doesn't include Top Sites, so we add it if enabled.
-
-    const expectedCount = enabledSections.length + ~~showTopSites;
-
-    for (const sectionId of sectionOrder.split(",")) {
-      const commonProps = {
-        key: sectionId,
-        isFirst: sections.length === 0,
-        isLast: sections.length === expectedCount - 1
-      };
-
-      if (sectionId === "topsites" && showTopSites) {
-        sections.push(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__["TopSites"], commonProps));
-      } else {
-        const section = enabledSections.find(s => s.id === sectionId);
-
-        if (section) {
-          sections.push(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(SectionIntl, _extends({}, section, commonProps)));
-        }
-      }
-    }
-
-    return sections;
-  }
-
-  render() {
-    return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("div", {
-      className: "sections-list"
-    }, this.renderSections());
-  }
-
-}
-const Sections = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({
-  Sections: state.Sections,
-  Prefs: state.Prefs
-}))(_Sections);
-/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
-
-/***/ }),
-/* 47 */
-/***/ (function(module, __webpack_exports__, __webpack_require__) {
-
-"use strict";
-__webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MoreRecommendations", function() { return MoreRecommendations; });
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_0__);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(10);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
-
-
-class MoreRecommendations extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
-  render() {
-    const {
-      read_more_endpoint
-    } = this.props;
-
-    if (read_more_endpoint) {
-      return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
-        className: "more-recommendations",
-        href: read_more_endpoint
-      }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], {
-        id: "pocket_more_reccommendations"
-      }));
-    }
-
-    return null;
-  }
-
-}
-
-/***/ }),
-/* 48 */
-/***/ (function(module, __webpack_exports__, __webpack_require__) {
-
-"use strict";
-__webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_PocketLoggedInCta", function() { return _PocketLoggedInCta; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PocketLoggedInCta", function() { return PocketLoggedInCta; });
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(25);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_0__);
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
-
-
-
-class _PocketLoggedInCta extends react__WEBPACK_IMPORTED_MODULE_2___default.a.PureComponent {
-  render() {
-    const {
-      pocketCta
-    } = this.props.Pocket;
-    return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("span", {
-      className: "pocket-logged-in-cta"
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("a", {
-      className: "pocket-cta-button",
-      href: pocketCta.ctaUrl ? pocketCta.ctaUrl : "https://getpocket.com/"
-    }, pocketCta.ctaButton ? pocketCta.ctaButton : react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], {
-      id: "pocket_cta_button"
-    })), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("a", {
-      href: pocketCta.ctaUrl ? pocketCta.ctaUrl : "https://getpocket.com/"
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("span", {
-      className: "cta-text"
-    }, pocketCta.ctaText ? pocketCta.ctaText : react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], {
-      id: "pocket_cta_text"
-    }))));
-  }
-
-}
-const PocketLoggedInCta = Object(react_redux__WEBPACK_IMPORTED_MODULE_0__["connect"])(state => ({
-  Pocket: state.Pocket
-}))(_PocketLoggedInCta);
-
-/***/ }),
-/* 49 */
-/***/ (function(module, __webpack_exports__, __webpack_require__) {
-
-"use strict";
-__webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Topic", function() { return Topic; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Topics", function() { return Topics; });
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
-/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_0__);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(10);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
-
-
-class Topic extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
-  render() {
-    const {
-      url,
-      name
-    } = this.props;
-    return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("li", null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
-      key: name,
-      href: url
-    }, name));
-  }
-
-}
-class Topics extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
-  render() {
-    const {
-      topics
-    } = this.props;
-    return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", {
-      className: "topics"
-    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], {
-      id: "pocket_read_more"
-    })), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("ul", null, topics && topics.map(t => react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(Topic, {
-      key: t.name,
-      url: t.url,
-      name: t.name
-    }))));
-  }
-
-}
-
-/***/ }),
 /* 50 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DetectUserSessionStart", function() { return DetectUserSessionStart; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(41);
@@ -7642,16 +7654,18 @@ var LinkMenu = __webpack_require__(29);
 
 class DSLinkMenu_DSLinkMenu extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
       activeCard: null,
       showContextMenu: false
     };
+    this.windowObj = this.props.windowObj || window; // Added to support unit tests
+
     this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
     this.onMenuUpdate = this.onMenuUpdate.bind(this);
     this.onMenuShow = this.onMenuShow.bind(this);
     this.contextMenuButtonRef = external_React_default.a.createRef();
   }
 
   onMenuButtonClick(event) {
     event.preventDefault();
@@ -7670,17 +7684,17 @@ class DSLinkMenu_DSLinkMenu extends exte
     this.setState({
       showContextMenu
     });
   }
 
   onMenuShow() {
     const dsLinkMenuHostDiv = this.contextMenuButtonRef.current.parentElement;
 
-    if (window.scrollMaxX > 0) {
+    if (this.windowObj.scrollMaxX > 0) {
       dsLinkMenuHostDiv.parentElement.classList.add("last-item");
     }
 
     dsLinkMenuHostDiv.parentElement.classList.add("active");
   }
 
   render() {
     const {
@@ -7716,16 +7730,17 @@ class DSLinkMenu_DSLinkMenu extends exte
       shouldSendImpressionStats: true,
       site: {
         referrer: "https://getpocket.com/recommendations",
         title: this.props.title,
         type: this.props.type,
         url: this.props.url,
         guid: this.props.id,
         pocket_id: this.props.pocket_id,
+        shim: this.props.shim,
         bookmarkGuid: this.props.bookmarkGuid
       }
     }));
   }
 
 }
 const DSLinkMenu = Object(external_ReactIntl_["injectIntl"])(DSLinkMenu_DSLinkMenu);
 // EXTERNAL MODULE: ./content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
@@ -7828,18 +7843,18 @@ class DSCard_DSCard extends external_Rea
         action_position: this.props.pos
       }));
       this.props.dispatch(Actions["actionCreators"].ImpressionStats({
         source: this.props.type.toUpperCase(),
         click: 0,
         tiles: [{
           id: this.props.id,
           pos: this.props.pos,
-          ...(this.props.shim ? {
-            shim: this.props.shim
+          ...(this.props.shim && this.props.shim.click ? {
+            shim: this.props.shim.click
           } : {})
         }]
       }));
     }
   }
 
   render() {
     return external_React_default.a.createElement("div", {
@@ -7867,32 +7882,33 @@ class DSCard_DSCard extends external_Rea
       className: "excerpt clamp"
     }, this.props.excerpt)), this.props.context && external_React_default.a.createElement("p", {
       className: "context"
     }, this.props.context)), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
       campaignId: this.props.campaignId,
       rows: [{
         id: this.props.id,
         pos: this.props.pos,
-        ...(this.props.shim ? {
-          shim: this.props.shim
+        ...(this.props.shim && this.props.shim.impression ? {
+          shim: this.props.shim.impression
         } : {})
       }],
       dispatch: this.props.dispatch,
       source: this.props.type
     })), !this.props.placeholder && external_React_default.a.createElement(DSLinkMenu, {
       id: this.props.id,
       index: this.props.pos,
       dispatch: this.props.dispatch,
       intl: this.props.intl,
       url: this.props.url,
       title: this.props.title,
       source: this.props.source,
       type: this.props.type,
       pocket_id: this.props.pocket_id,
+      shim: this.props.shim,
       bookmarkGuid: this.props.bookmarkGuid
     }));
   }
 
 }
 const PlaceholderDSCard = props => external_React_default.a.createElement(DSCard_DSCard, {
   placeholder: true
 });
@@ -8111,18 +8127,18 @@ class List_ListItem extends external_Rea
         action_position: this.props.pos
       }));
       this.props.dispatch(Actions["actionCreators"].ImpressionStats({
         source: this.props.type.toUpperCase(),
         click: 0,
         tiles: [{
           id: this.props.id,
           pos: this.props.pos,
-          ...(this.props.shim ? {
-            shim: this.props.shim
+          ...(this.props.shim && this.props.shim.click ? {
+            shim: this.props.shim.click
           } : {})
         }]
       }));
     }
   }
 
   render() {
     return external_React_default.a.createElement("li", {
@@ -8146,32 +8162,33 @@ class List_ListItem extends external_Rea
       extraClassNames: "ds-list-image",
       source: this.props.image_src,
       rawSource: this.props.raw_image_src
     }), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
       campaignId: this.props.campaignId,
       rows: [{
         id: this.props.id,
         pos: this.props.pos,
-        ...(this.props.shim ? {
-          shim: this.props.shim
+        ...(this.props.shim && this.props.shim.impression ? {
+          shim: this.props.shim.impression
         } : {})
       }],
       dispatch: this.props.dispatch,
       source: this.props.type
     })), !this.props.placeholder && external_React_default.a.createElement(DSLinkMenu, {
       id: this.props.id,
       index: this.props.pos,
       dispatch: this.props.dispatch,
       intl: this.props.intl,
       url: this.props.url,
       title: this.props.title,
       source: this.props.source,
       type: this.props.type,
       pocket_id: this.props.pocket_id,
+      shim: this.props.shim,
       bookmarkGuid: this.props.bookmarkGuid
     }));
   }
 
 }
 const PlaceholderListItem = props => external_React_default.a.createElement(List_ListItem, {
   placeholder: true
 });
@@ -8275,18 +8292,18 @@ class Hero_Hero extends external_React_d
         action_position: this.heroRec.pos
       }));
       this.props.dispatch(Actions["actionCreators"].ImpressionStats({
         source: this.props.type.toUpperCase(),
         click: 0,
         tiles: [{
           id: this.heroRec.id,
           pos: this.heroRec.pos,
-          ...(this.heroRec.shim ? {
-            shim: this.heroRec.shim
+          ...(this.heroRec.shim && this.heroRec.shim.click ? {
+            shim: this.heroRec.shim.click
           } : {})
         }]
       }));
     }
   }
 
   renderHero() {
     let [heroRec, ...otherRecs] = this.props.data.recommendations.slice(0, this.props.items);
@@ -8346,32 +8363,33 @@ class Hero_Hero extends external_React_d
         className: "clamp"
       }, heroRec.title), external_React_default.a.createElement("p", {
         className: "excerpt clamp"
       }, heroRec.excerpt))), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
         campaignId: heroRec.campaignId,
         rows: [{
           id: heroRec.id,
           pos: heroRec.pos,
-          ...(heroRec.shim ? {
-            shim: heroRec.shim
+          ...(heroRec.shim && heroRec.shim.impression ? {
+            shim: heroRec.shim.impression
           } : {})
         }],
         dispatch: this.props.dispatch,
         source: this.props.type
       })), external_React_default.a.createElement(DSLinkMenu, {
         id: heroRec.id,
         index: heroRec.pos,
         dispatch: this.props.dispatch,
         intl: this.props.intl,
         url: heroRec.url,
         title: heroRec.title,
         source: heroRec.domain,
         type: this.props.type,
         pocket_id: heroRec.pocket_id,
+        shim: heroRec.shim,
         bookmarkGuid: heroRec.bookmarkGuid
       }));
     }
 
     let list = external_React_default.a.createElement(List, {
       recStartingPoint: 1,
       data: this.props.data,
       feed: this.props.feed,
@@ -8411,16 +8429,44 @@ class Hero_Hero extends external_React_d
 
 }
 Hero_Hero.defaultProps = {
   data: {},
   border: `border`,
   items: 1 // Number of stories to display
 
 };
+// EXTERNAL MODULE: ./content-src/components/Sections/Sections.jsx
+var Sections = __webpack_require__(38);
+
+// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx
+function _extends() { _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; }; return _extends.apply(this, arguments); }
+
+
+
+
+class Highlights_Highlights extends external_React_default.a.PureComponent {
+  render() {
+    const section = this.props.Sections.find(s => s.id === "highlights");
+
+    if (!section || !section.enabled) {
+      return null;
+    }
+
+    return external_React_default.a.createElement("div", {
+      className: "ds-highlights sections-list"
+    }, external_React_default.a.createElement(Sections["SectionIntl"], _extends({}, section, {
+      isFixed: true
+    })));
+  }
+
+}
+const Highlights = Object(external_ReactRedux_["connect"])(state => ({
+  Sections: state.Sections
+}))(Highlights_Highlights);
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule.jsx
 
 class HorizontalRule_HorizontalRule extends external_React_default.a.PureComponent {
   render() {
     return external_React_default.a.createElement("hr", {
       className: "ds-hr"
     });
   }
@@ -8677,17 +8723,17 @@ const selectLayoutRender = (state, prefs
   }
 
   return {
     spocsFill,
     layoutRender
   };
 };
 // EXTERNAL MODULE: ./content-src/components/TopSites/TopSites.jsx
-var TopSites = __webpack_require__(38);
+var TopSites = __webpack_require__(45);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/TopSites/TopSites.jsx
 
 
 
 class TopSites_TopSites extends external_React_default.a.PureComponent {
   render() {
     const header = this.props.header || {};
@@ -8715,16 +8761,17 @@ const TopSites_TopSites_TopSites = Objec
 
 
 
 
 
 
 
 
+
 const ALLOWED_CSS_URL_PREFIXES = ["chrome://", "resource://", "https://img-getpocket.cdn.mozilla.net/"];
 const DUMMY_CSS_SELECTOR = "DUMMY#CSS.SELECTOR";
 let rickRollCache = []; // Cache of random probability values for a spoc position
 
 /**
  * Validate a CSS declaration. The values are assumed to be normalized by CSSOM.
  */
 
@@ -8791,16 +8838,19 @@ class DiscoveryStreamBase_DiscoveryStrea
           }
         });
       });
     });
   }
 
   renderComponent(component, embedWidth) {
     switch (component.type) {
+      case "Highlights":
+        return external_React_default.a.createElement(Highlights, null);
+
       case "TopSites":
         return external_React_default.a.createElement(TopSites_TopSites_TopSites, {
           header: component.header
         });
 
       case "Message":
         return external_React_default.a.createElement(DSMessage_DSMessage, {
           title: component.header && component.header.title,
@@ -8959,17 +9009,22 @@ class DiscoveryStreamBase_DiscoveryStrea
         link: {
           href: message.header.link_url,
           id: message.header.link_text
         }
       },
       privacyNoticeURL: topStories.privacyNoticeURL,
       showPrefName: topStories.pref.feed,
       title: message.header.title
-    }, this.renderLayout(layoutRender)));
+    }, this.renderLayout(layoutRender)), this.renderLayout([{
+      width: 12,
+      components: [{
+        type: "Highlights"
+      }]
+    }]));
   }
 
   renderLayout(layoutRender) {
     const styles = [];
     return external_React_default.a.createElement("div", {
       className: "discovery-stream ds-layout"
     }, layoutRender.map((row, rowIndex) => external_React_default.a.createElement("div", {
       key: `row-${rowIndex}`,
@@ -9956,17 +10011,17 @@ class SnippetBase_SnippetBase extends ex
 function _extends() { _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; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
 
-const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png"; // Alt text if available; in the future this should come from the server. See bug 1551711
+const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png"; // Alt text placeholder in case the prop from the server isn't available
 
 const ICON_ALT_TEXT = "";
 class SimpleSnippet_SimpleSnippet extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onButtonClick = this.onButtonClick.bind(this);
   }
 
@@ -10129,21 +10184,21 @@ class SimpleSnippet_SimpleSnippet extend
       className: className,
       textStyle: this.props.textStyle
     }), sectionHeader, external_React_default.a.createElement(ConditionalWrapper, {
       condition: sectionHeader,
       wrap: this.wrapSnippetContent
     }, external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon) || DEFAULT_ICON_PATH,
       className: "icon icon-light-theme",
-      alt: ICON_ALT_TEXT
+      alt: props.content.icon_alt_text || ICON_ALT_TEXT
     }), external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon_dark_theme || props.content.icon) || DEFAULT_ICON_PATH,
       className: "icon icon-dark-theme",
-      alt: ICON_ALT_TEXT
+      alt: props.content.icon_alt_text || ICON_ALT_TEXT
     }), external_React_default.a.createElement("div", null, this.renderTitle(), " ", external_React_default.a.createElement("p", {
       className: "body"
     }, this.renderText()), this.props.extraContent), external_React_default.a.createElement("div", null, this.renderButton())));
   }
 
 }
 // CONCATENATED MODULE: ./content-src/asrouter/templates/EOYSnippet/EOYSnippet.jsx
 function EOYSnippet_extends() { EOYSnippet_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; }; return EOYSnippet_extends.apply(this, arguments); }
@@ -10296,17 +10351,17 @@ var FXASignupSnippet_schema = __webpack_
 // CONCATENATED MODULE: ./content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx
 function SubmitFormSnippet_extends() { SubmitFormSnippet_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; }; return SubmitFormSnippet_extends.apply(this, arguments); }
 
 
 
 
 
 
- // Alt text if available; in the future this should come from the server. See bug 1551711
+ // Alt text placeholder in case the prop from the server isn't available
 
 const SubmitFormSnippet_ICON_ALT_TEXT = "";
 class SubmitFormSnippet_SubmitFormSnippet extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.expandSnippet = this.expandSnippet.bind(this);
     this.handleSubmit = this.handleSubmit.bind(this);
     this.handleSubmitAttempt = this.handleSubmitAttempt.bind(this);
@@ -10387,18 +10442,18 @@ class SubmitFormSnippet_SubmitFormSnippe
       }
 
       this.props.sendUserActionTelemetry({
         event: "CLICK_BUTTON",
         value: "subscribe-success",
         id: "NEWTAB_FOOTER_BAR_CONTENT"
       });
     } else {
-      console.error("There was a problem submitting the form", json || "[No JSON response]"); // eslint-disable-line no-console
-
+      // eslint-disable-next-line no-console
+      console.error("There was a problem submitting the form", json || "[No JSON response]");
       this.setState({
         signupSuccess: false,
         signupSubmitted: true
       });
       this.props.sendUserActionTelemetry({
         event: "CLICK_BUTTON",
         value: "subscribe-error",
         id: "NEWTAB_FOOTER_BAR_CONTENT"
@@ -10542,21 +10597,21 @@ class SubmitFormSnippet_SubmitFormSnippe
     return external_React_default.a.createElement(SnippetBase_SnippetBase, SubmitFormSnippet_extends({}, this.props, {
       className: containerClass,
       footerDismiss: true
     }), content.scene2_icon ? external_React_default.a.createElement("div", {
       className: "scene2Icon"
     }, external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(content.scene2_icon),
       className: "icon-light-theme",
-      alt: SubmitFormSnippet_ICON_ALT_TEXT
+      alt: content.scene2_icon_alt_text || SubmitFormSnippet_ICON_ALT_TEXT
     }), external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(content.scene2_icon_dark_theme || content.scene2_icon),
       className: "icon-dark-theme",
-      alt: SubmitFormSnippet_ICON_ALT_TEXT
+      alt: content.scene2_icon_alt_text || SubmitFormSnippet_ICON_ALT_TEXT
     })) : null, external_React_default.a.createElement("div", {
       className: "message"
     }, external_React_default.a.createElement("p", null, content.scene2_title && external_React_default.a.createElement("h3", {
       className: "scene2Title"
     }, content.scene2_title), " ", content.scene2_text && external_React_default.a.createElement(RichText["RichText"], {
       scene2_text: content.scene2_text,
       localization_id: "scene2_text"
     }))), external_React_default.a.createElement("form", {
@@ -10776,17 +10831,17 @@ const SendToDeviceSnippet = props => {
 };
 // CONCATENATED MODULE: ./content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
 function SimpleBelowSearchSnippet_extends() { SimpleBelowSearchSnippet_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; }; return SimpleBelowSearchSnippet_extends.apply(this, arguments); }
 
 
 
 
 
-const SimpleBelowSearchSnippet_DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png"; // Alt text if available; in the future this should come from the server. See bug 1551711
+const SimpleBelowSearchSnippet_DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png"; // Alt text placeholder in case the prop from the server isn't available
 
 const SimpleBelowSearchSnippet_ICON_ALT_TEXT = "";
 class SimpleBelowSearchSnippet_SimpleBelowSearchSnippet extends external_React_default.a.PureComponent {
   renderText() {
     const {
       props
     } = this;
     return external_React_default.a.createElement(RichText["RichText"], {
@@ -10809,21 +10864,21 @@ class SimpleBelowSearchSnippet_SimpleBel
     }
 
     return external_React_default.a.createElement(SnippetBase_SnippetBase, SimpleBelowSearchSnippet_extends({}, props, {
       className: className,
       textStyle: this.props.textStyle
     }), external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon) || SimpleBelowSearchSnippet_DEFAULT_ICON_PATH,
       className: "icon icon-light-theme",
-      alt: SimpleBelowSearchSnippet_ICON_ALT_TEXT
+      alt: props.content.icon_alt_text || SimpleBelowSearchSnippet_ICON_ALT_TEXT
     }), external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon_dark_theme || props.content.icon) || SimpleBelowSearchSnippet_DEFAULT_ICON_PATH,
       className: "icon icon-dark-theme",
-      alt: SimpleBelowSearchSnippet_ICON_ALT_TEXT
+      alt: props.content.icon_alt_text || SimpleBelowSearchSnippet_ICON_ALT_TEXT
     }), external_React_default.a.createElement("div", null, external_React_default.a.createElement("p", {
       className: "body"
     }, this.renderText()), this.props.extraContent));
   }
 
 }
 // CONCATENATED MODULE: ./content-src/asrouter/templates/template-manifest.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SnippetsTemplates", function() { return SnippetsTemplates; });
@@ -13157,17 +13212,17 @@ var link_menu_options = __webpack_requir
 // EXTERNAL MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx
 var LinkMenu = __webpack_require__(29);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(10);
 var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
 
 // EXTERNAL MODULE: ./content-src/lib/screenshot-utils.js
-var screenshot_utils = __webpack_require__(44);
+var screenshot_utils = __webpack_require__(39);
 
 // CONCATENATED MODULE: ./content-src/components/Card/Card.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Card", function() { return Card_Card; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Card", function() { return Card; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PlaceholderCard", function() { return PlaceholderCard; });
 
 
 
@@ -13520,17 +13575,17 @@ var A11yLinkButton = __webpack_require__
 // EXTERNAL MODULE: external "ReactIntl"
 var external_ReactIntl_ = __webpack_require__(4);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(10);
 var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
 
 // EXTERNAL MODULE: ./content-src/components/TopSites/TopSitesConstants.js
-var TopSitesConstants = __webpack_require__(39);
+var TopSitesConstants = __webpack_require__(46);
 
 // CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx
 
 
 class TopSiteFormInput_TopSiteFormInput extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
@@ -13636,17 +13691,17 @@ class TopSiteFormInput_TopSiteFormInput 
 
 }
 TopSiteFormInput_TopSiteFormInput.defaultProps = {
   showClearButton: false,
   value: "",
   validationError: false
 };
 // EXTERNAL MODULE: ./content-src/components/TopSites/TopSite.jsx
-var TopSite = __webpack_require__(43);
+var TopSite = __webpack_require__(48);
 
 // CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteForm.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteForm", function() { return TopSiteForm_TopSiteForm; });
 
 
 
 
 
--- a/browser/components/newtab/karma.mc.config.js
+++ b/browser/components/newtab/karma.mc.config.js
@@ -70,20 +70,20 @@ module.exports = function(config) {
             },
             "lib/*.jsm": {
               statements: 100,
               lines: 100,
               functions: 100,
               branches: 84,
             },
             "content-src/components/DiscoveryStreamComponents/**/*.jsx": {
-              statements: 65.2,
-              lines: 65.2,
-              functions: 50,
-              branches: 50,
+              statements: 90.48,
+              lines: 90.48,
+              functions: 85.71,
+              branches: 68.75,
             },
             "content-src/asrouter/**/*.jsx": {
               statements: 57,
               lines: 58,
               functions: 60,
               branches: 50,
             },
             "content-src/components/ASRouterAdmin/*.jsx": {
--- a/browser/components/newtab/lib/AboutPreferences.jsm
+++ b/browser/components/newtab/lib/AboutPreferences.jsm
@@ -95,20 +95,16 @@ this.AboutPreferences = class AboutPrefe
         break;
     }
   }
 
   handleDiscoverySettings(sections) {
     // Deep copy object to not modify original Sections state in store
     let sectionsCopy = JSON.parse(JSON.stringify(sections));
     sectionsCopy.forEach(obj => {
-      if (obj.id === "highlights") {
-        obj.shouldHidePref = true;
-      }
-
       if (obj.id === "topstories") {
         obj.rowsPref = "";
       }
     });
     return sectionsCopy;
   }
 
   async observe(window) {
@@ -268,22 +264,35 @@ this.AboutPreferences = class AboutPrefe
             const item = createAppend("menuitem", menupopup);
             item.setAttribute("label", PluralForm.get(num, plurals));
             item.setAttribute("value", num);
           }
           linkPref(menulist, rowsPref, "int");
         }
       }
 
+      const subChecks = [];
+      const fullName = `browser.newtabpage.activity-stream.${sectionData.pref.feed}`;
+      const pref = Preferences.get(fullName);
+
       // Add a checkbox pref for any nested preferences
       nestedPrefs.forEach(nested => {
         const subcheck = createAppend("checkbox", detailVbox);
         subcheck.classList.add("indent");
         subcheck.setAttribute("label", formatString(nested.titleString));
         linkPref(subcheck, nested.name, "bool");
+        subChecks.push(subcheck);
+        subcheck.disabled = !pref._value;
+      });
+
+      // Disable any nested checkboxes if the parent pref is not enabled.
+      pref.on("change", () => {
+        subChecks.forEach(subcheck => {
+          subcheck.disabled = !pref._value;
+        });
       });
     });
 
     // Update the visibility of the Restore Defaults btn based on checked prefs
     gHomePane.toggleRestoreDefaultsBtn();
   }
 };
 
--- a/browser/components/newtab/lib/ActivityStream.jsm
+++ b/browser/components/newtab/lib/ActivityStream.jsm
@@ -140,17 +140,17 @@ const PREFS_CONFIG = new Map([
     value: true,
   }],
   ["section.highlights.includeDownloads", {
     title: "Boolean flag that decides whether or not to show saved recent Downloads in highlights.",
     value: true,
   }],
   ["section.highlights.rows", {
     title: "Number of rows of Highlights to display",
-    value: 2,
+    value: 1,
   }],
   ["section.topstories.rows", {
     title: "Number of rows of Top Stories to display",
     value: 1,
   }],
   ["sectionOrder", {
     title: "The rendering order for the sections",
     value: "topsites,topstories,highlights",
@@ -242,16 +242,20 @@ const PREFS_CONFIG = new Map([
     title: "Endpoint prefixes (comma-separated) that are allowed to be requested",
     value: "https://getpocket.cdn.mozilla.net/",
   }],
   ["discoverystream.spoc.impressions", {
     title: "Track spoc impressions",
     skipBroadcast: true,
     value: "{}",
   }],
+  ["discoverystream.endpointSpocsClear", {
+    title: "Endpoint for when a user opts-out of sponsored content to delete the user's data from the ad server.",
+    value: "",
+  }],
   ["discoverystream.rec.impressions", {
     title: "Track rec impressions",
     skipBroadcast: true,
     value: "{}",
   }],
 ]);
 
 // Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
--- a/browser/components/newtab/lib/CFRPageActions.jsm
+++ b/browser/components/newtab/lib/CFRPageActions.jsm
@@ -368,17 +368,18 @@ class PageAction {
       panelTitle = await this.getStrings(content.addon.title);
       options = {popupIconURL: content.addon.icon};
 
       footerLink.value = await this.getStrings({string_id: "cfr-doorhanger-extension-learn-more-link"});
       footerLink.setAttribute("href", content.addon.amo_url);
       footerLink.onclick = () => this._sendTelemetry({message_id: id, bucket_id: content.bucket_id, event: "LEARN_MORE"});
 
       primaryActionCallback = async () => {
-        primary.action.data.url = await CFRPageActions._fetchLatestAddonVersion(content.addon.id); // eslint-disable-line no-use-before-define
+        // eslint-disable-next-line no-use-before-define
+        primary.action.data.url = await CFRPageActions._fetchLatestAddonVersion(content.addon.id);
         this._blockMessage(id);
         this.dispatchUserAction(primary.action);
         this.hideAddressBarNotifier();
         this._sendTelemetry({message_id: id, bucket_id: content.bucket_id, event: "INSTALL"});
         RecommendationMap.delete(browser);
       };
     } else {
       const stepsContainerId = "cfr-notification-feature-steps";
--- a/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
+++ b/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
@@ -8,17 +8,16 @@ const {NewTabUtils} = ChromeUtils.import
 const {setTimeout, clearTimeout} = ChromeUtils.import("resource://gre/modules/Timer.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 ChromeUtils.defineModuleGetter(this, "perfService", "resource://activity-stream/common/PerfService.jsm");
 const {UserDomainAffinityProvider} = ChromeUtils.import("resource://activity-stream/lib/UserDomainAffinityProvider.jsm");
 
 const {actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
 const {PersistentCache} = ChromeUtils.import("resource://activity-stream/lib/PersistentCache.jsm");
-const {PREF_IMPRESSION_ID} = ChromeUtils.import("resource://activity-stream/lib/TelemetryFeed.jsm");
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   gUUIDGenerator: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
 });
 
 const CACHE_KEY = "discovery_stream";
 const LAYOUT_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const STARTUP_CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week
@@ -26,16 +25,19 @@ const COMPONENT_FEEDS_UPDATE_TIME = 30 *
 const SPOCS_FEEDS_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour
 const MIN_DOMAIN_AFFINITIES_UPDATE_TIME = 12 * 60 * 60 * 1000; // 12 hours
 const MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server
 const DEFAULT_MAX_HISTORY_QUERY_RESULTS = 1000;
 const FETCH_TIMEOUT = 45 * 1000;
 const PREF_CONFIG = "discoverystream.config";
 const PREF_ENDPOINTS = "discoverystream.endpoints";
+const PREF_IMPRESSION_ID = "browser.newtabpage.activity-stream.impressionId";
+const PREF_TOPSTORIES = "feeds.section.topstories";
+const PREF_SPOCS_CLEAR_ENDPOINT = "discoverystream.endpointSpocsClear";
 const PREF_SHOW_SPONSORED = "showSponsored";
 const PREF_SPOC_IMPRESSIONS = "discoverystream.spoc.impressions";
 const PREF_REC_IMPRESSIONS = "discoverystream.rec.impressions";
 
 let defaultLayoutResp;
 
 this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
   constructor() {
@@ -450,16 +452,33 @@ this.DiscoveryStreamFeed = class Discove
       data: {
         lastUpdated: spocs.lastUpdated,
         spocs: newSpocs,
       },
     });
     this._sendSpocsFill({...filtered, frequency_cap: frequencyCapped}, true);
   }
 
+  async clearSpocs() {
+    const endpoint = this.store.getState().Prefs.values[PREF_SPOCS_CLEAR_ENDPOINT];
+    if (!endpoint) {
+      return;
+    }
+    const headers = new Headers();
+    headers.append("content-type", "application/json");
+
+    await this.fetchFromEndpoint(endpoint, {
+      method: "DELETE",
+      headers,
+      body: JSON.stringify({
+        pocket_id: this._impressionId,
+      }),
+    });
+  }
+
   async loadAffinityScoresCache() {
     const cachedData = await this.cache.get() || {};
     const {affinities} = cachedData;
     if (this.personalized && affinities && affinities.scores) {
       this.affinityProvider = new UserDomainAffinityProvider(
         affinities.timeSegments,
         affinities.parameterSets,
         affinities.maxHistoryQueryResults,
@@ -1028,18 +1047,28 @@ this.DiscoveryStreamFeed = class Discove
           case PREF_CONFIG:
             // Clear the cached config and broadcast the newly computed value
             this._prefCache.config = null;
             this.store.dispatch(ac.BroadcastToContent({
               type: at.DISCOVERY_STREAM_CONFIG_CHANGE,
               data: this.config,
             }));
             break;
+          case PREF_TOPSTORIES:
+            if (!action.data.value) {
+              // Ensure we delete any remote data potentially related to spocs.
+              this.clearSpocs();
+            }
+            break;
           // Check if spocs was disabled. Remove them if they were.
           case PREF_SHOW_SPONSORED:
+            if (!action.data.value) {
+              // Ensure we delete any remote data potentially related to spocs.
+              this.clearSpocs();
+            }
             await this.loadSpocs(update => this.store.dispatch(ac.BroadcastToContent(update)));
             break;
         }
         break;
     }
   }
 };
 
--- a/browser/components/newtab/lib/PlacesFeed.jsm
+++ b/browser/components/newtab/lib/PlacesFeed.jsm
@@ -85,17 +85,18 @@ class BookmarksObserver extends Observer
    * @param  {str} id
    * @param  {str} folderId
    * @param  {int} index
    * @param  {int} type       Indicates if the bookmark is an actual bookmark,
    *                          a folder, or a separator.
    * @param  {str} uri
    * @param  {str} guid      The unique id of the bookmark
    */
-  onItemRemoved(id, folderId, index, type, uri, guid, parentGuid, source) { // eslint-disable-line max-params
+  // eslint-disable-next-line max-params
+  onItemRemoved(id, folderId, index, type, uri, guid, parentGuid, source) {
     if (type === PlacesUtils.bookmarks.TYPE_BOOKMARK &&
         source !== PlacesUtils.bookmarks.SOURCES.IMPORT &&
         source !== PlacesUtils.bookmarks.SOURCES.RESTORE &&
         source !== PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP &&
         source !== PlacesUtils.bookmarks.SOURCES.SYNC) {
       this.dispatch({type: at.PLACES_LINKS_CHANGED});
       this.dispatch({
         type: at.PLACES_BOOKMARK_REMOVED,
--- a/browser/components/newtab/lib/RecipeExecutor.jsm
+++ b/browser/components/newtab/lib/RecipeExecutor.jsm
@@ -209,18 +209,19 @@ this.RecipeExecutor = class RecipeExecut
       rhs = config.rhsValue;
     } else if (("rhsField" in config) && (config.rhsField in item)) {
       rhs = item[config.rhsField];
     }
     if (rhs === null) {
       return null;
     }
 
+    if (
     // eslint-disable-next-line eqeqeq
-    if (((config.op === "==") && (item[config.field] == rhs)) ||
+      ((config.op === "==") && (item[config.field] == rhs)) ||
         // eslint-disable-next-line eqeqeq
         ((config.op === "!=") && (item[config.field] != rhs)) ||
         ((config.op === "<") && (item[config.field] < rhs)) ||
         ((config.op === "<=") && (item[config.field] <= rhs)) ||
         ((config.op === ">") && (item[config.field] > rhs)) ||
         ((config.op === ">=") && (item[config.field] >= rhs))) {
       return item;
     }
--- a/browser/components/newtab/lib/TopStoriesFeed.jsm
+++ b/browser/components/newtab/lib/TopStoriesFeed.jsm
@@ -31,17 +31,17 @@ const REC_IMPRESSION_TRACKING_PREF = "fe
 const OPTIONS_PREF = "feeds.section.topstories.options";
 const MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server
 const DISCOVERY_STREAM_PREF = "discoverystream.config";
 
 this.TopStoriesFeed = class TopStoriesFeed {
   constructor(ds) {
     // Use discoverystream config pref default values for fast path and
     // if needed lazy load activity stream top stories feed based on
-    // actual user preference when PREFS_INITIAL_VALUES and PREF_CHANGED is invoked
+    // actual user preference when INIT and PREF_CHANGED is invoked
     this.discoveryStreamEnabled = ds && ds.value && JSON.parse(ds.value).enabled;
     if (!this.discoveryStreamEnabled) {
       this.initializeProperties();
     }
   }
 
   initializeProperties() {
     this.contentUpdateQueue = [];
@@ -660,18 +660,23 @@ this.TopStoriesFeed = class TopStoriesFe
         };
       }
       return true;
     }
     return false;
   }
 
   lazyLoadTopStories(dsPref) {
+    let _dsPref = dsPref;
+    if (!_dsPref) {
+      _dsPref = this.store.getState().Prefs.values[DISCOVERY_STREAM_PREF];
+    }
+
     try {
-      this.discoveryStreamEnabled = JSON.parse(dsPref).enabled;
+      this.discoveryStreamEnabled = JSON.parse(_dsPref).enabled;
     } catch (e) {
       // Load activity stream top stories if fail to determine discovery stream state
       this.discoveryStreamEnabled = false;
     }
 
     // Return without invoking initialization if top stories are loaded
     if (this.storiesLoaded) {
       return;
@@ -680,18 +685,18 @@ this.TopStoriesFeed = class TopStoriesFe
     if (!this.discoveryStreamEnabled && !this.propertiesInitialized) {
       this.initializeProperties();
     }
     this.init();
   }
 
   handleDisabled(action) {
     switch (action.type) {
-      case at.PREFS_INITIAL_VALUES:
-        this.lazyLoadTopStories(action.data[DISCOVERY_STREAM_PREF]);
+      case at.INIT:
+        this.lazyLoadTopStories();
         break;
       case at.PREF_CHANGED:
         if (action.data.name === DISCOVERY_STREAM_PREF) {
           this.lazyLoadTopStories(action.data.value);
         }
         break;
       case at.UNINIT:
         this.uninit();
@@ -700,21 +705,19 @@ this.TopStoriesFeed = class TopStoriesFe
   }
 
   async onAction(action) {
     if (this.discoveryStreamEnabled) {
       this.handleDisabled(action);
       return;
     }
     switch (action.type) {
-      // Check for pref initial values to lazy load activity stream top stories
-      // Here we are not using usual INIT and relying on PREFS_INITIAL_VALUES
-      // to check discoverystream pref and load activity stream top stories only if needed.
-      case at.PREFS_INITIAL_VALUES:
-        this.lazyLoadTopStories(action.data[DISCOVERY_STREAM_PREF]);
+      // Check discoverystream pref and load activity stream top stories only if needed
+      case at.INIT:
+        this.lazyLoadTopStories();
         break;
       case at.SYSTEM_TICK:
         let stories;
         let topics;
         if (Date.now() - this.storiesLastUpdated >= STORIES_UPDATE_TIME) {
           stories = await this.fetchStories();
         }
         if (Date.now() - this.topicsLastUpdated >= TOPICS_UPDATE_TIME) {
--- a/browser/components/newtab/locales-src/kab/strings.properties
+++ b/browser/components/newtab/locales-src/kab/strings.properties
@@ -25,17 +25,17 @@ type_label_pocket=Yettwakles ɣer Pocket
 type_label_downloaded=Yuli-d
 
 # LOCALIZATION NOTE (menu_action_*): These strings are displayed in a context
 # menu and are meant as a call to action for a given page.
 # LOCALIZATION NOTE (menu_action_bookmark): Bookmark is a verb, as in "Add to
 # bookmarks"
 menu_action_bookmark=Creḍ asebter-agi
 menu_action_remove_bookmark=Kkes tacreṭ-agi
-menu_action_open_new_window=Ldei deg usfaylu amaynut
+menu_action_open_new_window=Ldi deg usfaylu amaynut
 menu_action_open_private_window=Ldi deg usfaylu uslig amaynut
 menu_action_dismiss=Kkes
 menu_action_delete=Kkes seg umazray
 menu_action_pin=Senteḍ
 menu_action_unpin=Serreḥ
 confirm_history_delete_p1=Tebɣiḍ ad tekksed yal tummant n usebter-agi seg umazray-ik?
 # LOCALIZATION NOTE (confirm_history_delete_notice_p2): this string is displayed in
 # the same dialog as confirm_history_delete_p1. "This action" refers to deleting a
@@ -88,16 +88,17 @@ section_disclaimer_topstories_buttontext
 # for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
 # in English, while "Home" should be localized matching the about:preferences
 # sidebar mozilla-central string for the panel that has preferences related to
 # what is shown for the homepage, new windows, and new tabs.
 prefs_home_header=Agbur agejdan Firefox
 prefs_home_description=Fren agbur i tebɣiḍ deg ugdil agejdan Firefox.
 
 prefs_content_discovery_header=Asebter agejdan Firefox
+
 prefs_content_discovery_button=Sens asnirem n ubur
 
 # LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
 # plural forms used in a drop down of multiple row options (1 row, 2 rows).
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 prefs_section_rows_option={num} izirig;{num} izirigen
 prefs_search_header=Anadi Web
 prefs_topsites_description=Ismal i tettwaliḍ aṭas
--- a/browser/components/newtab/locales-src/ro/strings.properties
+++ b/browser/components/newtab/locales-src/ro/strings.properties
@@ -200,17 +200,17 @@ firstrun_learn_more_link=Află mai multe despre Conturi Firefox
 firstrun_form_header=Introdu e-mailul tău
 firstrun_form_sub_header=pentru a continua la Firefox Sync.
 
 firstrun_email_input_placeholder=E-mail
 firstrun_invalid_input=Necesită o adresă de e-mail validă
 
 # LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
 # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
-firstrun_extra_legal_links=Prin continuare, ești de acord cu {terms} și {privacy}.
+firstrun_extra_legal_links=Continuând, ești de acord cu {terms} și {privacy}.
 firstrun_terms_of_service=Termenii de utilizare a serviciului
 firstrun_privacy_notice=Declarație de confidențialitate
 
 firstrun_continue_to_login=Continuă
 firstrun_skip_login=Omite acest pas
 
 # LOCALIZATION NOTE (context_menu_title): Action tooltip to open a context menu
 context_menu_title=Deschide meniul
--- a/browser/components/newtab/package-lock.json
+++ b/browser/components/newtab/package-lock.json
@@ -3001,16 +3001,25 @@
           "dev": true,
           "requires": {
             "argparse": "^1.0.7",
             "esprima": "^4.0.0"
           }
         }
       }
     },
+    "eslint-config-prettier": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.2.0.tgz",
+      "integrity": "sha512-y0uWc/FRfrHhpPZCYflWC8aE0KRJRY04rdZVfl8cL3sEZmOYyaBdhdlQPjKZBnuRMyLVK+JUZr7HaZFClQiH4w==",
+      "dev": true,
+      "requires": {
+        "get-stdin": "^6.0.0"
+      }
+    },
     "eslint-import-resolver-node": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz",
       "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==",
       "dev": true,
       "requires": {
         "debug": "^2.6.9",
         "resolve": "^1.5.0"
@@ -4740,16 +4749,22 @@
       "dev": true
     },
     "get-func-name": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
       "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
       "dev": true
     },
+    "get-stdin": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
+      "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
+      "dev": true
+    },
     "get-value": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
       "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
       "dev": true
     },
     "getpass": {
       "version": "0.1.7",
--- a/browser/components/newtab/package.json
+++ b/browser/components/newtab/package.json
@@ -26,16 +26,17 @@
     "babel-plugin-jsm-to-commonjs": "0.5.0",
     "babel-plugin-jsm-to-esmodules": "0.6.0",
     "chai": "4.2.0",
     "chai-json-schema": "1.5.1",
     "cpx": "1.5.0",
     "enzyme": "3.9.0",
     "enzyme-adapter-react-16": "1.13.2",
     "eslint": "5.16.0",
+    "eslint-config-prettier": "4.2.0",
     "eslint-plugin-fetch-options": "0.0.5",
     "eslint-plugin-import": "2.17.3",
     "eslint-plugin-jsx-a11y": "6.2.1",
     "eslint-plugin-mozilla": "1.3.0",
     "eslint-plugin-no-unsanitized": "3.0.2",
     "eslint-plugin-prettier": "3.0.1",
     "eslint-plugin-react": "7.13.0",
     "eslint-plugin-react-hooks": "1.6.0",
@@ -125,16 +126,17 @@
     "importmc": "rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/components/newtab/ .",
     "testmc": "npm-run-all testmc:*",
     "testmc:lint": "npm run lint",
     "testmc:build": "npm run bundle:webpack && npm run bundle:locales",
     "testmc:unit": "karma start karma.mc.config.js",
     "tddmc": "karma start karma.mc.config.js --tdd",
     "debugcoverage": "open logs/coverage/index.html",
     "lint": "npm-run-all lint:*",
+    "lint:eslint-check": "eslint --print-config . | eslint-config-prettier-check",
     "lint:eslint": "eslint --ext=.js,.jsm,.jsx .",
     "lint:sasslint": "sass-lint -v -q",
     "strings-import": "node ./bin/strings-import.js",
     "test": "npm run testmc",
     "tdd": "npm run tddmc",
     "vendor": "npm-run-all vendor:*",
     "vendor:react": "node ./bin/vendor-react.js",
     "help": "yamscripts help",
--- a/browser/components/newtab/prerendered/locales/kab/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/kab/activity-stream-strings.js
@@ -8,17 +8,17 @@ window.gActivityStreamStrings = {
   "section_context_menu_button_sr": "Ldi umuɣ n usatal n tgezmi",
   "type_label_visited": "Yettwarza",
   "type_label_bookmarked": "Yettwacreḍ",
   "type_label_recommended": "Tiddin",
   "type_label_pocket": "Yettwakles ɣer Pocket",
   "type_label_downloaded": "Yuli-d",
   "menu_action_bookmark": "Creḍ asebter-agi",
   "menu_action_remove_bookmark": "Kkes tacreṭ-agi",
-  "menu_action_open_new_window": "Ldei deg usfaylu amaynut",
+  "menu_action_open_new_window": "Ldi deg usfaylu amaynut",
   "menu_action_open_private_window": "Ldi deg usfaylu uslig amaynut",
   "menu_action_dismiss": "Kkes",
   "menu_action_delete": "Kkes seg umazray",
   "menu_action_pin": "Senteḍ",
   "menu_action_unpin": "Serreḥ",
   "confirm_history_delete_p1": "Tebɣiḍ ad tekksed yal tummant n usebter-agi seg umazray-ik?",
   "confirm_history_delete_notice_p2": "Tigawt-agi ur tettuɣal ara ar deffir.",
   "menu_action_save_to_pocket": "Sekles ɣer Pocket",
--- a/browser/components/newtab/prerendered/locales/ro/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/ro/activity-stream-strings.js
@@ -95,15 +95,15 @@ window.gActivityStreamStrings = {
   "section_menu_action_privacy_notice": "Declarație de confidențialitate",
   "firstrun_title": "Ia Firefox cu tine",
   "firstrun_content": "Ia marcajele, istoricul, parolele și alte setări cu tine pe toate dispozitivele.",
   "firstrun_learn_more_link": "Află mai multe despre Conturi Firefox",
   "firstrun_form_header": "Introdu e-mailul tău",
   "firstrun_form_sub_header": "pentru a continua la Firefox Sync.",
   "firstrun_email_input_placeholder": "E-mail",
   "firstrun_invalid_input": "Necesită o adresă de e-mail validă",
-  "firstrun_extra_legal_links": "Prin continuare, ești de acord cu {terms} și {privacy}.",
+  "firstrun_extra_legal_links": "Continuând, ești de acord cu {terms} și {privacy}.",
   "firstrun_terms_of_service": "Termenii de utilizare a serviciului",
   "firstrun_privacy_notice": "Declarație de confidențialitate",
   "firstrun_continue_to_login": "Continuă",
   "firstrun_skip_login": "Omite acest pas",
   "context_menu_title": "Deschide meniul"
 };
--- a/browser/components/newtab/test/browser/browser_activity_stream_strings.js
+++ b/browser/components/newtab/test/browser/browser_activity_stream_strings.js
@@ -4,14 +4,16 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 const LOCALE_PATH = "resource://activity-stream/prerendered/";
 
 /**
  * Test current locale strings can be fetched the way activity stream does it.
  */
 add_task(async function test_activity_stream_fetch_strings() {
   const file = `${LOCALE_PATH}${aboutNewTabService.activityStreamLocale}/activity-stream-strings.js`;
-  const strings = JSON.parse((await (await fetch(file)).text()).match(/{[^]*}/)[0]); // eslint-disable-line fetch-options/no-fetch-credentials
+  const strings = JSON.parse((await
+    // eslint-disable-next-line fetch-options/no-fetch-credentials
+    (await fetch(file)).text()).match(/{[^]*}/)[0]);
   const ids = Object.keys(strings);
 
   info(`Got string ids: ${ids}`);
   Assert.ok(ids.length, "Localized strings are available");
 });
--- a/browser/components/newtab/test/browser/browser_highlights_section.js
+++ b/browser/components/newtab/test/browser/browser_highlights_section.js
@@ -26,17 +26,17 @@ function test_highlights(bookmarkCount, 
 
 test_highlights(
   2, // Number of highlights cards
   function check_highlights_cards() {
     let found = content.document.querySelectorAll(".card-outer:not(.placeholder)").length;
     is(found, 2, "there should be 2 highlights cards");
 
     found = content.document.querySelectorAll(".section-list .placeholder").length;
-    is(found, 6, "there should be 2 rows * 4 - 2 = 6 highlights placeholder");
+    is(found, 2, "there should be 1 row * 4 - 2 = 2 highlights placeholder");
 
     found = content.document.querySelectorAll(".card-context-icon.icon-bookmark-added").length;
     is(found, 2, "there should be 2 bookmark icons");
   }
 );
 
 test_highlights(
   1, // Number of highlights cards
--- a/browser/components/newtab/test/browser/head.js
+++ b/browser/components/newtab/test/browser/head.js
@@ -7,27 +7,29 @@ ChromeUtils.defineModuleGetter(this, "Qu
 
 function popPrefs() {
   return SpecialPowers.popPrefEnv();
 }
 function pushPrefs(...prefs) {
   return SpecialPowers.pushPrefEnv({set: prefs});
 }
 
-async function setDefaultTopSites() { // eslint-disable-line no-unused-vars
+// eslint-disable-next-line no-unused-vars
+async function setDefaultTopSites() {
   // The pref for TopSites is empty by default.
   await pushPrefs(["browser.newtabpage.activity-stream.default.sites",
     "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"]);
   // Toggle the feed off and on as a workaround to read the new prefs.
   await pushPrefs(["browser.newtabpage.activity-stream.feeds.topsites", false]);
   await pushPrefs(["browser.newtabpage.activity-stream.feeds.topsites", true]);
   await pushPrefs(["browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", true]);
 }
 
-async function clearHistoryAndBookmarks() { // eslint-disable-line no-unused-vars
+// eslint-disable-next-line no-unused-vars
+async function clearHistoryAndBookmarks() {
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesUtils.history.clear();
   QueryCache.expireAll();
 }
 
 /**
  * Helper to wait for potentially preloaded browsers to "load" where a preloaded
  * page has already loaded and won't trigger "load", and a "load"ed page might
@@ -48,17 +50,18 @@ function refreshHighlightsFeed() {
   Services.prefs.setBoolPref("browser.newtabpage.activity-stream.feeds.section.highlights", false);
   Services.prefs.setBoolPref("browser.newtabpage.activity-stream.feeds.section.highlights", true);
 }
 
 /**
  * Helper to populate the Highlights section with bookmark cards.
  * @param count Number of items to add.
  */
-async function addHighlightsBookmarks(count) { // eslint-disable-line no-unused-vars
+// eslint-disable-next-line no-unused-vars
+async function addHighlightsBookmarks(count) {
   const bookmarks = new Array(count).fill(null).map((entry, i) => ({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     title: "foo",
     url: `https://mozilla${i}.com/nowNew`,
   }));
 
   for (let placeInfo of bookmarks) {
     await PlacesUtils.bookmarks.insert(placeInfo);
@@ -102,17 +105,18 @@ function addContentHelpers() {
  *   {Function} This parameter will be used as if the function were called with
  *              an Object with this parameter as "test" key's value.
  *   {Object} The following keys are expected:
  *     before {Function} Optional. Runs before and returns an arg for "test"
  *     test   {Function} The test to run in the about:newtab content task taking
  *                       an arg from "before" and returns a result to "after"
  *     after  {Function} Optional. Runs after and with the result of "test"
  */
-function test_newtab(testInfo) { // eslint-disable-line no-unused-vars
+// eslint-disable-next-line no-unused-vars
+function test_newtab(testInfo) {
   // Extract any test parts or default to just the single content task
   let {before, test: contentTask, after} = testInfo;
   if (!before) {
     before = () => ({});
   }
   if (!contentTask) {
     contentTask = testInfo;
   }
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -1372,20 +1372,22 @@ describe("ASRouter", () => {
 
         const message = {id: "foo", provider: "bar", frequency: {lifetime: 3}};
         const provider = {id: "bar", frequency: {lifetime: 5}};
 
         await Router.setState(state => {
           // Add provider
           const providers = [...state.providers, provider];
           // Add fooMessageImpressions
-          const messageImpressions = Object.assign({}, state.messageImpressions); // eslint-disable-line no-shadow
+          // eslint-disable-next-line no-shadow
+          const messageImpressions = Object.assign({}, state.messageImpressions);
           messageImpressions.foo = fooMessageImpressions;
           // Add barProviderImpressions
-          const providerImpressions = Object.assign({}, state.providerImpressions); // eslint-disable-line no-shadow
+          // eslint-disable-next-line no-shadow
+          const providerImpressions = Object.assign({}, state.providerImpressions);
           providerImpressions.bar = barProviderImpressions;
           return {providers, messageImpressions, providerImpressions};
         });
 
         await Router.isBelowFrequencyCaps(message);
 
         assert.calledTwice(Router._isBelowItemFrequencyCap);
         assert.calledWithExactly(Router._isBelowItemFrequencyCap, message, fooMessageImpressions, MAX_MESSAGE_LIFETIME_CAP);
--- a/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
@@ -71,17 +71,18 @@ describe("ASRouterPreferences", () => {
     it("should set ._initialized to false", () => {
       ASRouterPreferences.init();
       ASRouterPreferences.uninit();
       assert.isFalse(ASRouterPreferences._initialized);
     });
     it("should clear cached values for ._initialized, .devtoolsEnabled", () => {
       ASRouterPreferences.init();
       // trigger caching
-      const result = [ASRouterPreferences.providers, ASRouterPreferences.devtoolsEnabled]; // eslint-disable-line no-unused-vars
+      // eslint-disable-next-line no-unused-vars
+      const result = [ASRouterPreferences.providers, ASRouterPreferences.devtoolsEnabled];
       assert.isNotNull(ASRouterPreferences._providers, "providers should not be null");
       assert.isNotNull(ASRouterPreferences._devtoolsEnabled, "devtolosEnabled should not be null");
 
       ASRouterPreferences.uninit();
       assert.isNull(ASRouterPreferences._providers);
       assert.isNull(ASRouterPreferences._devtoolsEnabled);
     });
     it("should clear all listeners and remove observers (only once)", () => {
--- a/browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js
+++ b/browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js
@@ -483,17 +483,18 @@ describe("CFRPageActions", () => {
             event: "INSTALL",
           },
         });
         // Should remove the recommendation
         assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
       });
       it("should set the secondary action correctly", async () => {
         await pageAction._showPopupOnClick();
-        const [secondaryAction] = global.PopupNotifications.show.firstCall.args[5]; // eslint-disable-line prefer-destructuring
+        // eslint-disable-next-line prefer-destructuring
+        const [secondaryAction] = global.PopupNotifications.show.firstCall.args[5];
 
         assert.deepEqual(secondaryAction.label, {value: "Secondary Button", attributes: {accesskey: "s"}});
         sandbox.spy(pageAction, "hideAddressBarNotifier");
         CFRPageActions.RecommendationMap.set(fakeBrowser, {});
         secondaryAction.callback();
         // Should send telemetry
         assert.calledWith(dispatchStub, {
           type: "DOORHANGER_TELEMETRY",
@@ -506,17 +507,18 @@ describe("CFRPageActions", () => {
           },
         });
         // Don't remove the recommendation on `DISMISS` action
         assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
         assert.notCalled(pageAction.hideAddressBarNotifier);
       });
       it("should send right telemetry for BLOCK secondary action", async () => {
         await pageAction._showPopupOnClick();
-        const blockAction = global.PopupNotifications.show.firstCall.args[5][1]; // eslint-disable-line prefer-destructuring
+        // eslint-disable-next-line prefer-destructuring
+        const blockAction = global.PopupNotifications.show.firstCall.args[5][1];
 
         assert.deepEqual(blockAction.label, {value: "Secondary Button 2", attributes: {accesskey: "a"}});
         sandbox.spy(pageAction, "hideAddressBarNotifier");
         sandbox.spy(pageAction, "_blockMessage");
         CFRPageActions.RecommendationMap.set(fakeBrowser, {});
         blockAction.callback();
         assert.calledOnce(pageAction.hideAddressBarNotifier);
         assert.calledOnce(pageAction._blockMessage);
@@ -531,17 +533,18 @@ describe("CFRPageActions", () => {
             event: "BLOCK",
           },
         });
         // Should remove the recommendation
         assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
       });
       it("should send right telemetry for MANAGE secondary action", async () => {
         await pageAction._showPopupOnClick();
-        const manageAction = global.PopupNotifications.show.firstCall.args[5][2]; // eslint-disable-line prefer-destructuring
+        // eslint-disable-next-line prefer-destructuring
+        const manageAction = global.PopupNotifications.show.firstCall.args[5][2];
 
         assert.deepEqual(manageAction.label, {value: "Secondary Button 3", attributes: {accesskey: "g"}});
         sandbox.spy(pageAction, "hideAddressBarNotifier");
         CFRPageActions.RecommendationMap.set(fakeBrowser, {});
         manageAction.callback();
         // Should send telemetry
         assert.calledWith(dispatchStub, {
           type: "DOORHANGER_TELEMETRY",
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
@@ -1,10 +1,10 @@
+import {DSCard, PlaceholderDSCard} from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard";
 import {actionCreators as ac} from "common/Actions.jsm";
-import {DSCard} from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard";
 import {DSLinkMenu} from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu";
 import React from "react";
 import {SafeAnchor} from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
 import {shallowWithIntl} from "test/unit/utils";
 
 describe("<DSCard>", () => {
   let wrapper;
   let sandbox;
@@ -29,16 +29,20 @@ describe("<DSCard>", () => {
     assert.equal(wrapper.children().at(0).type(), SafeAnchor);
     assert.propertyVal(wrapper.children().at(0).props(), "url", "https://foo.com");
   });
 
   it("should pass onLinkClick prop", () => {
     assert.propertyVal(wrapper.children().at(0).props(), "onLinkClick", wrapper.instance().onLinkClick);
   });
 
+  it("should render DSLinkMenu", () => {
+    assert.equal(wrapper.children().at(1).type(), DSLinkMenu);
+  });
+
   describe("onLinkClick", () => {
     let dispatch;
 
     beforeEach(() => {
       dispatch = sandbox.stub();
       wrapper = shallowWithIntl(<DSCard dispatch={dispatch} />);
     });
 
@@ -54,14 +58,67 @@ describe("<DSCard>", () => {
         action_position: 1,
       }));
       assert.calledWith(dispatch, ac.ImpressionStats({
         click: 0,
         source: "FOO",
         tiles: [{id: "fooidx", pos: 1}],
       }));
     });
+
+    it("should call dispatch with a shim", () => {
+      wrapper.setProps({
+        id: "fooidx",
+        pos: 1,
+        type: "foo",
+        shim: {
+          click: "click shim",
+        },
+      });
+
+      wrapper.instance().onLinkClick();
+
+      assert.calledTwice(dispatch);
+      assert.calledWith(dispatch, ac.UserEvent({
+        event: "CLICK",
+        source: "FOO",
+        action_position: 1,
+      }));
+      assert.calledWith(dispatch, ac.ImpressionStats({
+        click: 0,
+        source: "FOO",
+        tiles: [{id: "fooidx", pos: 1, shim: "click shim"}],
+      }));
+    });
+  });
+});
+
+describe("<PlaceholderDSCard> component", () => {
+  it("should have placeholder prop", () => {
+    const wrapper = shallowWithIntl(<PlaceholderDSCard />);
+    const card = wrapper.find(DSCard);
+    assert.lengthOf(card, 1);
+
+    const placeholder = wrapper.find(DSCard).prop("placeholder");
+    assert.isTrue(placeholder);
   });
 
-  it("should render DSLinkMenu", () => {
-    assert.equal(wrapper.children().at(1).type(), DSLinkMenu);
+  it("should contain placeholder div", () => {
+    const wrapper = shallowWithIntl(<DSCard placeholder={true} />);
+    const card = wrapper.find("div.ds-card.placeholder");
+    assert.lengthOf(card, 1);
+  });
+
+  it("should not be clickable", () => {
+    const wrapper = shallowWithIntl(<DSCard placeholder={true} />);
+    const anchor = wrapper.find("SafeAnchor.ds-card-link");
+    assert.lengthOf(anchor, 1);
+
+    const linkClick = anchor.prop("onLinkClick");
+    assert.isUndefined(linkClick);
+  });
+
+  it("should not have context menu", () => {
+    const wrapper = shallowWithIntl(<DSCard placeholder={true} />);
+    const linkMenu = wrapper.find(DSLinkMenu);
+    assert.lengthOf(linkMenu, 0);
   });
 });
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx
@@ -1,39 +1,83 @@
+import {mountWithIntl, shallowWithIntl} from "test/unit/utils";
 import {_DSLinkMenu as DSLinkMenu} from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu";
 import {LinkMenu} from "content-src/components/LinkMenu/LinkMenu";
 import React from "react";
-import {shallowWithIntl} from "test/unit/utils";
 
 describe("<DSLinkMenu>", () => {
-  const ValidDSLinkMenuProps = {
-    site: {},
-  };
   let wrapper;
+  let parentNode;
+
+  describe("DS link menu actions", () => {
+    beforeEach(() => {
+      wrapper = mountWithIntl(<DSLinkMenu />);
+      parentNode = wrapper.getDOMNode().parentNode;
+    });
+
+    afterEach(() => {
+      wrapper.unmount();
+    });
+
+    it("Should remove active on Menu Update", () => {
+      wrapper.setState({showContextMenu: true});
+      // Add active class name to DSLinkMenu parent node
+      // to simulate menu open state
+      parentNode.classList.add("active");
+      assert.equal(parentNode.className, "active");
 
-  beforeEach(() => {
-    wrapper = shallowWithIntl(<DSLinkMenu {...ValidDSLinkMenuProps} />);
-  });
+      wrapper.instance().onMenuUpdate(false);
+      wrapper.update();
+
+      assert.isEmpty(parentNode.className);
+      assert.isFalse(wrapper.state(["showContextMenu"]));
+    });
 
-  it("should render a context menu button", () => {
-    assert.ok(wrapper.exists());
-    assert.ok(wrapper.find(".context-menu-button").exists());
+    it("Should add active on Menu Show", () => {
+      wrapper.instance().onMenuShow();
+      wrapper.update();
+      assert.equal(parentNode.className, "active");
+    });
+
+    it("Should add last-item to support resized window", () => {
+      const fakeWindow = {scrollMaxX: "20"};
+      wrapper = mountWithIntl(<DSLinkMenu windowObj={fakeWindow} />);
+      parentNode = wrapper.getDOMNode().parentNode;
+      wrapper.instance().onMenuShow();
+      wrapper.update();
+      assert.equal(parentNode.className, "last-item active");
+    });
   });
 
-  it("should render LinkMenu when context menu button is clicked", () => {
-    let button = wrapper.find(".context-menu-button");
-    button.simulate("click", {preventDefault: () => {}});
-    assert.equal(wrapper.find(LinkMenu).length, 1);
-  });
+  describe("DS context menu options", () => {
+    const ValidDSLinkMenuProps = {
+      site: {},
+    };
+
+    beforeEach(() => {
+      wrapper = shallowWithIntl(<DSLinkMenu {...ValidDSLinkMenuProps} />);
+    });
+
+    it("should render a context menu button", () => {
+      assert.ok(wrapper.exists());
+      assert.ok(wrapper.find(".context-menu-button").exists());
+    });
 
-  it("should pass dispatch, onUpdate, onShow, site, options, shouldSendImpressionStats, source and index to LinkMenu", () => {
-    wrapper.find(".context-menu-button").simulate("click", {preventDefault: () => {}});
-    const linkMenuProps = wrapper.find(LinkMenu).props();
-    ["dispatch", "onUpdate", "onShow", "site", "index", "options", "source", "shouldSendImpressionStats"].forEach(prop => assert.property(linkMenuProps, prop));
-  });
+    it("should render LinkMenu when context menu button is clicked", () => {
+      let button = wrapper.find(".context-menu-button");
+      button.simulate("click", {preventDefault: () => {}});
+      assert.equal(wrapper.find(LinkMenu).length, 1);
+    });
 
-  it("should pass through the correct menu options to LinkMenu", () => {
-    wrapper.find(".context-menu-button").simulate("click", {preventDefault: () => {}});
-    const linkMenuProps = wrapper.find(LinkMenu).props();
-    assert.deepEqual(linkMenuProps.options,
-      ["CheckBookmarkOrArchive", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"]);
+    it("should pass dispatch, onUpdate, onShow, site, options, shouldSendImpressionStats, source and index to LinkMenu", () => {
+      wrapper.find(".context-menu-button").simulate("click", {preventDefault: () => {}});
+      const linkMenuProps = wrapper.find(LinkMenu).props();
+      ["dispatch", "onUpdate", "onShow", "site", "index", "options", "source", "shouldSendImpressionStats"].forEach(prop => assert.property(linkMenuProps, prop));
+    });
+
+    it("should pass through the correct menu options to LinkMenu", () => {
+      wrapper.find(".context-menu-button").simulate("click", {preventDefault: () => {}});
+      const linkMenuProps = wrapper.find(LinkMenu).props();
+      assert.deepEqual(linkMenuProps.options,
+        ["CheckBookmarkOrArchive", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"]);
+    });
   });
 });
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Hero.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Hero.test.jsx
@@ -1,9 +1,10 @@
 import {DSCard, PlaceholderDSCard} from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard";
+import {actionCreators as ac} from "common/Actions.jsm";
 import {DSEmptyState} from "content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState";
 import {Hero} from "content-src/components/DiscoveryStreamComponents/Hero/Hero";
 import {List} from "content-src/components/DiscoveryStreamComponents/List/List";
 import React from "react";
 import {shallow} from "enzyme";
 
 describe("<Hero>", () => {
   let DEFAULT_PROPS;
@@ -88,9 +89,46 @@ describe("<Hero>", () => {
     });
 
     it("should render list with 1 item for 2 hero items", () => {
       const wrapper = shallow(<Hero {...DEFAULT_PROPS} items={2} />);
 
       assert.equal(wrapper.find(List).prop("items"), 1);
     });
   });
+
+  describe("onLinkClick", () => {
+    let dispatch;
+    let sandbox;
+    let wrapper;
+    const heroProps = {
+      data: {recommendations: [{url: 1, id: "foo-id", pos: 1}]},
+      type: "foo",
+      items: 1,
+    };
+
+    beforeEach(() => {
+      sandbox = sinon.createSandbox();
+      dispatch = sandbox.stub();
+      wrapper = shallow(<Hero dispatch={dispatch} {...heroProps} />);
+    });
+
+    afterEach(() => {
+      sandbox.restore();
+    });
+
+    it("should call dispatch with the correct events", () => {
+      wrapper.instance().onLinkClick();
+
+      assert.calledTwice(dispatch);
+      assert.calledWith(dispatch, ac.UserEvent({
+        event: "CLICK",
+        source: "FOO",
+        action_position: 1,
+      }));
+      assert.calledWith(dispatch, ac.ImpressionStats({
+        click: 0,
+        source: "FOO",
+        tiles: [{id: "foo-id", pos: 1}],
+      }));
+    });
+  });
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx
@@ -0,0 +1,33 @@
+import {combineReducers, createStore} from "redux";
+import {INITIAL_STATE, reducers} from "common/Reducers.jsm";
+import {Highlights} from "content-src/components/DiscoveryStreamComponents/Highlights/Highlights";
+import {mountWithIntl} from "test/unit/utils";
+import {Provider} from "react-redux";
+import React from "react";
+
+describe("Discovery Stream <Highlights>", () => {
+  let wrapper;
+
+  afterEach(() => {
+    wrapper.unmount();
+  });
+
+  it("should render nothing with no highlights data", () => {
+    const store = createStore(combineReducers(reducers), {...INITIAL_STATE});
+
+    wrapper = mountWithIntl(<Provider store={store}><Highlights /></Provider>);
+
+    assert.ok(wrapper.isEmptyRender());
+  });
+
+  it("should render highlights", () => {
+    const store = createStore(combineReducers(reducers), {
+      ...INITIAL_STATE,
+      Sections: [{id: "highlights", enabled: true}],
+    });
+
+    wrapper = mountWithIntl(<Provider store={store}><Highlights /></Provider>);
+
+    assert.lengthOf(wrapper.find(".ds-highlights"), 1);
+  });
+});
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/List.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/List.test.jsx
@@ -1,9 +1,10 @@
 import {_List as List, ListItem, PlaceholderListItem} from "content-src/components/DiscoveryStreamComponents/List/List";
+import {actionCreators as ac} from "common/Actions.jsm";
 import {DSEmptyState} from "content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState";
 import {DSLinkMenu} from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu";
 import {GlobalOverrider} from "test/unit/utils";
 import React from "react";
 import {shallow} from "enzyme";
 
 describe("<List> presentation component", () => {
   const ValidRecommendations = [{url: 1}, {url: 2}, {context: "test spoc", url: 3}];
@@ -139,16 +140,50 @@ describe("<ListItem> presentation compon
   });
 
   it("should contain 'span.ds-list-item-context' spoc element", () => {
     const wrapper = shallow(<ListItem {...ValidLSpocListItemProps} />);
 
     const contextEl = wrapper.find("span.ds-list-item-context");
     assert.lengthOf(contextEl, 1);
   });
+
+  describe("onLinkClick", () => {
+    let dispatch;
+    let sandbox;
+    let wrapper;
+
+    beforeEach(() => {
+      sandbox = sinon.createSandbox();
+      dispatch = sandbox.stub();
+      wrapper = shallow(<ListItem dispatch={dispatch} {...ValidListItemProps} />);
+    });
+
+    afterEach(() => {
+      sandbox.restore();
+    });
+
+    it("should call dispatch with the correct events", () => {
+      wrapper.setProps({id: "foo-id", pos: 1, type: "foo"});
+
+      wrapper.instance().onLinkClick();
+
+      assert.calledTwice(dispatch);
+      assert.calledWith(dispatch, ac.UserEvent({
+        event: "CLICK",
+        source: "FOO",
+        action_position: 1,
+      }));
+      assert.calledWith(dispatch, ac.ImpressionStats({
+        click: 0,
+        source: "FOO",
+        tiles: [{id: "foo-id", pos: 1}],
+      }));
+    });
+  });
 });
 
 describe("<PlaceholderListItem> component", () => {
   it("should have placeholder prop", () => {
     const wrapper = shallow(<PlaceholderListItem />);
     const listItem = wrapper.find(ListItem);
     assert.lengthOf(listItem, 1);
 
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopSites.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopSites.test.jsx
@@ -1,17 +1,47 @@
+import {combineReducers, createStore} from "redux";
+import {INITIAL_STATE, reducers, TOP_SITES_DEFAULT_ROWS} from "common/Reducers.jsm";
+import {mountWithIntl} from "test/unit/utils";
 import {TopSites as OldTopSites} from "content-src/components/TopSites/TopSites";
+import {Provider} from "react-redux";
 import React from "react";
-import {shallow} from "enzyme";
-import {_TopSites as TopSites} from "content-src/components/DiscoveryStreamComponents/TopSites/TopSites";
+import {TopSites} from "content-src/components/DiscoveryStreamComponents/TopSites/TopSites";
 
 describe("Discovery Stream <TopSites>", () => {
+  let wrapper;
+  let store;
+
+  beforeEach(() => {
+    INITIAL_STATE.Prefs.values.topSitesRows = TOP_SITES_DEFAULT_ROWS;
+    store = createStore(combineReducers(reducers), INITIAL_STATE);
+    wrapper = mountWithIntl(<Provider store={store}><TopSites /></Provider>);
+  });
+
+  afterEach(() => {
+    wrapper.unmount();
+  });
+
   it("should return a wrapper around old TopSites", () => {
-    const wrapper = shallow(<TopSites />);
-
     const oldTopSites = wrapper.find(OldTopSites);
     const dsTopSitesWrapper = wrapper.find(".ds-top-sites");
 
     assert.ok(wrapper.exists());
     assert.lengthOf(oldTopSites, 1);
     assert.lengthOf(dsTopSitesWrapper, 1);
   });
+
+  describe("TopSites header", () => {
+    it("should have header title undefined by default", () => {
+      const oldTopSites = wrapper.find(OldTopSites);
+      assert.isUndefined(oldTopSites.props().title);
+    });
+
+    it("should set header title on old TopSites", () => {
+      let DEFAULT_PROPS = {
+        header: {title: "test"},
+      };
+      wrapper = mountWithIntl(<Provider store={store}><TopSites {...DEFAULT_PROPS} /></Provider>);
+      const oldTopSites = wrapper.find(OldTopSites);
+      assert.equal(oldTopSites.props().title, "test");
+    });
+  });
 });
--- a/browser/components/newtab/test/unit/lib/AboutPreferences.test.js
+++ b/browser/components/newtab/test/unit/lib/AboutPreferences.test.js
@@ -78,32 +78,16 @@ describe("AboutPreferences Feed", () => 
 
       await instance.observe(window, PREFERENCES_LOADED_EVENT);
 
       assert.calledOnce(stub);
       assert.equal(stub.firstCall.args[0], window);
       assert.deepEqual(stub.firstCall.args[1], instance._strings);
       assert.include(stub.firstCall.args[2], Sections[0]);
     });
-    it("Hide highlights in sections if discovery stream is enabled", async () => {
-      const stub = sandbox.stub(instance, "renderPreferences");
-      instance._strings = {};
-      const titleString = "title";
-
-      Sections.push({pref: {titleString}, id: "highlights"});
-      DiscoveryStream = {config: {enabled: true}};
-
-      await instance.observe(window, PREFERENCES_LOADED_EVENT);
-
-      assert.calledOnce(stub);
-      assert.equal(stub.firstCall.args[2][0].id, "search");
-      assert.equal(stub.firstCall.args[2][1].id, "topsites");
-      assert.equal(stub.firstCall.args[2][2].id, "highlights");
-      assert.isTrue(stub.firstCall.args[2][2].shouldHidePref);
-    });
     it("Hide topstories rows select in sections if discovery stream is enabled", async () => {
       const stub = sandbox.stub(instance, "renderPreferences");
       instance._strings = {};
 
       Sections.push({rowsPref: "row_pref", maxRows: 3, pref: {descString: "foo"}, learnMore: {link: "https://foo.com"}, id: "topstories"});
       DiscoveryStream = {config: {enabled: true}};
 
       await instance.observe(window, PREFERENCES_LOADED_EVENT);
@@ -188,17 +172,19 @@ describe("AboutPreferences Feed", () => 
         setAttribute: sandbox.stub(),
         remove: sandbox.stub(),
         style: {},
       };
       strings = {};
       prefStructure = [];
       Preferences = {
         add: sandbox.stub(),
-        get: sandbox.stub().returns({}),
+        get: sandbox.stub().returns({
+          on: sandbox.stub(),
+        }),
       };
       gHomePane = {toggleRestoreDefaultsBtn: sandbox.stub()};
     });
     describe("#formatString", () => {
       it("should fall back to string id if missing", () => {
         testRender();
 
         assert.equal(node.textContent, "prefs_home_description");
@@ -232,48 +218,48 @@ describe("AboutPreferences Feed", () => 
 
         testRender();
 
         assert.calledWith(node.setAttribute, "label", "l33t");
       });
     });
     describe("#linkPref", () => {
       it("should add a pref to the global", () => {
-        prefStructure = [{}];
+        prefStructure = [{pref: {feed: "feed"}}];
 
         testRender();
 
         assert.calledOnce(Preferences.add);
       });
       it("should skip adding if not shown", () => {
         prefStructure = [{shouldHidePref: true}];
 
         testRender();
 
         assert.notCalled(Preferences.add);
       });
     });
     describe("pref icon", () => {
       it("should default to webextension icon", () => {
-        prefStructure = [{}];
+        prefStructure = [{pref: {feed: "feed"}}];
 
         testRender();
 
         assert.calledWith(node.setAttribute, "src", "resource://activity-stream/data/content/assets/glyph-webextension-16.svg");
       });
       it("should use desired glyph icon", () => {
-        prefStructure = [{icon: "highlights"}];
+        prefStructure = [{icon: "highlights", pref: {feed: "feed"}}];
 
         testRender();
 
         assert.calledWith(node.setAttribute, "src", "resource://activity-stream/data/content/assets/glyph-highlights-16.svg");
       });
       it("should use specified chrome icon", () => {
         const icon = "chrome://the/icon.svg";
-        prefStructure = [{icon}];
+        prefStructure = [{icon, pref: {feed: "feed"}}];
 
         testRender();
 
         assert.calledWith(node.setAttribute, "src", icon);
       });
     });
     describe("title line", () => {
       it("should render a title", () => {
@@ -281,17 +267,17 @@ describe("AboutPreferences Feed", () => 
         prefStructure = [{pref: {titleString}}];
 
         testRender();
 
         assert.calledWith(node.setAttribute, "label", titleString);
       });
       it("should add a link for top stories", () => {
         const href = "https://disclaimer/";
-        prefStructure = [{learnMore: {link: {href}}, id: "topstories"}];
+        prefStructure = [{learnMore: {link: {href}}, id: "topstories", pref: {feed: "feed"}}];
 
         testRender();
         assert.calledWith(node.setAttribute, "href", href);
       });
     });
     describe("description line", () => {
       it("should render a description", () => {
         const descString = "the_desc";
@@ -307,23 +293,71 @@ describe("AboutPreferences Feed", () => 
         testRender();
 
         assert.calledWith(node.setAttribute, "value", 1);
         assert.calledWith(node.setAttribute, "value", 2);
         assert.calledWith(node.setAttribute, "value", 3);
       });
     });
     describe("nested prefs", () => {
+      const titleString = "im_nested";
+      beforeEach(() => {
+        prefStructure = [{pref: {nestedPrefs: [{titleString}]}}];
+      });
       it("should render a nested pref", () => {
-        const titleString = "im_nested";
-        prefStructure = [{pref: {nestedPrefs: [{titleString}]}}];
+        testRender();
+
+        assert.calledWith(node.setAttribute, "label", titleString);
+      });
+      it("should add a change event", () => {
+        testRender();
+
+        assert.calledOnce(Preferences.get().on);
+        assert.calledWith(Preferences.get().on, "change");
+      });
+      it("should default node disabled to false", async () => {
+        Preferences.get = sandbox.stub().returns({
+          on: sandbox.stub(),
+          _value: true,
+        });
 
         testRender();
 
-        assert.calledWith(node.setAttribute, "label", titleString);
+        assert.isFalse(node.disabled);
+      });
+      it("should default node disabled to true", async () => {
+        testRender();
+
+        assert.isTrue(node.disabled);
+      });
+      it("should set node disabled to true", async () => {
+        const pref = {
+          on: sandbox.stub(),
+          _value: true,
+        };
+        Preferences.get = sandbox.stub().returns(pref);
+
+        testRender();
+        pref._value = !pref._value;
+        await Preferences.get().on.firstCall.args[1]();
+
+        assert.isTrue(node.disabled);
+      });
+      it("should set node disabled to false", async () => {
+        const pref = {
+          on: sandbox.stub(),
+          _value: false,
+        };
+        Preferences.get = sandbox.stub().returns(pref);
+
+        testRender();
+        pref._value = !pref._value;
+        await Preferences.get().on.firstCall.args[1]();
+
+        assert.isFalse(node.disabled);
       });
     });
     describe("restore defaults btn", () => {
       it("should call toggleRestoreDefaultsBtn", () => {
         testRender();
 
         assert.calledOnce(gHomePane.toggleRestoreDefaultsBtn);
       });
--- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
@@ -9,17 +9,18 @@ const DUMMY_ENDPOINT = "https://getpocke
 const ENDPOINTS_PREF_NAME = "discoverystream.endpoints";
 const SPOC_IMPRESSION_TRACKING_PREF = "discoverystream.spoc.impressions";
 const REC_IMPRESSION_TRACKING_PREF = "discoverystream.rec.impressions";
 const THIRTY_MINUTES = 30 * 60 * 1000;
 const ONE_WEEK = 7 * 24 * 60 * 60 * 1000; // 1 week
 
 const FAKE_UUID = "{foo-123-foo}";
 
-describe("DiscoveryStreamFeed", () => { // eslint-disable-line max-statements
+// eslint-disable-next-line max-statements
+describe("DiscoveryStreamFeed", () => {
   let DiscoveryStreamFeed;
   let feed;
   let sandbox;
   let fetchStub;
   let clock;
   let fakeNewTabUtils;
   let globals;
 
@@ -58,17 +59,16 @@ describe("DiscoveryStreamFeed", () => { 
     // Time
     clock = sinon.useFakeTimers();
 
     // Injector
     ({
       DiscoveryStreamFeed,
     } = injector({
       "lib/UserDomainAffinityProvider.jsm": {UserDomainAffinityProvider: FakeUserDomainAffinityProvider},
-      "lib/TelemetryFeed.jsm": {PREF_IMPRESSION_ID: "fake-pref-impression-id"},
     }));
 
     globals = new GlobalOverrider();
     globals.set("gUUIDGenerator", {generateUUID: () => FAKE_UUID});
 
     // Feed
     feed = new DiscoveryStreamFeed();
     feed.store = createStore(combineReducers(reducers), {
@@ -565,16 +565,46 @@ describe("DiscoveryStreamFeed", () => { 
         Prefs: {values: {showSponsored: true}},
       });
       Object.defineProperty(feed, "config", {get: () => ({show_spocs: true})});
 
       assert.isTrue(feed.showSpocs);
     });
   });
 
+  describe("#clearSpocs", () => {
+    it("should not fail with no endpoint", async () => {
+      sandbox.stub(feed.store, "getState").returns({
+        Prefs: {
+          values: {"discoverystream.endpointSpocsClear": null},
+        },
+      });
+      sandbox.stub(feed, "fetchFromEndpoint").resolves(null);
+
+      await feed.clearSpocs();
+
+      assert.notCalled(feed.fetchFromEndpoint);
+    });
+    it("should call DELETE with endpoint", async () => {
+      sandbox.stub(feed.store, "getState").returns({
+        Prefs: {
+          values: {"discoverystream.endpointSpocsClear": "https://spocs/user"},
+        },
+      });
+      sandbox.stub(feed, "fetchFromEndpoint").resolves(null);
+      feed._impressionId = "1234";
+
+      await feed.clearSpocs();
+
+      assert.equal(feed.fetchFromEndpoint.firstCall.args[0], "https://spocs/user");
+      assert.equal(feed.fetchFromEndpoint.firstCall.args[1].method, "DELETE");
+      assert.equal(feed.fetchFromEndpoint.firstCall.args[1].body, "{\"pocket_id\":\"1234\"}");
+    });
+  });
+
   describe("#rotate", () => {
     it("should move seen first story to the back of the response", () => {
       const recsExpireTime = 5600;
       const feedResponse = {
         recommendations: [
           {
             id: "first",
           },
@@ -1363,16 +1393,30 @@ describe("DiscoveryStreamFeed", () => { 
     });
     it("should fire loadSpocs is showSponsored pref changes", async () => {
       sandbox.stub(feed, "loadSpocs").returns(Promise.resolve());
 
       await feed.onAction({type: at.PREF_CHANGED, data: {name: "showSponsored"}});
 
       assert.calledOnce(feed.loadSpocs);
     });
+    it("should call clearSpocs when sponsored content is turned off", async () => {
+      sandbox.stub(feed, "clearSpocs").returns(Promise.resolve());
+
+      await feed.onAction({type: at.PREF_CHANGED, data: {name: "showSponsored", value: false}});
+
+      assert.calledOnce(feed.clearSpocs);
+    });
+    it("should call clearSpocs when top stories is turned off", async () => {
+      sandbox.stub(feed, "clearSpocs").returns(Promise.resolve());
+
+      await feed.onAction({type: at.PREF_CHANGED, data: {name: "feeds.section.topstories", value: false}});
+
+      assert.calledOnce(feed.clearSpocs);
+    });
   });
 
   describe("#onAction: SYSTEM_TICK", () => {
     it("should not refresh if DiscoveryStream has not been loaded", async () => {
       sandbox.stub(feed, "refreshAll").resolves();
       setPref(CONFIG_PREF_NAME, {enabled: true});
 
       await feed.onAction({type: at.SYSTEM_TICK});
--- a/browser/components/newtab/test/unit/lib/TopStoriesFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/TopStoriesFeed.test.js
@@ -93,21 +93,24 @@ describe("Top Stories Feed", () => {
   });
 
   describe("#lazyloading TopStories", () => {
     beforeEach(() => {
       instance.discoveryStreamEnabled = true;
     });
     it("should bind parseOptions to SectionsManager.onceInitialized when discovery stream is true", () => {
       instance.discoveryStreamEnabled = false;
-      instance.onAction({type: at.PREFS_INITIAL_VALUES, data: {"discoverystream.config": JSON.stringify({enabled: true})}});
+      instance.store.getState = () => ({Prefs: {values: {"discoverystream.config": JSON.stringify({enabled: true})}}});
+      instance.onAction({type: at.INIT, data: {}});
+
       assert.calledOnce(sectionsManagerStub.onceInitialized);
     });
     it("should bind parseOptions to SectionsManager.onceInitialized when discovery stream is false", () => {
-      instance.onAction({type: at.PREFS_INITIAL_VALUES, data: {"discoverystream.config": JSON.stringify({enabled: false})}});
+      instance.store.getState = () => ({Prefs: {values: {"discoverystream.config": JSON.stringify({enabled: false})}}});
+      instance.onAction({type: at.INIT, data: {}});
       assert.calledOnce(sectionsManagerStub.onceInitialized);
     });
     it("Should initialize properties once while lazy loading if not initialized earlier", () => {
       instance.discoveryStreamEnabled = false;
       instance.propertiesInitialized = false;
       sinon.stub(instance, "initializeProperties");
       instance.lazyLoadTopStories();
       assert.calledOnce(instance.initializeProperties);
@@ -132,27 +135,29 @@ describe("Top Stories Feed", () => {
       sinon.stub(instance, "doContentUpdate");
       await instance.onInit();
       assert.calledOnce(instance.doContentUpdate);
       assert.isTrue(instance.storiesLoaded);
     });
     it("should handle limited actions when discoverystream is enabled", async () => {
       sinon.spy(instance, "handleDisabled");
       sinon.stub(instance, "getPocketState");
-      instance.onAction({type: at.PREFS_INITIAL_VALUES, data: {"discoverystream.config": JSON.stringify({enabled: true})}});
+      instance.store.getState = () => ({Prefs: {values: {"discoverystream.config": JSON.stringify({enabled: true})}}});
+      instance.onAction({type: at.INIT, data: {}});
+
       assert.calledOnce(instance.handleDisabled);
-
       instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
       assert.notCalled(instance.getPocketState);
     });
     it("should handle NEW_TAB_REHYDRATED when discoverystream is disabled", async () => {
       instance.discoveryStreamEnabled = false;
       sinon.spy(instance, "handleDisabled");
       sinon.stub(instance, "getPocketState");
-      instance.onAction({type: at.PREFS_INITIAL_VALUES, data: {"discoverystream.config": JSON.stringify({enabled: false})}});
+      instance.store.getState = () => ({Prefs: {values: {"discoverystream.config": JSON.stringify({enabled: false})}}});
+      instance.onAction({type: at.INIT, data: {}});
       assert.notCalled(instance.handleDisabled);
 
       instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
       assert.calledOnce(instance.getPocketState);
     });
     it("should handle UNINIT when discoverystream is enabled", async () => {
       sinon.stub(instance, "uninit");
       instance.onAction({type: at.UNINIT});
@@ -188,33 +193,33 @@ describe("Top Stories Feed", () => {
     });
   });
 
   describe("#init", () => {
     it("should create a TopStoriesFeed", () => {
       assert.instanceOf(instance, TopStoriesFeed);
     });
     it("should bind parseOptions to SectionsManager.onceInitialized", () => {
-      instance.onAction({type: at.PREFS_INITIAL_VALUES, data: {}});
+      instance.onAction({type: at.INIT, data: {}});
       assert.calledOnce(sectionsManagerStub.onceInitialized);
     });
     it("should initialize endpoints based on options", async () => {
       await instance.onInit();
       assert.equal("https://somedomain.org/stories?key=test-api-key", instance.stories_endpoint);
       assert.equal("https://somedomain.org/referrer", instance.stories_referrer);
       assert.equal("https://somedomain.org/topics?key=test-api-key", instance.topics_endpoint);
     });
     it("should enable its section", () => {
-      instance.onAction({type: at.PREFS_INITIAL_VALUES, data: {}});
+      instance.onAction({type: at.INIT, data: {}});
       assert.calledOnce(sectionsManagerStub.enableSection);
       assert.calledWith(sectionsManagerStub.enableSection, SECTION_ID);
     });
     it("init should fire onInit", () => {
       instance.onInit = sinon.spy();
-      instance.onAction({type: at.PREFS_INITIAL_VALUES, data: {}});
+      instance.onAction({type: at.INIT, data: {}});
       assert.calledOnce(instance.onInit);
     });
     it("should fetch stories on init", async () => {
       instance.fetchStories = sinon.spy();
       await instance.onInit();
       assert.calledOnce(instance.fetchStories);
     });
     it("should fetch topics on init", async () => {
--- a/browser/components/newtab/test/unit/unit-entry.js
+++ b/browser/components/newtab/test/unit/unit-entry.js
@@ -2,17 +2,18 @@ import {EventEmitter, FakePerformance, F
 import Adapter from "enzyme-adapter-react-16";
 import {chaiAssertions} from "test/schemas/pings";
 import chaiJsonSchema from "chai-json-schema";
 import enzyme from "enzyme";
 enzyme.configure({adapter: new Adapter()});
 
 // Cause React warnings to make tests that trigger them fail
 const origConsoleError = console.error; // eslint-disable-line no-console
-console.error = function(msg, ...args) { // eslint-disable-line no-console
+ // eslint-disable-next-line no-console
+console.error = function(msg, ...args) {
   // eslint-disable-next-line no-console
   origConsoleError.apply(console, [msg, ...args]);
 
   if (/(Invalid prop|Failed prop type|Check the render method|React Intl)/.test(msg)) {
     throw new Error(msg);
   }
 };
 
--- a/browser/components/newtab/yamscripts.yml
+++ b/browser/components/newtab/yamscripts.yml
@@ -58,16 +58,17 @@ scripts:
     unit: karma start karma.mc.config.js
 
   tddmc: karma start karma.mc.config.js --tdd
 
   debugcoverage: open logs/coverage/index.html
 
 # lint: Run eslint and sass-lint
   lint:
+    eslint-check: eslint --print-config . | eslint-config-prettier-check
     eslint: eslint --ext=.js,.jsm,.jsx .
     sasslint: sass-lint -v -q
 
 # strings-import: Replace local strings with those from l10n-central
   strings-import: node ./bin/strings-import.js
 
 # test: Run all tests once
   test: =>testmc