Bug 1571115 - Add context message, react-transition-group package and bug fixes to New Tab Page r=k88hudson,fluent-reviewers,flod
authorEd Lee <edilee@mozilla.com>
Thu, 08 Aug 2019 05:36:24 +0000
changeset 486850 a28a338396c3e05c6b7b944832984e591d92ff0c
parent 486849 edaa39d2b4e530b9cd9a0d566316b0be55e5f9f9
child 486851 dfac7f4ddfae815ea9077956ce3152bd36944bf6
child 486852 96e453630656501e8a3ce777e9a5707d19480987
child 486953 10b5f7873d9a5b96ab47727615472cc52994fc5f
push id36407
push userncsoregi@mozilla.com
push dateThu, 08 Aug 2019 09:33:10 +0000
treeherdermozilla-central@a28a338396c3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson, fluent-reviewers, flod
bugs1571115
milestone70.0a1
first release with
nightly linux32
a28a338396c3 / 70.0a1 / 20190808093310 / files
nightly linux64
a28a338396c3 / 70.0a1 / 20190808093310 / files
nightly mac
a28a338396c3 / 70.0a1 / 20190808093310 / files
nightly win32
a28a338396c3 / 70.0a1 / 20190808093310 / files
nightly win64
a28a338396c3 / 70.0a1 / 20190808093310 / 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 1571115 - Add context message, react-transition-group package and bug fixes to New Tab Page r=k88hudson,fluent-reviewers,flod Depends on D40119 Differential Revision: https://phabricator.services.mozilla.com/D41092
browser/components/newtab/AboutNewTabService.jsm
browser/components/newtab/bin/render-activity-stream-html.js
browser/components/newtab/bin/vendor.js
browser/components/newtab/common/Actions.jsm
browser/components/newtab/common/Reducers.jsm
browser/components/newtab/content-src/asrouter/asrouter-content.jsx
browser/components/newtab/content-src/asrouter/templates/FirstRun/FirstRun.jsx
browser/components/newtab/content-src/asrouter/templates/FirstRun/addUtmParams.js
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/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.scss
browser/components/newtab/content-src/components/Base/_Base.scss
browser/components/newtab/content-src/components/Card/types.js
browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss
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/data/content/assets/icon-removed-bookmark.svg
browser/components/newtab/jar.mn
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/ActivityStream.jsm
browser/components/newtab/lib/DiscoveryStreamFeed.jsm
browser/components/newtab/lib/PrefsFeed.jsm
browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
browser/components/newtab/lib/ToolbarBadgeHub.jsm
browser/components/newtab/locales-src/newtab.ftl
browser/components/newtab/package-lock.json
browser/components/newtab/package.json
browser/components/newtab/prerendered/activity-stream-debug.html
browser/components/newtab/prerendered/activity-stream.html
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
browser/components/newtab/test/unit/lib/PrefsFeed.test.js
browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
browser/components/newtab/vendor/REACT_TRANSITION_GROUP_LICENSE
browser/components/newtab/vendor/react-transition-group.js
browser/components/newtab/webpack.system-addon.config.js
browser/locales/en-US/browser/newtab/newtab.ftl
--- a/browser/components/newtab/AboutNewTabService.jsm
+++ b/browser/components/newtab/AboutNewTabService.jsm
@@ -148,16 +148,17 @@ AboutNewTabService.prototype = {
 
           // This list must match any similar ones in render-activity-stream-html.js.
           const scripts = [
             "chrome://browser/content/contentSearchUI.js",
             "chrome://browser/content/contentTheme.js",
             `${BASE_URL}vendor/react${debugString}.js`,
             `${BASE_URL}vendor/react-dom${debugString}.js`,
             `${BASE_URL}vendor/prop-types.js`,
+            `${BASE_URL}vendor/react-transition-group.js`,
             `${BASE_URL}vendor/redux.js`,
             `${BASE_URL}vendor/react-redux.js`,
             `${BASE_URL}data/content/activity-stream.bundle.js`,
           ];
 
           for (let script of scripts) {
             Services.scriptloader.loadSubScript(script, win); // Synchronous call
           }
--- a/browser/components/newtab/bin/render-activity-stream-html.js
+++ b/browser/components/newtab/bin/render-activity-stream-html.js
@@ -29,16 +29,17 @@ function templateHTML(options) {
   const scripts = [
     "chrome://browser/content/contentSearchUI.js",
     "chrome://browser/content/contentTheme.js",
     `${options.baseUrl}vendor/react${debugString}.js`,
     `${options.baseUrl}vendor/react-dom${debugString}.js`,
     `${options.baseUrl}vendor/prop-types.js`,
     `${options.baseUrl}vendor/redux.js`,
     `${options.baseUrl}vendor/react-redux.js`,
+    `${options.baseUrl}vendor/react-transition-group.js`,
     `${options.baseUrl}data/content/activity-stream.bundle.js`,
   ];
 
   // Add spacing and script tags
   const scriptRender = `\n${scripts
     .map(script => `    <script src="${script}"></script>`)
     .join("\n")}`;
 
--- a/browser/components/newtab/bin/vendor.js
+++ b/browser/components/newtab/bin/vendor.js
@@ -13,16 +13,19 @@ const filesToVendor = {
   // in case that changes at some point in the future.
   "react/LICENSE": "REACT_AND_REACT_DOM_LICENSE",
   "react/umd/react.production.min.js": "react.js",
   "react/umd/react.development.js": "react-dev.js",
   "react-dom/umd/react-dom.production.min.js": "react-dom.js",
   "react-dom/umd/react-dom.development.js": "react-dom-dev.js",
   "react-redux/LICENSE.md": "REACT_REDUX_LICENSE",
   "react-redux/dist/react-redux.min.js": "react-redux.js",
+  "react-transition-group/dist/react-transition-group.min.js":
+    "react-transition-group.js",
+  "react-transition-group/LICENSE": "REACT_TRANSITION_GROUP_LICENSE",
 };
 
 set("-v"); // Echo all the copy commands so the user can see what's going on
 for (let srcPath of Object.keys(filesToVendor)) {
   cp(
     path.join("node_modules", srcPath),
     path.join("vendor", filesToVendor[srcPath])
   );
--- a/browser/components/newtab/common/Actions.jsm
+++ b/browser/components/newtab/common/Actions.jsm
@@ -30,23 +30,25 @@ for (const type of [
   "ADDONS_INFO_RESPONSE",
   "ARCHIVE_FROM_POCKET",
   "AS_ROUTER_INITIALIZED",
   "AS_ROUTER_PREF_CHANGED",
   "AS_ROUTER_TARGETING_UPDATE",
   "AS_ROUTER_TELEMETRY_USER_EVENT",
   "BLOCK_URL",
   "BOOKMARK_URL",
+  "CLEAR_PREF",
   "COPY_DOWNLOAD_LINK",
   "DELETE_BOOKMARK_BY_ID",
   "DELETE_FROM_POCKET",
   "DELETE_HISTORY_URL",
   "DIALOG_CANCEL",
   "DIALOG_OPEN",
   "DISCOVERY_STREAM_CONFIG_CHANGE",
+  "DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS",
   "DISCOVERY_STREAM_CONFIG_SETUP",
   "DISCOVERY_STREAM_CONFIG_SET_VALUE",
   "DISCOVERY_STREAM_FEEDS_UPDATE",
   "DISCOVERY_STREAM_FEED_UPDATE",
   "DISCOVERY_STREAM_IMPRESSION_STATS",
   "DISCOVERY_STREAM_LAYOUT_RESET",
   "DISCOVERY_STREAM_LAYOUT_UPDATE",
   "DISCOVERY_STREAM_LINK_BLOCKED",
@@ -152,16 +154,17 @@ for (const type of [
   "INSTALL_ADDON_FROM_URL",
   "OPEN_APPLICATIONS_MENU",
   "OPEN_PRIVATE_BROWSER_WINDOW",
   "OPEN_URL",
   "OPEN_ABOUT_PAGE",
   "OPEN_PREFERENCES_PAGE",
   "SHOW_FIREFOX_ACCOUNTS",
   "PIN_CURRENT_TAB",
+  "ENABLE_FIREFOX_MONITOR",
 ]) {
   ASRouterActions[type] = type;
 }
 
 // Helper function for creating routed actions between content and main
 // Not intended to be used by consumers
 function _RouteMessage(action, options) {
   const meta = action.meta ? { ...action.meta } : {};
--- a/browser/components/newtab/common/Reducers.jsm
+++ b/browser/components/newtab/common/Reducers.jsm
@@ -629,16 +629,17 @@ function DiscoveryStream(prevState = INI
           );
 
     case at.PLACES_SAVED_TO_POCKET:
       const addPocketInfo = item => {
         if (item.url === action.data.url) {
           return Object.assign({}, item, {
             open_url: action.data.open_url,
             pocket_id: action.data.pocket_id,
+            context_type: "pocket",
           });
         }
         return item;
       };
       return isNotReady()
         ? prevState
         : nextState(items => items.map(addPocketInfo));
 
@@ -653,31 +654,35 @@ function DiscoveryStream(prevState = INI
     case at.PLACES_BOOKMARK_ADDED:
       const updateBookmarkInfo = item => {
         if (item.url === action.data.url) {
           const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
           return Object.assign({}, item, {
             bookmarkGuid,
             bookmarkTitle,
             bookmarkDateCreated: dateAdded,
+            context_type: "bookmark",
           });
         }
         return item;
       };
       return isNotReady()
         ? prevState
         : nextState(items => items.map(updateBookmarkInfo));
 
     case at.PLACES_BOOKMARK_REMOVED:
       const removeBookmarkInfo = item => {
         if (item.url === action.data.url) {
           const newSite = Object.assign({}, item);
           delete newSite.bookmarkGuid;
           delete newSite.bookmarkTitle;
           delete newSite.bookmarkDateCreated;
+          if (!newSite.context_type || newSite.context_type === "bookmark") {
+            newSite.context_type = "removedBookmark";
+          }
           return newSite;
         }
         return item;
       };
       return isNotReady()
         ? prevState
         : nextState(items => items.map(removeBookmarkInfo));
 
--- a/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
+++ b/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
@@ -1,13 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-import { actionCreators as ac } from "common/Actions.jsm";
+import {
+  actionCreators as ac,
+  actionTypes as at,
+  ASRouterActions as ra,
+} from "common/Actions.jsm";
 import { OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME } from "content-src/lib/init-store";
 import { generateBundles } from "./rich-text-strings";
 import { ImpressionsWrapper } from "./components/ImpressionsWrapper/ImpressionsWrapper";
 import { LocalizationProvider } from "fluent-react";
 import { NEWTAB_DARK_THEME } from "content-src/lib/constants";
 import React from "react";
 import ReactDOM from "react-dom";
 import { SnippetsTemplates } from "./templates/template-manifest";
@@ -104,27 +108,73 @@ function shouldSendImpressionOnUpdate(ne
 
 export class ASRouterUISurface extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onMessageFromParent = this.onMessageFromParent.bind(this);
     this.sendClick = this.sendClick.bind(this);
     this.sendImpression = this.sendImpression.bind(this);
     this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
+    this.onUserAction = this.onUserAction.bind(this);
+    this.fetchFlowParams = this.fetchFlowParams.bind(this);
+
     this.state = { message: {} };
     if (props.document) {
       this.headerPortal = props.document.getElementById(
         "header-asrouter-container"
       );
       this.footerPortal = props.document.getElementById(
         "footer-asrouter-container"
       );
     }
   }
 
+  async fetchFlowParams(params = {}) {
+    let result = {};
+    const { fxaEndpoint, dispatch } = this.props;
+    if (!fxaEndpoint) {
+      const err =
+        "Tried to fetch flow params before fxaEndpoint pref was ready";
+      console.error(err); // eslint-disable-line no-console
+    }
+
+    try {
+      const urlObj = new URL(fxaEndpoint);
+      urlObj.pathname = "metrics-flow";
+      Object.keys(params).forEach(key => {
+        urlObj.searchParams.append(key, params[key]);
+      });
+      const response = await fetch(urlObj.toString(), { credentials: "omit" });
+      if (response.status === 200) {
+        const { deviceId, flowId, flowBeginTime } = await response.json();
+        result = { deviceId, flowId, flowBeginTime };
+      } else {
+        console.error("Non-200 response", response); // eslint-disable-line no-console
+        dispatch(
+          ac.OnlyToMain({
+            type: at.TELEMETRY_UNDESIRED_EVENT,
+            data: {
+              event: "FXA_METRICS_FETCH_ERROR",
+              value: response.status,
+            },
+          })
+        );
+      }
+    } catch (error) {
+      console.error(error); // eslint-disable-line no-console
+      dispatch(
+        ac.OnlyToMain({
+          type: at.TELEMETRY_UNDESIRED_EVENT,
+          data: { event: "FXA_METRICS_ERROR" },
+        })
+      );
+    }
+    return result;
+  }
+
   sendUserActionTelemetry(extraProps = {}) {
     const { message } = this.state;
     const eventType = `${message.provider}_user_event`;
     ASRouterUtils.sendTelemetry({
       message_id: message.id,
       source: extraProps.id,
       action: eventType,
       ...extraProps,
@@ -233,16 +283,42 @@ export class ASRouterUISurface extends R
       });
     }
   }
 
   componentWillUnmount() {
     ASRouterUtils.removeListener(this.onMessageFromParent);
   }
 
+  async getMonitorUrl({ url, flowRequestParams = {} }) {
+    const flowValues = await this.fetchFlowParams(flowRequestParams);
+
+    // Note that flowParams are actually added dynamically on the page
+    const urlObj = new URL(url);
+    ["deviceId", "flowId", "flowBeginTime"].forEach(key => {
+      if (key in flowValues) {
+        urlObj.searchParams.append(key, flowValues[key]);
+      }
+    });
+
+    return urlObj.toString();
+  }
+
+  async onUserAction(action) {
+    switch (action.type) {
+      // This needs to be handled locally because its
+      case ra.ENABLE_FIREFOX_MONITOR:
+        const url = await this.getMonitorUrl(action.data.args);
+        ASRouterUtils.executeAction({ type: ra.OPEN_URL, data: { args: url } });
+        break;
+      default:
+        ASRouterUtils.executeAction(action);
+    }
+  }
+
   renderSnippets() {
     const { message } = this.state;
     if (!SnippetsTemplates[message.template]) {
       return null;
     }
     const SnippetComponent = SnippetsTemplates[message.template];
     const { content } = this.state.message;
 
@@ -256,17 +332,17 @@ export class ASRouterUISurface extends R
         document={this.props.document}
       >
         <LocalizationProvider bundles={generateBundles(content)}>
           <SnippetComponent
             {...this.state.message}
             UISurface="NEWTAB_FOOTER_BAR"
             onBlock={this.onBlockById(this.state.message.id)}
             onDismiss={this.onDismissById(this.state.message.id)}
-            onAction={ASRouterUtils.executeAction}
+            onAction={this.onUserAction}
             sendClick={this.sendClick}
             sendUserActionTelemetry={this.sendUserActionTelemetry}
           />
         </LocalizationProvider>
       </ImpressionsWrapper>
     );
   }
 
@@ -299,16 +375,17 @@ export class ASRouterUISurface extends R
             document={this.props.document}
             message={message}
             sendUserActionTelemetry={this.sendUserActionTelemetry}
             executeAction={ASRouterUtils.executeAction}
             dispatch={this.props.dispatch}
             onBlock={this.onBlockById(this.state.message.id)}
             onDismiss={this.onDismissById(this.state.message.id)}
             fxaEndpoint={this.props.fxaEndpoint}
+            fetchFlowParams={this.fetchFlowParams}
           />
         </ImpressionsWrapper>
       );
     }
     return null;
   }
 
   render() {
--- a/browser/components/newtab/content-src/asrouter/templates/FirstRun/FirstRun.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/FirstRun/FirstRun.jsx
@@ -1,17 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 import React from "react";
 import { Interrupt } from "./Interrupt";
 import { Triplets } from "./Triplets";
-import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
-import { addUtmParams } from "./addUtmParams";
+import { BASE_PARAMS } from "./addUtmParams";
 
 export const FLUENT_FILES = [
   "branding/brand.ftl",
   "browser/branding/brandings.ftl",
   "browser/branding/sync-brand.ftl",
   "browser/newtab/onboarding.ftl",
 ];
 
@@ -31,47 +30,16 @@ export const helpers = {
 
   addFluent(document) {
     FLUENT_FILES.forEach(file => {
       const link = document.head.appendChild(document.createElement("link"));
       link.href = file;
       link.rel = "localization";
     });
   },
-
-  async fetchFlowParams({ fxaEndpoint, UTMTerm, dispatch, setFlowParams }) {
-    try {
-      const url = new URL(
-        `${fxaEndpoint}/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`
-      );
-      addUtmParams(url, UTMTerm);
-      const response = await fetch(url, { credentials: "omit" });
-      if (response.status === 200) {
-        const { deviceId, flowId, flowBeginTime } = await response.json();
-        setFlowParams({ deviceId, flowId, flowBeginTime });
-      } else {
-        dispatch(
-          ac.OnlyToMain({
-            type: at.TELEMETRY_UNDESIRED_EVENT,
-            data: {
-              event: "FXA_METRICS_FETCH_ERROR",
-              value: response.status,
-            },
-          })
-        );
-      }
-    } catch (error) {
-      dispatch(
-        ac.OnlyToMain({
-          type: at.TELEMETRY_UNDESIRED_EVENT,
-          data: { event: "FXA_METRICS_ERROR" },
-        })
-      );
-    }
-  },
 };
 
 export class FirstRun extends React.PureComponent {
   constructor(props) {
     super(props);
 
     this.didLoadFlowParams = false;
 
@@ -124,27 +92,28 @@ export class FirstRun extends React.Pure
         isTripletsContentVisible: !(hasInterrupt || !hasTriplets),
 
         UTMTerm,
       };
     }
     return null;
   }
 
-  fetchFlowParams() {
-    const { fxaEndpoint, dispatch } = this.props;
+  async fetchFlowParams() {
+    const { fxaEndpoint, fetchFlowParams } = this.props;
     const { UTMTerm } = this.state;
     if (fxaEndpoint && UTMTerm && !this.didLoadFlowParams) {
       this.didLoadFlowParams = true;
-      helpers.fetchFlowParams({
-        fxaEndpoint,
-        UTMTerm,
-        dispatch,
-        setFlowParams: flowParams => this.setState({ flowParams }),
+      const flowParams = await fetchFlowParams({
+        ...BASE_PARAMS,
+        entrypoint: "activity-stream-firstrun",
+        form_type: "email",
+        utm_term: UTMTerm,
       });
+      this.setState({ flowParams });
     }
   }
 
   removeHideMain() {
     if (!this.state.hasInterrupt) {
       // We need to remove hide-main since we should show it underneath everything that has rendered
       this.props.document.body.classList.remove("hide-main", "welcome");
     }
--- a/browser/components/newtab/content-src/asrouter/templates/FirstRun/addUtmParams.js
+++ b/browser/components/newtab/content-src/asrouter/templates/FirstRun/addUtmParams.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const BASE_PARAMS = {
+export const BASE_PARAMS = {
   utm_source: "activity-stream",
   utm_campaign: "firstrun",
   utm_medium: "referral",
 };
 
 /**
  * Takes in a url as a string or URL object and returns a URL object with the
  * utm_* parameters added to it. If a URL object is passed in, the paraemeters
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
@@ -37,27 +37,27 @@ export class SimpleBelowSearchSnippet ex
       <h3
         className={`title ${this._shouldRenderButton() ? "title-inline" : ""}`}
       >
         {title}
       </h3>
     ) : null;
   }
 
-  onButtonClick() {
+  async onButtonClick() {
     if (this.props.provider !== "preview") {
       this.props.sendUserActionTelemetry({
         event: "CLICK_BUTTON",
         id: this.props.UISurface,
       });
     }
     const { button_url } = this.props.content;
     // If button_url is defined handle it as OPEN_URL action
     const type = this.props.content.button_action || (button_url && "OPEN_URL");
-    this.props.onAction({
+    await this.props.onAction({
       type,
       data: { args: this.props.content.button_action_args || button_url },
     });
     if (!this.props.content.do_not_autoblock) {
       this.props.onBlock();
     }
   }
 
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
@@ -55,17 +55,16 @@
     },
     "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."}
       ]
     },
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
@@ -1,44 +1,53 @@
 
 .below-search-snippet {
   margin: 0 auto 16px;
 
   &.withButton {
     padding: 0 25px;
     margin: auto;
+    background-color: transparent;
 
     // Add more padding if discovery stream is enabled.
     .ds-outer-wrapper-breakpoint-override & {
       padding: 0 50px;
     }
 
     .snippet-hover-wrapper {
 
       &:hover {
         background-color: var(--newtab-element-hover-color);
 
         .blockButton {
           display: block;
+
+          // larger inset if discovery stream is enabled.
+          .ds-outer-wrapper-breakpoint-override & {
+            inset-inline-end: -8%;
+          }
         }
       }
     }
   }
 }
 
 .SimpleBelowSearchSnippet {
   background-color: transparent;
   border: 0;
   box-shadow: none;
   position: relative;
   line-height: 40px;
-  width: 736px;
   margin: auto;
   z-index: auto;
 
+  @media (min-width: $break-point-large) {
+    width: 736px;
+  }
+
   &.active {
     background-color: var(--newtab-element-hover-color);
   }
 
   .innerWrapper {
     align-items: center;
     background-color: transparent;
     border-radius: 4px;
@@ -56,32 +65,40 @@
       padding: 0;
       text-align: inherit;
     }
 
     @media (min-width: $break-point-medium) {
       @include full-width-styles;
     }
 
+    @media (max-width: 1120px) {
+      margin: 0 60px;
+    }
+
+    @media (max-width: 865px) {
+      margin: 0 60px 0 0;
+    }
+
     // Disable breakpoints for now if discovery stream is enabled.
     .ds-outer-wrapper-breakpoint-override & {
       @include full-width-styles;
+      margin: auto;
     }
   }
 
   .blockButton {
     display: block;
     inset-inline-end: 20px;
     opacity: 1;
     top: 24px;
   }
 
   .icon {
     height: 32px;
-    margin-inline-start: 12px;
     margin-top: 15px;
     width: 32px;
 
     @mixin full-width-styles {
       height: 24px;
       margin-top: 10px;
       width: 24px;
     }
@@ -94,22 +111,36 @@
     .ds-outer-wrapper-breakpoint-override & {
       @include full-width-styles;
     }
   }
 
   &.withButton {
     line-height: 20px;
     margin-bottom: 10px;
+    background-color: transparent;
 
     .blockButton {
       display: none;
-      inset-inline-end: -80px;
+      inset-inline-end: -15%;
       opacity: 1;
       margin: auto;
+
+      @media (max-width: 1120px) {
+        inset-inline-end: 2%;
+      }
+
+      @media (max-width: 865px) {
+        margin-top: 10px;
+      }
+
+      .ds-outer-wrapper-breakpoint-override & {
+        inset-inline-end: -10%;
+        margin: auto;
+      }
     }
 
     .textContainer {
       margin: 10px;
     }
 
     .title {
       font-size: inherit;
@@ -118,18 +149,21 @@
 
     .title-inline {
       display: inline;
     }
 
     .icon {
       width: 42px;
       height: 42px;
-      margin-inline-end: 12px;
       flex-shrink: 0;
+
+      @media (max-width: 865px) {
+        display: none;
+      }
     }
 
     .buttonContainer {
       margin: auto;
     }
   }
 
   .body {
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
@@ -63,17 +63,16 @@
     },
     "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."}
       ]
     },
--- a/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
+++ b/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
@@ -30,16 +30,17 @@ function relativeTime(timestamp) {
   } else if (minutes < 600) {
     return `${minutes} minutes ago`;
   }
   return new Date(timestamp).toLocaleString();
 }
 
 const LAYOUT_VARIANTS = {
   basic: "Basic default layout (on by default in nightly)",
+  staging_spocs: "A layout with all spocs shown",
   "dev-test-all":
     "A little bit of everything. Good layout for testing all components",
   "dev-test-feeds": "Stress testing for slow feeds",
 };
 
 export class ToggleStoryButton extends React.PureComponent {
   constructor(props) {
     super(props);
@@ -50,38 +51,78 @@ export class ToggleStoryButton extends R
     this.props.onClick(this.props.story);
   }
 
   render() {
     return <button onClick={this.handleClick}>collapse/open</button>;
   }
 }
 
+export class TogglePrefCheckbox extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.onChange = this.onChange.bind(this);
+  }
+
+  onChange(event) {
+    this.props.onChange(this.props.pref, event.target.checked);
+  }
+
+  render() {
+    return (
+      <>
+        <input
+          type="checkbox"
+          checked={this.props.checked}
+          onChange={this.onChange}
+        />{" "}
+        {this.props.pref}{" "}
+      </>
+    );
+  }
+}
+
 export class DiscoveryStreamAdmin extends React.PureComponent {
   constructor(props) {
     super(props);
-    this.onEnableToggle = this.onEnableToggle.bind(this);
+    this.restorePrefDefaults = this.restorePrefDefaults.bind(this);
+    this.setConfigValue = this.setConfigValue.bind(this);
+    this.expireCache = this.expireCache.bind(this);
     this.changeEndpointVariant = this.changeEndpointVariant.bind(this);
     this.onStoryToggle = this.onStoryToggle.bind(this);
     this.state = {
       toggledStories: {},
     };
   }
 
   setConfigValue(name, value) {
     this.props.dispatch(
       ac.OnlyToMain({
         type: at.DISCOVERY_STREAM_CONFIG_SET_VALUE,
         data: { name, value },
       })
     );
   }
 
-  onEnableToggle(event) {
-    this.setConfigValue("enabled", event.target.checked);
+  restorePrefDefaults(event) {
+    this.props.dispatch(
+      ac.OnlyToMain({
+        type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS,
+      })
+    );
+  }
+
+  expireCache() {
+    const { config } = this.props.state;
+    this.props.dispatch(
+      ac.OnlyToMain({
+        type: at.DISCOVERY_STREAM_CONFIG_CHANGE,
+        data: config,
+      })
+    );
   }
 
   changeEndpointVariant(event) {
     const endpoint = this.props.state.config.layout_endpoint;
     if (endpoint) {
       this.setConfigValue(
         "layout_endpoint",
         endpoint.replace(
@@ -224,33 +265,53 @@ export class DiscoveryStreamAdmin extend
             ) || "(no data)"}
           </td>
         </Row>
       </React.Fragment>
     );
   }
 
   render() {
+    const prefToggles = "enabled hardcoded_layout show_spocs personalized collapsible".split(
+      " "
+    );
     const { config, lastUpdated, layout } = this.props.state;
     return (
       <div>
-        <div className="dsEnabled">
-          <input
-            type="checkbox"
-            checked={config.enabled}
-            onChange={this.onEnableToggle}
-          />{" "}
-          enabled{" "}
-        </div>
+        <button className="button" onClick={this.restorePrefDefaults}>
+          Restore Pref Defaults
+        </button>{" "}
+        <button className="button" onClick={this.expireCache}>
+          Expire Cache
+        </button>
+        <table>
+          <tbody>
+            {prefToggles.map(pref => (
+              <Row key={pref}>
+                <td>
+                  <TogglePrefCheckbox
+                    checked={config[pref]}
+                    pref={pref}
+                    onChange={this.setConfigValue}
+                  />
+                </td>
+              </Row>
+            ))}
+          </tbody>
+        </table>
         <h3>Endpoint variant</h3>
         <p>
           You can also change this manually by changing this pref:{" "}
           <code>browser.newtabpage.activity-stream.discoverystream.config</code>
         </p>
-        <table style={config.enabled ? null : { opacity: 0.5 }}>
+        <table
+          style={
+            config.enabled && !config.hardcoded_layout ? null : { opacity: 0.5 }
+          }
+        >
           <tbody>
             {Object.keys(LAYOUT_VARIANTS).map(id => (
               <Row key={id}>
                 <td className="min">
                   <input
                     type="radio"
                     value={id}
                     checked={this.isCurrentVariant(id)}
@@ -258,42 +319,37 @@ export class DiscoveryStreamAdmin extend
                   />
                 </td>
                 <td className="min">{id}</td>
                 <td>{LAYOUT_VARIANTS[id]}</td>
               </Row>
             ))}
           </tbody>
         </table>
-
         <h3>Caching info</h3>
         <table style={config.enabled ? null : { opacity: 0.5 }}>
           <tbody>
             <Row>
               <td className="min">Data last fetched</td>
               <td>{relativeTime(lastUpdated) || "(no data)"}</td>
             </Row>
           </tbody>
         </table>
-
         <h3>Layout</h3>
-
         {layout.map((row, rowIndex) => (
           <div key={`row-${rowIndex}`}>
             {row.components.map((component, componentIndex) => (
               <div key={`component-${componentIndex}`} className="ds-component">
                 {this.renderComponent(row.width, component)}
               </div>
             ))}
           </div>
         ))}
-
         <h3>Feeds Data</h3>
         {this.renderFeedsData()}
-
         <h3>Spocs</h3>
         {this.renderSpocs()}
       </div>
     );
   }
 }
 
 export class ASRouterAdminInner extends React.PureComponent {
--- a/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.scss
+++ b/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.scss
@@ -204,23 +204,16 @@
     background: $black-10;
     border-radius: 3px;
 
     a {
       text-decoration: underline;
     }
   }
 
-  .dsEnabled {
-    padding: 10px;
-    font-size: 16px;
-    margin-bottom: 20px;
-    border: 1px solid $border-color;
-  }
-
   .ds-component {
     margin-bottom: 20px;
   }
 
   .modalOverlayInner {
     height: 80%;
   }
 }
--- a/browser/components/newtab/content-src/components/Base/_Base.scss
+++ b/browser/components/newtab/content-src/components/Base/_Base.scss
@@ -10,48 +10,49 @@
     padding-top: 134px;
   }
 
   a {
     color: var(--newtab-link-primary-color);
   }
 }
 
-main,
-.below-search-snippet.withButton {
+main {
   margin: auto;
   width: $wrapper-default-width;
+  // Offset the snippets container so things at the bottom of the page are still
+  // visible when snippets are visible. Adjust for other spacing.
+  padding-bottom: $snippets-container-height - $section-spacing - $base-gutter;
+
+  section {
+    margin-bottom: $section-spacing;
+    position: relative;
+  }
+
+  .hide-main & {
+    visibility: hidden;
+  }
 
   @media (min-width: $break-point-medium) {
     width: $wrapper-max-width-medium;
   }
 
   @media (min-width: $break-point-large) {
     width: $wrapper-max-width-large;
   }
 
   @media (min-width: $break-point-widest) {
     width: $wrapper-max-width-widest;
   }
 
 }
 
-main {
-  // Offset the snippets container so things at the bottom of the page are still
-  // visible when snippets are visible. Adjust for other spacing.
-  padding-bottom: $snippets-container-height - $section-spacing - $base-gutter;
-
-  section {
-    margin-bottom: $section-spacing;
-    position: relative;
-  }
-
-  .hide-main & {
-    visibility: hidden;
-  }
+.below-search-snippet.withButton {
+  margin: auto;
+  width: 100%;
 }
 
 .ds-outer-wrapper-search-alignment {
   main {
     // This override is to ensure while Discovery Stream loads,
     // the search bar does not jump around. (it sticks to the top)
     margin: 0 auto;
   }
--- a/browser/components/newtab/content-src/components/Card/types.js
+++ b/browser/components/newtab/content-src/components/Card/types.js
@@ -2,16 +2,20 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 export const cardContextTypes = {
   history: {
     fluentID: "newtab-label-visited",
     icon: "history-item",
   },
+  removedBookmark: {
+    fluentID: "newtab-label-removed-bookmark",
+    icon: "bookmark-removed",
+  },
   bookmark: {
     fluentID: "newtab-label-bookmarked",
     icon: "bookmark-added",
   },
   trending: {
     fluentID: "newtab-label-recommended",
     icon: "trending",
   },
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
@@ -28,16 +28,17 @@ export class CardGrid extends React.Pure
             url={rec.url}
             id={rec.id}
             shim={rec.shim}
             type={this.props.type}
             context={rec.context}
             dispatch={this.props.dispatch}
             source={rec.domain}
             pocket_id={rec.pocket_id}
+            context_type={rec.context_type}
             bookmarkGuid={rec.bookmarkGuid}
           />
         )
       );
     }
 
     let divisibility = ``;
 
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
@@ -3,16 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 import { actionCreators as ac } from "common/Actions.jsm";
 import { DSImage } from "../DSImage/DSImage.jsx";
 import { DSLinkMenu } from "../DSLinkMenu/DSLinkMenu";
 import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats";
 import React from "react";
 import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
+import { DSContextFooter } from "../DSContextFooter/DSContextFooter.jsx";
 
 export class DSCard extends React.PureComponent {
   constructor(props) {
     super(props);
 
     this.onLinkClick = this.onLinkClick.bind(this);
   }
 
@@ -63,19 +64,20 @@ export class DSCard extends React.PureCo
           <div className="meta">
             <div className="info-wrap">
               <p className="source clamp">{this.props.source}</p>
               <header className="title clamp">{this.props.title}</header>
               {this.props.excerpt && (
                 <p className="excerpt clamp">{this.props.excerpt}</p>
               )}
             </div>
-            {this.props.context && (
-              <p className="context">{this.props.context}</p>
-            )}
+            <DSContextFooter
+              context_type={this.props.context_type}
+              context={this.props.context}
+            />
           </div>
           <ImpressionStats
             campaignId={this.props.campaignId}
             rows={[
               {
                 id: this.props.id,
                 pos: this.props.pos,
                 ...(this.props.shim && this.props.shim.impression
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
@@ -18,17 +18,16 @@
       cursor: default;
     }
 
     .img-wrapper {
       opacity: 0;
     }
   }
 
-
   .img-wrapper {
     width: 100%;
   }
 
   .img {
     height: 0;
     padding-top: 50%; // 2:1 aspect ratio
 
@@ -91,42 +90,38 @@
     flex-direction: column;
     padding: 12px 16px;
     flex-grow: 1;
 
     .info-wrap {
       flex-grow: 1;
     }
 
-    .context {
-      margin: 12px 0 0;
-    }
-
     .title {
       // show only 3 lines of copy
       @include limit-visibile-lines(3, $header-line-height, $header-font-size);
       font-weight: 600;
     }
 
     .excerpt {
       // show only 3 lines of copy
-      @include limit-visibile-lines(3, $excerpt-line-height, $excerpt-font-size);
+      @include limit-visibile-lines(
+        3,
+        $excerpt-line-height,
+        $excerpt-font-size
+      );
     }
 
     .source {
-      -webkit-line-clamp: 1;
-      margin-bottom: 2px;
-    }
-
-    .context,
-    .source {
       @include dark-theme-only {
         color: $grey-40;
       }
 
+      -webkit-line-clamp: 1;
+      margin-bottom: 2px;
       font-size: 13px;
       color: $grey-50;
     }
   }
 
   header {
     @include dark-theme-only {
       color: $grey-10;
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { cardContextTypes } from "../../Card/types.js";
+import { CSSTransition, TransitionGroup } from "react-transition-group";
+import React from "react";
+
+// Animation time is mirrored in DSContextFooter.scss
+const ANIMATION_DURATION = 3000;
+
+const StatusMessage = ({ icon, fluentID }) => (
+  <div className="status-message">
+    <span
+      aria-haspopup="true"
+      className={`story-badge-icon icon icon-${icon}`}
+    />
+    <div className="story-context-label" data-l10n-id={fluentID} />
+  </div>
+);
+
+export class DSContextFooter extends React.PureComponent {
+  render() {
+    const { context, context_type } = this.props;
+    const { icon, fluentID } = cardContextTypes[context_type] || {};
+
+    return (
+      <div className="story-footer">
+        {context && <p className="story-sponsored-label">{context}</p>}
+        <TransitionGroup component={null}>
+          {!context && context_type && (
+            <CSSTransition
+              key={fluentID}
+              timeout={ANIMATION_DURATION}
+              classNames="story-animate"
+            >
+              <StatusMessage icon={icon} fluentID={fluentID} />
+            </CSSTransition>
+          )}
+        </TransitionGroup>
+      </div>
+    );
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss
@@ -0,0 +1,106 @@
+$status-green: #058B00;
+$status-dark-green: #7C6;
+
+.story-footer {
+  color: var(--newtab-text-secondary-color);
+  inset-inline-start: 0;
+  margin-top: 12px;
+
+  .story-sponsored-label,
+  .status-message {
+    @include dark-theme-only {
+      color: $grey-40;
+    }
+
+    font-size: 13px;
+    line-height: 24px;
+    color: $grey-50;
+  }
+
+  .status-message {
+    display: flex;
+    align-items: center;
+    height: 24px;
+
+    .story-badge-icon {
+      @include dark-theme-only {
+        fill: $grey-40;
+      }
+
+      fill: $grey-50;
+      height: 16px;
+      margin-inline-end: 6px;
+
+      &.icon-bookmark-removed {
+        background-image: url('#{$image-path}icon-removed-bookmark.svg');
+      }
+    }
+
+    .story-context-label {
+      @include dark-theme-only {
+        color: $grey-40;
+      }
+
+      color: $grey-50;
+      flex-grow: 1;
+      font-size: 13px;
+      line-height: 24px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+}
+
+.story-animate-enter {
+  opacity: 0;
+}
+
+.story-animate-enter-active {
+  opacity: 1;
+  transition: opacity 150ms ease-in 300ms;
+
+  .story-badge-icon,
+  .story-context-label {
+    @include dark-theme-only {
+      animation: dark-color 3s ease-out 0.3s;
+    }
+
+    animation: color 3s ease-out 0.3s;
+
+    @keyframes color {
+      0% {
+        color: $status-green;
+        fill: $status-green;
+      }
+
+      100% {
+        color: $grey-50;
+        fill: $grey-50;
+      }
+    }
+
+    @keyframes dark-color {
+      0% {
+        color: $status-dark-green;
+        fill: $status-dark-green;
+      }
+
+      100% {
+        color: $grey-40;
+        fill: $grey-40;
+      }
+    }
+  }
+}
+
+.story-animate-exit {
+  position: absolute;
+  top: 0;
+  opacity: 1;
+}
+
+.story-animate-exit-active {
+  opacity: 0;
+  transition: opacity 250ms ease-in;
+}
--- a/browser/components/newtab/content-src/styles/_activity-stream.scss
+++ b/browser/components/newtab/content-src/styles/_activity-stream.scss
@@ -152,16 +152,17 @@ input {
 @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/DSContextFooter/DSContextFooter';
 @import '../components/DiscoveryStreamComponents/DSImage/DSImage';
 @import '../components/DiscoveryStreamComponents/DSMessage/DSMessage';
 @import '../components/DiscoveryStreamImpressionStats/ImpressionStats';
 @import '../components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState';
 
 // AS Router
 @import '../asrouter/components/Button/Button';
 @import '../asrouter/components/SnippetBase/SnippetBase';
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -363,40 +363,38 @@ input[type='text'], input[type='search']
   min-height: 100vh;
   padding: 30px 32px 32px; }
   .outer-wrapper.only-search {
     display: block;
     padding-top: 134px; }
   .outer-wrapper a {
     color: var(--newtab-link-primary-color); }
 
-main,
-.below-search-snippet.withButton {
+main {
   margin: auto;
-  width: 274px; }
-  @media (min-width: 610px) {
-    main,
-    .below-search-snippet.withButton {
-      width: 530px; } }
-  @media (min-width: 866px) {
-    main,
-    .below-search-snippet.withButton {
-      width: 786px; } }
-  @media (min-width: 1122px) {
-    main,
-    .below-search-snippet.withButton {
-      width: 1042px; } }
-
-main {
+  width: 274px;
   padding-bottom: 68px; }
   main section {
     margin-bottom: 20px;
     position: relative; }
   .hide-main main {
     visibility: hidden; }
+  @media (min-width: 610px) {
+    main {
+      width: 530px; } }
+  @media (min-width: 866px) {
+    main {
+      width: 786px; } }
+  @media (min-width: 1122px) {
+    main {
+      width: 1042px; } }
+
+.below-search-snippet.withButton {
+  margin: auto;
+  width: 100%; }
 
 .ds-outer-wrapper-search-alignment main {
   margin: 0 auto; }
 
 .ds-outer-wrapper-breakpoint-override main {
   width: 1042px; }
 
 .ds-outer-wrapper-breakpoint-override:not(.fixed-search) .search-wrapper .search-inner-wrapper {
@@ -1767,21 +1765,16 @@ main {
     border: 1px solid #D70022; }
   .asrouter-admin .helpLink {
     padding: 10px;
     display: flex;
     background: rgba(0, 0, 0, 0.1);
     border-radius: 3px; }
     .asrouter-admin .helpLink a {
       text-decoration: underline; }
-  .asrouter-admin .dsEnabled {
-    padding: 10px;
-    font-size: 16px;
-    margin-bottom: 20px;
-    border: 1px solid var(--newtab-border-secondary-color); }
   .asrouter-admin .ds-component {
     margin-bottom: 20px; }
   .asrouter-admin .modalOverlayInner {
     height: 80%; }
 
 .pocket-logged-in-cta {
   font-size: 13px;
   margin-inline-end: 20px;
@@ -2691,51 +2684,119 @@ main {
         color: #0A84FF; }
   .ds-card .meta {
     display: flex;
     flex-direction: column;
     padding: 12px 16px;
     flex-grow: 1; }
     .ds-card .meta .info-wrap {
       flex-grow: 1; }
-    .ds-card .meta .context {
-      margin: 12px 0 0; }
     .ds-card .meta .title {
       font-size: 17px;
       -webkit-line-clamp: 3;
       line-height: 24px;
       font-weight: 600; }
     .ds-card .meta .excerpt {
       font-size: 14px;
       -webkit-line-clamp: 3;
       line-height: 20px; }
     .ds-card .meta .source {
       -webkit-line-clamp: 1;
-      margin-bottom: 2px; }
-    .ds-card .meta .context,
-    .ds-card .meta .source {
+      margin-bottom: 2px;
       font-size: 13px;
       color: #737373; }
-      [lwt-newtab-brighttext] .ds-card .meta .context, [lwt-newtab-brighttext]
-      .ds-card .meta .source {
+      [lwt-newtab-brighttext] .ds-card .meta .source {
         color: #B1B1B3; }
   .ds-card header {
     line-height: 24px;
     font-size: 17px;
     color: #0C0C0D; }
     [lwt-newtab-brighttext] .ds-card header {
       color: #F9F9FA; }
   .ds-card p {
     font-size: 14px;
     line-height: 20px;
     color: #0C0C0D;
     margin: 0; }
     [lwt-newtab-brighttext] .ds-card p {
       color: #F9F9FA; }
 
+.story-footer {
+  color: var(--newtab-text-secondary-color);
+  inset-inline-start: 0;
+  margin-top: 12px; }
+  .story-footer .story-sponsored-label,
+  .story-footer .status-message {
+    font-size: 13px;
+    line-height: 24px;
+    color: #737373; }
+    [lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
+    .story-footer .status-message {
+      color: #B1B1B3; }
+  .story-footer .status-message {
+    display: flex;
+    align-items: center;
+    height: 24px; }
+    .story-footer .status-message .story-badge-icon {
+      fill: #737373;
+      height: 16px;
+      margin-inline-end: 6px; }
+      [lwt-newtab-brighttext] .story-footer .status-message .story-badge-icon {
+        fill: #B1B1B3; }
+      .story-footer .status-message .story-badge-icon.icon-bookmark-removed {
+        background-image: url("../data/content/assets/icon-removed-bookmark.svg"); }
+    .story-footer .status-message .story-context-label {
+      color: #737373;
+      flex-grow: 1;
+      font-size: 13px;
+      line-height: 24px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap; }
+      [lwt-newtab-brighttext] .story-footer .status-message .story-context-label {
+        color: #B1B1B3; }
+
+.story-animate-enter {
+  opacity: 0; }
+
+.story-animate-enter-active {
+  opacity: 1;
+  transition: opacity 150ms ease-in 300ms; }
+  .story-animate-enter-active .story-badge-icon,
+  .story-animate-enter-active .story-context-label {
+    animation: color 3s ease-out 0.3s; }
+    [lwt-newtab-brighttext] .story-animate-enter-active .story-badge-icon, [lwt-newtab-brighttext]
+    .story-animate-enter-active .story-context-label {
+      animation: dark-color 3s ease-out 0.3s; }
+
+@keyframes color {
+  0% {
+    color: #058B00;
+    fill: #058B00; }
+  100% {
+    color: #737373;
+    fill: #737373; } }
+
+@keyframes dark-color {
+  0% {
+    color: #7C6;
+    fill: #7C6; }
+  100% {
+    color: #B1B1B3;
+    fill: #B1B1B3; } }
+
+.story-animate-exit {
+  position: absolute;
+  top: 0;
+  opacity: 1; }
+
+.story-animate-exit-active {
+  opacity: 0;
+  transition: opacity 250ms ease-in; }
+
 .ds-image {
   display: block;
   position: relative; }
   .ds-image img,
   .ds-image .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
@@ -3173,33 +3234,38 @@ body[lwt-newtab-brighttext] .scene2Icon 
   .amo + body.hide-main .icon-add {
     fill: #FFF;
     vertical-align: sub; }
 
 .below-search-snippet {
   margin: 0 auto 16px; }
   .below-search-snippet.withButton {
     padding: 0 25px;
-    margin: auto; }
+    margin: auto;
+    background-color: transparent; }
     .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton {
       padding: 0 50px; }
     .below-search-snippet.withButton .snippet-hover-wrapper:hover {
       background-color: var(--newtab-element-hover-color); }
       .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
         display: block; }
+        .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+          inset-inline-end: -8%; }
 
 .SimpleBelowSearchSnippet {
   background-color: transparent;
   border: 0;
   box-shadow: none;
   position: relative;
   line-height: 40px;
-  width: 736px;
   margin: auto;
   z-index: auto; }
+  @media (min-width: 866px) {
+    .SimpleBelowSearchSnippet {
+      width: 736px; } }
   .SimpleBelowSearchSnippet.active {
     background-color: var(--newtab-element-hover-color); }
   .SimpleBelowSearchSnippet .innerWrapper {
     align-items: center;
     background-color: transparent;
     border-radius: 4px;
     box-shadow: var(--newtab-card-shadow);
     flex-direction: column;
@@ -3209,62 +3275,80 @@ body[lwt-newtab-brighttext] .scene2Icon 
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         align-items: flex-start;
         background-color: inherit;
         box-shadow: none;
         flex-direction: row;
         padding: 0;
         text-align: inherit; } }
+    @media (max-width: 1120px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px; } }
+    @media (max-width: 865px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px 0 0; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .innerWrapper {
       align-items: flex-start;
       background-color: inherit;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
-      text-align: inherit; }
+      text-align: inherit;
+      margin: auto; }
   .SimpleBelowSearchSnippet .blockButton {
     display: block;
     inset-inline-end: 20px;
     opacity: 1;
     top: 24px; }
   .SimpleBelowSearchSnippet .icon {
     height: 32px;
-    margin-inline-start: 12px;
     margin-top: 15px;
     width: 32px; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .icon {
         height: 24px;
         margin-top: 10px;
         width: 24px; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .icon {
       height: 24px;
       margin-top: 10px;
       width: 24px; }
   .SimpleBelowSearchSnippet.withButton {
     line-height: 20px;
-    margin-bottom: 10px; }
+    margin-bottom: 10px;
+    background-color: transparent; }
     .SimpleBelowSearchSnippet.withButton .blockButton {
       display: none;
-      inset-inline-end: -80px;
+      inset-inline-end: -15%;
       opacity: 1;
       margin: auto; }
+      @media (max-width: 1120px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          inset-inline-end: 2%; } }
+      @media (max-width: 865px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          margin-top: 10px; } }
+      .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
+        inset-inline-end: -10%;
+        margin: auto; }
     .SimpleBelowSearchSnippet.withButton .textContainer {
       margin: 10px; }
     .SimpleBelowSearchSnippet.withButton .title {
       font-size: inherit;
       margin: 0; }
     .SimpleBelowSearchSnippet.withButton .title-inline {
       display: inline; }
     .SimpleBelowSearchSnippet.withButton .icon {
       width: 42px;
       height: 42px;
-      margin-inline-end: 12px;
       flex-shrink: 0; }
+      @media (max-width: 865px) {
+        .SimpleBelowSearchSnippet.withButton .icon {
+          display: none; } }
     .SimpleBelowSearchSnippet.withButton .buttonContainer {
       margin: auto; }
   .SimpleBelowSearchSnippet .body {
     display: inline;
     margin: 8px 0 0; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .body {
         margin: 12px 0; } }
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -366,40 +366,38 @@ input[type='text'], input[type='search']
   min-height: 100vh;
   padding: 30px 32px 32px; }
   .outer-wrapper.only-search {
     display: block;
     padding-top: 134px; }
   .outer-wrapper a {
     color: var(--newtab-link-primary-color); }
 
-main,
-.below-search-snippet.withButton {
+main {
   margin: auto;
-  width: 274px; }
-  @media (min-width: 610px) {
-    main,
-    .below-search-snippet.withButton {
-      width: 530px; } }
-  @media (min-width: 866px) {
-    main,
-    .below-search-snippet.withButton {
-      width: 786px; } }
-  @media (min-width: 1122px) {
-    main,
-    .below-search-snippet.withButton {
-      width: 1042px; } }
-
-main {
+  width: 274px;
   padding-bottom: 68px; }
   main section {
     margin-bottom: 20px;
     position: relative; }
   .hide-main main {
     visibility: hidden; }
+  @media (min-width: 610px) {
+    main {
+      width: 530px; } }
+  @media (min-width: 866px) {
+    main {
+      width: 786px; } }
+  @media (min-width: 1122px) {
+    main {
+      width: 1042px; } }
+
+.below-search-snippet.withButton {
+  margin: auto;
+  width: 100%; }
 
 .ds-outer-wrapper-search-alignment main {
   margin: 0 auto; }
 
 .ds-outer-wrapper-breakpoint-override main {
   width: 1042px; }
 
 .ds-outer-wrapper-breakpoint-override:not(.fixed-search) .search-wrapper .search-inner-wrapper {
@@ -1770,21 +1768,16 @@ main {
     border: 1px solid #D70022; }
   .asrouter-admin .helpLink {
     padding: 10px;
     display: flex;
     background: rgba(0, 0, 0, 0.1);
     border-radius: 3px; }
     .asrouter-admin .helpLink a {
       text-decoration: underline; }
-  .asrouter-admin .dsEnabled {
-    padding: 10px;
-    font-size: 16px;
-    margin-bottom: 20px;
-    border: 1px solid var(--newtab-border-secondary-color); }
   .asrouter-admin .ds-component {
     margin-bottom: 20px; }
   .asrouter-admin .modalOverlayInner {
     height: 80%; }
 
 .pocket-logged-in-cta {
   font-size: 13px;
   margin-inline-end: 20px;
@@ -2694,51 +2687,119 @@ main {
         color: #0A84FF; }
   .ds-card .meta {
     display: flex;
     flex-direction: column;
     padding: 12px 16px;
     flex-grow: 1; }
     .ds-card .meta .info-wrap {
       flex-grow: 1; }
-    .ds-card .meta .context {
-      margin: 12px 0 0; }
     .ds-card .meta .title {
       font-size: 17px;
       -webkit-line-clamp: 3;
       line-height: 24px;
       font-weight: 600; }
     .ds-card .meta .excerpt {
       font-size: 14px;
       -webkit-line-clamp: 3;
       line-height: 20px; }
     .ds-card .meta .source {
       -webkit-line-clamp: 1;
-      margin-bottom: 2px; }
-    .ds-card .meta .context,
-    .ds-card .meta .source {
+      margin-bottom: 2px;
       font-size: 13px;
       color: #737373; }
-      [lwt-newtab-brighttext] .ds-card .meta .context, [lwt-newtab-brighttext]
-      .ds-card .meta .source {
+      [lwt-newtab-brighttext] .ds-card .meta .source {
         color: #B1B1B3; }
   .ds-card header {
     line-height: 24px;
     font-size: 17px;
     color: #0C0C0D; }
     [lwt-newtab-brighttext] .ds-card header {
       color: #F9F9FA; }
   .ds-card p {
     font-size: 14px;
     line-height: 20px;
     color: #0C0C0D;
     margin: 0; }
     [lwt-newtab-brighttext] .ds-card p {
       color: #F9F9FA; }
 
+.story-footer {
+  color: var(--newtab-text-secondary-color);
+  inset-inline-start: 0;
+  margin-top: 12px; }
+  .story-footer .story-sponsored-label,
+  .story-footer .status-message {
+    font-size: 13px;
+    line-height: 24px;
+    color: #737373; }
+    [lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
+    .story-footer .status-message {
+      color: #B1B1B3; }
+  .story-footer .status-message {
+    display: flex;
+    align-items: center;
+    height: 24px; }
+    .story-footer .status-message .story-badge-icon {
+      fill: #737373;
+      height: 16px;
+      margin-inline-end: 6px; }
+      [lwt-newtab-brighttext] .story-footer .status-message .story-badge-icon {
+        fill: #B1B1B3; }
+      .story-footer .status-message .story-badge-icon.icon-bookmark-removed {
+        background-image: url("../data/content/assets/icon-removed-bookmark.svg"); }
+    .story-footer .status-message .story-context-label {
+      color: #737373;
+      flex-grow: 1;
+      font-size: 13px;
+      line-height: 24px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap; }
+      [lwt-newtab-brighttext] .story-footer .status-message .story-context-label {
+        color: #B1B1B3; }
+
+.story-animate-enter {
+  opacity: 0; }
+
+.story-animate-enter-active {
+  opacity: 1;
+  transition: opacity 150ms ease-in 300ms; }
+  .story-animate-enter-active .story-badge-icon,
+  .story-animate-enter-active .story-context-label {
+    animation: color 3s ease-out 0.3s; }
+    [lwt-newtab-brighttext] .story-animate-enter-active .story-badge-icon, [lwt-newtab-brighttext]
+    .story-animate-enter-active .story-context-label {
+      animation: dark-color 3s ease-out 0.3s; }
+
+@keyframes color {
+  0% {
+    color: #058B00;
+    fill: #058B00; }
+  100% {
+    color: #737373;
+    fill: #737373; } }
+
+@keyframes dark-color {
+  0% {
+    color: #7C6;
+    fill: #7C6; }
+  100% {
+    color: #B1B1B3;
+    fill: #B1B1B3; } }
+
+.story-animate-exit {
+  position: absolute;
+  top: 0;
+  opacity: 1; }
+
+.story-animate-exit-active {
+  opacity: 0;
+  transition: opacity 250ms ease-in; }
+
 .ds-image {
   display: block;
   position: relative; }
   .ds-image img,
   .ds-image .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
@@ -3176,33 +3237,38 @@ body[lwt-newtab-brighttext] .scene2Icon 
   .amo + body.hide-main .icon-add {
     fill: #FFF;
     vertical-align: sub; }
 
 .below-search-snippet {
   margin: 0 auto 16px; }
   .below-search-snippet.withButton {
     padding: 0 25px;
-    margin: auto; }
+    margin: auto;
+    background-color: transparent; }
     .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton {
       padding: 0 50px; }
     .below-search-snippet.withButton .snippet-hover-wrapper:hover {
       background-color: var(--newtab-element-hover-color); }
       .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
         display: block; }
+        .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+          inset-inline-end: -8%; }
 
 .SimpleBelowSearchSnippet {
   background-color: transparent;
   border: 0;
   box-shadow: none;
   position: relative;
   line-height: 40px;
-  width: 736px;
   margin: auto;
   z-index: auto; }
+  @media (min-width: 866px) {
+    .SimpleBelowSearchSnippet {
+      width: 736px; } }
   .SimpleBelowSearchSnippet.active {
     background-color: var(--newtab-element-hover-color); }
   .SimpleBelowSearchSnippet .innerWrapper {
     align-items: center;
     background-color: transparent;
     border-radius: 4px;
     box-shadow: var(--newtab-card-shadow);
     flex-direction: column;
@@ -3212,62 +3278,80 @@ body[lwt-newtab-brighttext] .scene2Icon 
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         align-items: flex-start;
         background-color: inherit;
         box-shadow: none;
         flex-direction: row;
         padding: 0;
         text-align: inherit; } }
+    @media (max-width: 1120px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px; } }
+    @media (max-width: 865px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px 0 0; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .innerWrapper {
       align-items: flex-start;
       background-color: inherit;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
-      text-align: inherit; }
+      text-align: inherit;
+      margin: auto; }
   .SimpleBelowSearchSnippet .blockButton {
     display: block;
     inset-inline-end: 20px;
     opacity: 1;
     top: 24px; }
   .SimpleBelowSearchSnippet .icon {
     height: 32px;
-    margin-inline-start: 12px;
     margin-top: 15px;
     width: 32px; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .icon {
         height: 24px;
         margin-top: 10px;
         width: 24px; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .icon {
       height: 24px;
       margin-top: 10px;
       width: 24px; }
   .SimpleBelowSearchSnippet.withButton {
     line-height: 20px;
-    margin-bottom: 10px; }
+    margin-bottom: 10px;
+    background-color: transparent; }
     .SimpleBelowSearchSnippet.withButton .blockButton {
       display: none;
-      inset-inline-end: -80px;
+      inset-inline-end: -15%;
       opacity: 1;
       margin: auto; }
+      @media (max-width: 1120px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          inset-inline-end: 2%; } }
+      @media (max-width: 865px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          margin-top: 10px; } }
+      .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
+        inset-inline-end: -10%;
+        margin: auto; }
     .SimpleBelowSearchSnippet.withButton .textContainer {
       margin: 10px; }
     .SimpleBelowSearchSnippet.withButton .title {
       font-size: inherit;
       margin: 0; }
     .SimpleBelowSearchSnippet.withButton .title-inline {
       display: inline; }
     .SimpleBelowSearchSnippet.withButton .icon {
       width: 42px;
       height: 42px;
-      margin-inline-end: 12px;
       flex-shrink: 0; }
+      @media (max-width: 865px) {
+        .SimpleBelowSearchSnippet.withButton .icon {
+          display: none; } }
     .SimpleBelowSearchSnippet.withButton .buttonContainer {
       margin: auto; }
   .SimpleBelowSearchSnippet .body {
     display: inline;
     margin: 8px 0 0; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .body {
         margin: 12px 0; } }
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -363,40 +363,38 @@ input[type='text'], input[type='search']
   min-height: 100vh;
   padding: 30px 32px 32px; }
   .outer-wrapper.only-search {
     display: block;
     padding-top: 134px; }
   .outer-wrapper a {
     color: var(--newtab-link-primary-color); }
 
-main,
-.below-search-snippet.withButton {
+main {
   margin: auto;
-  width: 274px; }
-  @media (min-width: 610px) {
-    main,
-    .below-search-snippet.withButton {
-      width: 530px; } }
-  @media (min-width: 866px) {
-    main,
-    .below-search-snippet.withButton {
-      width: 786px; } }
-  @media (min-width: 1122px) {
-    main,
-    .below-search-snippet.withButton {
-      width: 1042px; } }
-
-main {
+  width: 274px;
   padding-bottom: 68px; }
   main section {
     margin-bottom: 20px;
     position: relative; }
   .hide-main main {
     visibility: hidden; }
+  @media (min-width: 610px) {
+    main {
+      width: 530px; } }
+  @media (min-width: 866px) {
+    main {
+      width: 786px; } }
+  @media (min-width: 1122px) {
+    main {
+      width: 1042px; } }
+
+.below-search-snippet.withButton {
+  margin: auto;
+  width: 100%; }
 
 .ds-outer-wrapper-search-alignment main {
   margin: 0 auto; }
 
 .ds-outer-wrapper-breakpoint-override main {
   width: 1042px; }
 
 .ds-outer-wrapper-breakpoint-override:not(.fixed-search) .search-wrapper .search-inner-wrapper {
@@ -1767,21 +1765,16 @@ main {
     border: 1px solid #D70022; }
   .asrouter-admin .helpLink {
     padding: 10px;
     display: flex;
     background: rgba(0, 0, 0, 0.1);
     border-radius: 3px; }
     .asrouter-admin .helpLink a {
       text-decoration: underline; }
-  .asrouter-admin .dsEnabled {
-    padding: 10px;
-    font-size: 16px;
-    margin-bottom: 20px;
-    border: 1px solid var(--newtab-border-secondary-color); }
   .asrouter-admin .ds-component {
     margin-bottom: 20px; }
   .asrouter-admin .modalOverlayInner {
     height: 80%; }
 
 .pocket-logged-in-cta {
   font-size: 13px;
   margin-inline-end: 20px;
@@ -2691,51 +2684,119 @@ main {
         color: #0A84FF; }
   .ds-card .meta {
     display: flex;
     flex-direction: column;
     padding: 12px 16px;
     flex-grow: 1; }
     .ds-card .meta .info-wrap {
       flex-grow: 1; }
-    .ds-card .meta .context {
-      margin: 12px 0 0; }
     .ds-card .meta .title {
       font-size: 17px;
       -webkit-line-clamp: 3;
       line-height: 24px;
       font-weight: 600; }
     .ds-card .meta .excerpt {
       font-size: 14px;
       -webkit-line-clamp: 3;
       line-height: 20px; }
     .ds-card .meta .source {
       -webkit-line-clamp: 1;
-      margin-bottom: 2px; }
-    .ds-card .meta .context,
-    .ds-card .meta .source {
+      margin-bottom: 2px;
       font-size: 13px;
       color: #737373; }
-      [lwt-newtab-brighttext] .ds-card .meta .context, [lwt-newtab-brighttext]
-      .ds-card .meta .source {
+      [lwt-newtab-brighttext] .ds-card .meta .source {
         color: #B1B1B3; }
   .ds-card header {
     line-height: 24px;
     font-size: 17px;
     color: #0C0C0D; }
     [lwt-newtab-brighttext] .ds-card header {
       color: #F9F9FA; }
   .ds-card p {
     font-size: 14px;
     line-height: 20px;
     color: #0C0C0D;
     margin: 0; }
     [lwt-newtab-brighttext] .ds-card p {
       color: #F9F9FA; }
 
+.story-footer {
+  color: var(--newtab-text-secondary-color);
+  inset-inline-start: 0;
+  margin-top: 12px; }
+  .story-footer .story-sponsored-label,
+  .story-footer .status-message {
+    font-size: 13px;
+    line-height: 24px;
+    color: #737373; }
+    [lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
+    .story-footer .status-message {
+      color: #B1B1B3; }
+  .story-footer .status-message {
+    display: flex;
+    align-items: center;
+    height: 24px; }
+    .story-footer .status-message .story-badge-icon {
+      fill: #737373;
+      height: 16px;
+      margin-inline-end: 6px; }
+      [lwt-newtab-brighttext] .story-footer .status-message .story-badge-icon {
+        fill: #B1B1B3; }
+      .story-footer .status-message .story-badge-icon.icon-bookmark-removed {
+        background-image: url("../data/content/assets/icon-removed-bookmark.svg"); }
+    .story-footer .status-message .story-context-label {
+      color: #737373;
+      flex-grow: 1;
+      font-size: 13px;
+      line-height: 24px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap; }
+      [lwt-newtab-brighttext] .story-footer .status-message .story-context-label {
+        color: #B1B1B3; }
+
+.story-animate-enter {
+  opacity: 0; }
+
+.story-animate-enter-active {
+  opacity: 1;
+  transition: opacity 150ms ease-in 300ms; }
+  .story-animate-enter-active .story-badge-icon,
+  .story-animate-enter-active .story-context-label {
+    animation: color 3s ease-out 0.3s; }
+    [lwt-newtab-brighttext] .story-animate-enter-active .story-badge-icon, [lwt-newtab-brighttext]
+    .story-animate-enter-active .story-context-label {
+      animation: dark-color 3s ease-out 0.3s; }
+
+@keyframes color {
+  0% {
+    color: #058B00;
+    fill: #058B00; }
+  100% {
+    color: #737373;
+    fill: #737373; } }
+
+@keyframes dark-color {
+  0% {
+    color: #7C6;
+    fill: #7C6; }
+  100% {
+    color: #B1B1B3;
+    fill: #B1B1B3; } }
+
+.story-animate-exit {
+  position: absolute;
+  top: 0;
+  opacity: 1; }
+
+.story-animate-exit-active {
+  opacity: 0;
+  transition: opacity 250ms ease-in; }
+
 .ds-image {
   display: block;
   position: relative; }
   .ds-image img,
   .ds-image .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
@@ -3173,33 +3234,38 @@ body[lwt-newtab-brighttext] .scene2Icon 
   .amo + body.hide-main .icon-add {
     fill: #FFF;
     vertical-align: sub; }
 
 .below-search-snippet {
   margin: 0 auto 16px; }
   .below-search-snippet.withButton {
     padding: 0 25px;
-    margin: auto; }
+    margin: auto;
+    background-color: transparent; }
     .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton {
       padding: 0 50px; }
     .below-search-snippet.withButton .snippet-hover-wrapper:hover {
       background-color: var(--newtab-element-hover-color); }
       .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
         display: block; }
+        .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+          inset-inline-end: -8%; }
 
 .SimpleBelowSearchSnippet {
   background-color: transparent;
   border: 0;
   box-shadow: none;
   position: relative;
   line-height: 40px;
-  width: 736px;
   margin: auto;
   z-index: auto; }
+  @media (min-width: 866px) {
+    .SimpleBelowSearchSnippet {
+      width: 736px; } }
   .SimpleBelowSearchSnippet.active {
     background-color: var(--newtab-element-hover-color); }
   .SimpleBelowSearchSnippet .innerWrapper {
     align-items: center;
     background-color: transparent;
     border-radius: 4px;
     box-shadow: var(--newtab-card-shadow);
     flex-direction: column;
@@ -3209,62 +3275,80 @@ body[lwt-newtab-brighttext] .scene2Icon 
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         align-items: flex-start;
         background-color: inherit;
         box-shadow: none;
         flex-direction: row;
         padding: 0;
         text-align: inherit; } }
+    @media (max-width: 1120px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px; } }
+    @media (max-width: 865px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px 0 0; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .innerWrapper {
       align-items: flex-start;
       background-color: inherit;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
-      text-align: inherit; }
+      text-align: inherit;
+      margin: auto; }
   .SimpleBelowSearchSnippet .blockButton {
     display: block;
     inset-inline-end: 20px;
     opacity: 1;
     top: 24px; }
   .SimpleBelowSearchSnippet .icon {
     height: 32px;
-    margin-inline-start: 12px;
     margin-top: 15px;
     width: 32px; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .icon {
         height: 24px;
         margin-top: 10px;
         width: 24px; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .icon {
       height: 24px;
       margin-top: 10px;
       width: 24px; }
   .SimpleBelowSearchSnippet.withButton {
     line-height: 20px;
-    margin-bottom: 10px; }
+    margin-bottom: 10px;
+    background-color: transparent; }
     .SimpleBelowSearchSnippet.withButton .blockButton {
       display: none;
-      inset-inline-end: -80px;
+      inset-inline-end: -15%;
       opacity: 1;
       margin: auto; }
+      @media (max-width: 1120px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          inset-inline-end: 2%; } }
+      @media (max-width: 865px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          margin-top: 10px; } }
+      .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
+        inset-inline-end: -10%;
+        margin: auto; }
     .SimpleBelowSearchSnippet.withButton .textContainer {
       margin: 10px; }
     .SimpleBelowSearchSnippet.withButton .title {
       font-size: inherit;
       margin: 0; }
     .SimpleBelowSearchSnippet.withButton .title-inline {
       display: inline; }
     .SimpleBelowSearchSnippet.withButton .icon {
       width: 42px;
       height: 42px;
-      margin-inline-end: 12px;
       flex-shrink: 0; }
+      @media (max-width: 865px) {
+        .SimpleBelowSearchSnippet.withButton .icon {
+          display: none; } }
     .SimpleBelowSearchSnippet.withButton .buttonContainer {
       margin: auto; }
   .SimpleBelowSearchSnippet .body {
     display: inline;
     margin: 8px 0 0; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .body {
         margin: 12px 0; } }
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -88,25 +88,25 @@
 /******/ ([
 /* 0 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
-/* harmony import */ var content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(51);
+/* harmony import */ var content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(54);
 /* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_5__);
 /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(12);
 /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_6__);
-/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(56);
+/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(59);
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
 
 
@@ -189,25 +189,25 @@ const globalImportContext = typeof Windo
 
 // Create an object that avoids accidental differing key/value pairs:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 const actionTypes = {};
 
-for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TARGETING_UPDATE", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "DISCOVERY_STREAM_FEEDS_UPDATE", "DISCOVERY_STREAM_FEED_UPDATE", "DISCOVERY_STREAM_IMPRESSION_STATS", "DISCOVERY_STREAM_LAYOUT_RESET", "DISCOVERY_STREAM_LAYOUT_UPDATE", "DISCOVERY_STREAM_LINK_BLOCKED", "DISCOVERY_STREAM_LOADED_CONTENT", "DISCOVERY_STREAM_RETRY_FEED", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_FILL", "DISCOVERY_STREAM_SPOCS_UPDATE", "DISCOVERY_STREAM_SPOC_BLOCKED", "DISCOVERY_STREAM_SPOC_IMPRESSION", "DOWNLOAD_CHANGED", "FAKE_FOCUS_SEARCH", "FILL_SEARCH_TERM", "HANDOFF_SEARCH_TO_AWESOMEBAR", "HIDE_SEARCH", "INIT", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LINK_DELETED_OR_ARCHIVED", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SHOW_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "TRAILHEAD_ENROLL_EVENT", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
+for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TARGETING_UPDATE", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "CLEAR_PREF", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "DISCOVERY_STREAM_FEEDS_UPDATE", "DISCOVERY_STREAM_FEED_UPDATE", "DISCOVERY_STREAM_IMPRESSION_STATS", "DISCOVERY_STREAM_LAYOUT_RESET", "DISCOVERY_STREAM_LAYOUT_UPDATE", "DISCOVERY_STREAM_LINK_BLOCKED", "DISCOVERY_STREAM_LOADED_CONTENT", "DISCOVERY_STREAM_RETRY_FEED", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_FILL", "DISCOVERY_STREAM_SPOCS_UPDATE", "DISCOVERY_STREAM_SPOC_BLOCKED", "DISCOVERY_STREAM_SPOC_IMPRESSION", "DOWNLOAD_CHANGED", "FAKE_FOCUS_SEARCH", "FILL_SEARCH_TERM", "HANDOFF_SEARCH_TO_AWESOMEBAR", "HIDE_SEARCH", "INIT", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LINK_DELETED_OR_ARCHIVED", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SHOW_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "TRAILHEAD_ENROLL_EVENT", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
   actionTypes[type] = type;
 } // These are acceptable actions for AS Router messages to have. They can show up
 // as call-to-action buttons in snippets, onboarding tour, etc.
 
 
 const ASRouterActions = {};
 
-for (const type of ["INSTALL_ADDON_FROM_URL", "OPEN_APPLICATIONS_MENU", "OPEN_PRIVATE_BROWSER_WINDOW", "OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PREFERENCES_PAGE", "SHOW_FIREFOX_ACCOUNTS", "PIN_CURRENT_TAB"]) {
+for (const type of ["INSTALL_ADDON_FROM_URL", "OPEN_APPLICATIONS_MENU", "OPEN_PRIVATE_BROWSER_WINDOW", "OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PREFERENCES_PAGE", "SHOW_FIREFOX_ACCOUNTS", "PIN_CURRENT_TAB", "ENABLE_FIREFOX_MONITOR"]) {
   ASRouterActions[type] = type;
 } // Helper function for creating routed actions between content and main
 // Not intended to be used by consumers
 
 
 function _RouteMessage(action, options) {
   const meta = action.meta ? { ...action.meta
   } : {};
@@ -560,22 +560,22 @@ var actionUtils = {
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BaseContent", function() { return BaseContent; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Base", function() { return Base; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var content_src_components_ASRouterAdmin_ASRouterAdmin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
 /* harmony import */ var _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(5);
 /* harmony import */ var content_src_components_ConfirmDialog_ConfirmDialog__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(29);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
-/* harmony import */ var content_src_components_DiscoveryStreamBase_DiscoveryStreamBase__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(52);
-/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(34);
+/* harmony import */ var content_src_components_DiscoveryStreamBase_DiscoveryStreamBase__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(55);
+/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(36);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_7__);
-/* harmony import */ var content_src_components_Search_Search__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(50);
-/* harmony import */ var content_src_components_Sections_Sections__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(39);
+/* harmony import */ var content_src_components_Search_Search__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(53);
+/* harmony import */ var content_src_components_Sections_Sections__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(41);
 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); }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
@@ -747,16 +747,17 @@ const Base = Object(react_redux__WEBPACK
 
 /***/ }),
 /* 4 */
 /***/ (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__, "ToggleStoryButton", function() { return ToggleStoryButton; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TogglePrefCheckbox", function() { return TogglePrefCheckbox; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DiscoveryStreamAdmin", function() { return DiscoveryStreamAdmin; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterAdminInner", function() { return ASRouterAdminInner; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CollapseToggle", function() { return CollapseToggle; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterAdmin", function() { return ASRouterAdmin; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_2__);
@@ -798,16 +799,17 @@ function relativeTime(timestamp) {
     return `${minutes} minutes ago`;
   }
 
   return new Date(timestamp).toLocaleString();
 }
 
 const LAYOUT_VARIANTS = {
   basic: "Basic default layout (on by default in nightly)",
+  staging_spocs: "A layout with all spocs shown",
   "dev-test-all": "A little bit of everything. Good layout for testing all components",
   "dev-test-feeds": "Stress testing for slow feeds"
 };
 class ToggleStoryButton extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.handleClick = this.handleClick.bind(this);
   }
@@ -818,20 +820,41 @@ class ToggleStoryButton extends react__W
 
   render() {
     return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
       onClick: this.handleClick
     }, "collapse/open");
   }
 
 }
+class TogglePrefCheckbox extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureComponent {
+  constructor(props) {
+    super(props);
+    this.onChange = this.onChange.bind(this);
+  }
+
+  onChange(event) {
+    this.props.onChange(this.props.pref, event.target.checked);
+  }
+
+  render() {
+    return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
+      type: "checkbox",
+      checked: this.props.checked,
+      onChange: this.onChange
+    }), " ", this.props.pref, " ");
+  }
+
+}
 class DiscoveryStreamAdmin extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureComponent {
   constructor(props) {
     super(props);
-    this.onEnableToggle = this.onEnableToggle.bind(this);
+    this.restorePrefDefaults = this.restorePrefDefaults.bind(this);
+    this.setConfigValue = this.setConfigValue.bind(this);
+    this.expireCache = this.expireCache.bind(this);
     this.changeEndpointVariant = this.changeEndpointVariant.bind(this);
     this.onStoryToggle = this.onStoryToggle.bind(this);
     this.state = {
       toggledStories: {}
     };
   }
 
   setConfigValue(name, value) {
@@ -839,18 +862,30 @@ class DiscoveryStreamAdmin extends react
       type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].DISCOVERY_STREAM_CONFIG_SET_VALUE,
       data: {
         name,
         value
       }
     }));
   }
 
-  onEnableToggle(event) {
-    this.setConfigValue("enabled", event.target.checked);
+  restorePrefDefaults(event) {
+    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
+      type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS
+    }));
+  }
+
+  expireCache() {
+    const {
+      config
+    } = this.props.state;
+    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
+      type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].DISCOVERY_STREAM_CONFIG_CHANGE,
+      data: config
+    }));
   }
 
   changeEndpointVariant(event) {
     const endpoint = this.props.state.config.layout_endpoint;
 
     if (endpoint) {
       this.setConfigValue("layout_endpoint", endpoint.replace(/layout_variant=.+/, `layout_variant=${event.target.value}`));
     }
@@ -945,29 +980,36 @@ class DiscoveryStreamAdmin extends react
     return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(Row, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", {
       className: "min"
     }, "Feed url"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, feed.url)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(Row, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", {
       className: "min"
     }, "Data last fetched"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, relativeTime(feeds.data[feed.url] ? feeds.data[feed.url].lastUpdated : null) || "(no data)")));
   }
 
   render() {
+    const prefToggles = "enabled hardcoded_layout show_spocs personalized collapsible".split(" ");
     const {
       config,
       lastUpdated,
       layout
     } = this.props.state;
-    return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
-      className: "dsEnabled"
-    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
-      type: "checkbox",
-      checked: config.enabled,
-      onChange: this.onEnableToggle
-    }), " ", "enabled", " "), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h3", null, "Endpoint variant"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", null, "You can also change this manually by changing this pref:", " ", react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("code", null, "browser.newtabpage.activity-stream.discoverystream.config")), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("table", {
-      style: config.enabled ? null : {
+    return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
+      className: "button",
+      onClick: this.restorePrefDefaults
+    }, "Restore Pref Defaults"), " ", react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
+      className: "button",
+      onClick: this.expireCache
+    }, "Expire Cache"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("table", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tbody", null, prefToggles.map(pref => react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(Row, {
+      key: pref
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(TogglePrefCheckbox, {
+      checked: config[pref],
+      pref: pref,
+      onChange: this.setConfigValue
+    })))))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h3", null, "Endpoint variant"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", null, "You can also change this manually by changing this pref:", " ", react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("code", null, "browser.newtabpage.activity-stream.discoverystream.config")), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("table", {
+      style: config.enabled && !config.hardcoded_layout ? null : {
         opacity: 0.5
       }
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tbody", null, Object.keys(LAYOUT_VARIANTS).map(id => react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(Row, {
       key: id
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", {
       className: "min"
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       type: "radio",
@@ -1714,26 +1756,26 @@ const ASRouterAdmin = Object(react_redux
 /***/ (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__, "ASRouterUtils", function() { return ASRouterUtils; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterUISurface", function() { return ASRouterUISurface; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6);
-/* harmony import */ var _rich_text_strings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55);
+/* harmony import */ var _rich_text_strings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(58);
 /* harmony import */ var _components_ImpressionsWrapper_ImpressionsWrapper__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8);
-/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(53);
+/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(56);
 /* harmony import */ var content_src_lib_constants__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(11);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
 /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(12);
 /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_7__);
-/* harmony import */ var _templates_template_manifest__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(54);
-/* harmony import */ var _templates_FirstRun_FirstRun__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(58);
+/* harmony import */ var _templates_template_manifest__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(57);
+/* harmony import */ var _templates_FirstRun_FirstRun__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(60);
 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); }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
@@ -1857,26 +1899,86 @@ function shouldSendImpressionOnUpdate(ne
 
 class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onMessageFromParent = this.onMessageFromParent.bind(this);
     this.sendClick = this.sendClick.bind(this);
     this.sendImpression = this.sendImpression.bind(this);
     this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
+    this.onUserAction = this.onUserAction.bind(this);
+    this.fetchFlowParams = this.fetchFlowParams.bind(this);
     this.state = {
       message: {}
     };
 
     if (props.document) {
       this.headerPortal = props.document.getElementById("header-asrouter-container");
       this.footerPortal = props.document.getElementById("footer-asrouter-container");
     }
   }
 
+  async fetchFlowParams(params = {}) {
+    let result = {};
+    const {
+      fxaEndpoint,
+      dispatch
+    } = this.props;
+
+    if (!fxaEndpoint) {
+      const err = "Tried to fetch flow params before fxaEndpoint pref was ready";
+      console.error(err); // eslint-disable-line no-console
+    }
+
+    try {
+      const urlObj = new URL(fxaEndpoint);
+      urlObj.pathname = "metrics-flow";
+      Object.keys(params).forEach(key => {
+        urlObj.searchParams.append(key, params[key]);
+      });
+      const response = await fetch(urlObj.toString(), {
+        credentials: "omit"
+      });
+
+      if (response.status === 200) {
+        const {
+          deviceId,
+          flowId,
+          flowBeginTime
+        } = await response.json();
+        result = {
+          deviceId,
+          flowId,
+          flowBeginTime
+        };
+      } else {
+        console.error("Non-200 response", response); // eslint-disable-line no-console
+
+        dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
+          type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
+          data: {
+            event: "FXA_METRICS_FETCH_ERROR",
+            value: response.status
+          }
+        }));
+      }
+    } catch (error) {
+      console.error(error); // eslint-disable-line no-console
+
+      dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
+        type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
+        data: {
+          event: "FXA_METRICS_ERROR"
+        }
+      }));
+    }
+
+    return result;
+  }
+
   sendUserActionTelemetry(extraProps = {}) {
     const {
       message
     } = this.state;
     const eventType = `${message.provider}_user_event`;
     ASRouterUtils.sendTelemetry({
       message_id: message.id,
       source: extraProps.id,
@@ -2017,16 +2119,49 @@ class ASRouterUISurface extends react__W
       });
     }
   }
 
   componentWillUnmount() {
     ASRouterUtils.removeListener(this.onMessageFromParent);
   }
 
+  async getMonitorUrl({
+    url,
+    flowRequestParams = {}
+  }) {
+    const flowValues = await this.fetchFlowParams(flowRequestParams); // Note that flowParams are actually added dynamically on the page
+
+    const urlObj = new URL(url);
+    ["deviceId", "flowId", "flowBeginTime"].forEach(key => {
+      if (key in flowValues) {
+        urlObj.searchParams.append(key, flowValues[key]);
+      }
+    });
+    return urlObj.toString();
+  }
+
+  async onUserAction(action) {
+    switch (action.type) {
+      // This needs to be handled locally because its
+      case common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["ASRouterActions"].ENABLE_FIREFOX_MONITOR:
+        const url = await this.getMonitorUrl(action.data.args);
+        ASRouterUtils.executeAction({
+          type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["ASRouterActions"].OPEN_URL,
+          data: {
+            args: url
+          }
+        });
+        break;
+
+      default:
+        ASRouterUtils.executeAction(action);
+    }
+  }
+
   renderSnippets() {
     const {
       message
     } = this.state;
 
     if (!_templates_template_manifest__WEBPACK_IMPORTED_MODULE_8__["SnippetsTemplates"][message.template]) {
       return null;
     }
@@ -2043,17 +2178,17 @@ class ASRouterUISurface extends react__W
       ,
       document: this.props.document
     }, react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(fluent_react__WEBPACK_IMPORTED_MODULE_4__["LocalizationProvider"], {
       bundles: Object(_rich_text_strings__WEBPACK_IMPORTED_MODULE_2__["generateBundles"])(content)
     }, react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(SnippetComponent, _extends({}, this.state.message, {
       UISurface: "NEWTAB_FOOTER_BAR",
       onBlock: this.onBlockById(this.state.message.id),
       onDismiss: this.onDismissById(this.state.message.id),
-      onAction: ASRouterUtils.executeAction,
+      onAction: this.onUserAction,
       sendClick: this.sendClick,
       sendUserActionTelemetry: this.sendUserActionTelemetry
     }))));
   }
 
   renderPreviewBanner() {
     if (this.state.message.provider !== "preview") {
       return null;
@@ -2082,17 +2217,18 @@ class ASRouterUISurface extends react__W
       }, react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(_templates_FirstRun_FirstRun__WEBPACK_IMPORTED_MODULE_9__["FirstRun"], {
         document: this.props.document,
         message: message,
         sendUserActionTelemetry: this.sendUserActionTelemetry,
         executeAction: ASRouterUtils.executeAction,
         dispatch: this.props.dispatch,
         onBlock: this.onBlockById(this.state.message.id),
         onDismiss: this.onDismissById(this.state.message.id),
-        fxaEndpoint: this.props.fxaEndpoint
+        fxaEndpoint: this.props.fxaEndpoint,
+        fetchFlowParams: this.fetchFlowParams
       }));
     }
 
     return null;
   }
 
   render() {
     const {
@@ -2436,20 +2572,20 @@ module.exports = {"title":"EOYSnippet","
 /***/ }),
 /* 14 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "convertLinks", function() { return convertLinks; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RichText", function() { return RichText; });
-/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(53);
+/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(56);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
-/* harmony import */ var _rich_text_strings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55);
+/* harmony import */ var _rich_text_strings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(58);
 /* harmony import */ var _template_utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(15);
 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); }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
@@ -2535,17 +2671,17 @@ function safeURI(url) {
 
   return isAllowed ? url : "";
 }
 
 /***/ }),
 /* 16 */
 /***/ (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."},"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"]}};
+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":{"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"]}};
 
 /***/ }),
 /* 17 */
 /***/ (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"]}};
 
 /***/ }),
@@ -2930,16 +3066,17 @@ class ModalOverlay extends react__WEBPAC
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
 /* 22 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BASE_PARAMS", function() { return BASE_PARAMS; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "addUtmParams", function() { return addUtmParams; });
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 const BASE_PARAMS = {
   utm_source: "activity-stream",
   utm_campaign: "firstrun",
   utm_medium: "referral"
@@ -4115,24 +4252,67 @@ ImpressionStats.defaultProps = {
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
 /* 33 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "cardContextTypes", function() { return cardContextTypes; });
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+const cardContextTypes = {
+  history: {
+    fluentID: "newtab-label-visited",
+    icon: "history-item"
+  },
+  removedBookmark: {
+    fluentID: "newtab-label-removed-bookmark",
+    icon: "bookmark-removed"
+  },
+  bookmark: {
+    fluentID: "newtab-label-bookmarked",
+    icon: "bookmark-added"
+  },
+  trending: {
+    fluentID: "newtab-label-recommended",
+    icon: "trending"
+  },
+  pocket: {
+    fluentID: "newtab-label-saved",
+    icon: "pocket"
+  },
+  download: {
+    fluentID: "newtab-label-download",
+    icon: "download"
+  }
+};
+
+/***/ }),
+/* 34 */
+/***/ (function(module, exports) {
+
+module.exports = ReactTransitionGroup;
+
+/***/ }),
+/* 35 */
+/***/ (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__, "CollapsibleSection", function() { return CollapsibleSection; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(34);
-/* harmony import */ var content_src_components_FluentOrText_FluentOrText__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(36);
+/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(36);
+/* harmony import */ var content_src_components_FluentOrText_FluentOrText__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(38);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
-/* harmony import */ var content_src_components_SectionMenu_SectionMenu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(37);
-/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(38);
+/* harmony import */ var content_src_components_SectionMenu_SectionMenu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(39);
+/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(40);
 /* harmony import */ var content_src_components_ContextMenu_ContextMenuButton__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(31);
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
 
@@ -4399,24 +4579,24 @@ CollapsibleSection.defaultProps = {
   },
   Prefs: {
     values: {}
   }
 };
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 34 */
+/* 36 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ErrorBoundaryFallback", function() { return ErrorBoundaryFallback; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ErrorBoundary", function() { return ErrorBoundary; });
-/* harmony import */ var content_src_components_A11yLinkButton_A11yLinkButton__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(35);
+/* harmony import */ var content_src_components_A11yLinkButton_A11yLinkButton__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(37);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 class ErrorBoundaryFallback extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
@@ -4486,17 +4666,17 @@ class ErrorBoundary extends react__WEBPA
   }
 
 }
 ErrorBoundary.defaultProps = {
   FallbackComponent: ErrorBoundaryFallback
 };
 
 /***/ }),
-/* 35 */
+/* 37 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "A11yLinkButton", function() { return A11yLinkButton; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 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); }
@@ -4516,17 +4696,17 @@ function A11yLinkButton(props) {
   return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", _extends({
     type: "button"
   }, props, {
     className: className
   }), props.children);
 }
 
 /***/ }),
-/* 36 */
+/* 38 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FluentOrText", function() { return FluentOrText; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 /* This Source Code Form is subject to the terms of the Mozilla Public
@@ -4562,28 +4742,28 @@ class FluentOrText extends react__WEBPAC
 
 
     return react__WEBPACK_IMPORTED_MODULE_0___default.a.cloneElement(child, extraProps, grandChildren);
   }
 
 }
 
 /***/ }),
-/* 37 */
+/* 39 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_SectionMenu", function() { return _SectionMenu; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionMenu", function() { return SectionMenu; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var content_src_components_ContextMenu_ContextMenu__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
-/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(38);
+/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(40);
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
 
 const DEFAULT_SECTION_MENU_OPTIONS = ["MoveUp", "MoveDown", "Separator", "RemoveSection", "CheckCollapsed", "Separator", "ManageSection"];
@@ -4668,17 +4848,17 @@ class _SectionMenu extends react__WEBPAC
       keyboardAccess: this.props.keyboardAccess
     });
   }
 
 }
 const SectionMenu = _SectionMenu;
 
 /***/ }),
-/* 38 */
+/* 40 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionMenuOptions", function() { return SectionMenuOptions; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -4798,38 +4978,38 @@ const SectionMenuOptions = {
       }
     }),
     userEvent: "MENU_PRIVACY_NOTICE"
   }),
   CheckCollapsed: section => section.collapsed ? SectionMenuOptions.ExpandSection(section) : SectionMenuOptions.CollapseSection(section)
 };
 
 /***/ }),
-/* 39 */
+/* 41 */
 /***/ (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__(57);
-/* 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__(41);
-/* harmony import */ var content_src_components_FluentOrText_FluentOrText__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(36);
+/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(42);
+/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(35);
+/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(44);
+/* harmony import */ var content_src_components_FluentOrText_FluentOrText__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(38);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(27);
 /* 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__(43);
-/* harmony import */ var content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(44);
+/* harmony import */ var content_src_components_MoreRecommendations_MoreRecommendations__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(46);
+/* harmony import */ var content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(47);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(9);
 /* 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__(45);
-/* harmony import */ var content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(46);
+/* harmony import */ var content_src_components_Topics_Topics__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(48);
+/* harmony import */ var content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(49);
 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); }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
@@ -5169,17 +5349,362 @@ class _Sections extends react__WEBPACK_I
 }
 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)))
 
 /***/ }),
-/* 40 */
+/* 42 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Card", function() { return _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; });
+/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
+/* harmony import */ var _types__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(33);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_2__);
+/* harmony import */ var content_src_components_ContextMenu_ContextMenuButton__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(31);
+/* harmony import */ var content_src_components_LinkMenu_LinkMenu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(61);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(9);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_5__);
+/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(43);
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+ // Keep track of pending image loads to only request once
+
+const gImageLoading = new Map();
+/**
+ * Card component.
+ * Cards are found within a Section component and contain information about a link such
+ * as preview image, page title, page description, and some context about if the page
+ * was visited, bookmarked, trending etc...
+ * Each Section can make an unordered list of Cards which will create one instane of
+ * this class. Each card will then get a context menu which reflects the actions that
+ * can be done on this Card.
+ */
+
+class _Card extends react__WEBPACK_IMPORTED_MODULE_5___default.a.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      activeCard: null,
+      imageLoaded: false,
+      cardImage: null
+    };
+    this.onMenuButtonUpdate = this.onMenuButtonUpdate.bind(this);
+    this.onLinkClick = this.onLinkClick.bind(this);
+  }
+  /**
+   * Helper to conditionally load an image and update state when it loads.
+   */
+
+
+  async maybeLoadImage() {
+    // No need to load if it's already loaded or no image
+    const {
+      cardImage
+    } = this.state;
+
+    if (!cardImage) {
+      return;
+    }
+
+    const imageUrl = cardImage.url;
+
+    if (!this.state.imageLoaded) {
+      // Initialize a promise to share a load across multiple card updates
+      if (!gImageLoading.has(imageUrl)) {
+        const loaderPromise = new Promise((resolve, reject) => {
+          const loader = new Image();
+          loader.addEventListener("load", resolve);
+          loader.addEventListener("error", reject);
+          loader.src = imageUrl;
+        }); // Save and remove the promise only while it's pending
+
+        gImageLoading.set(imageUrl, loaderPromise);
+        loaderPromise.catch(ex => ex).then(() => gImageLoading.delete(imageUrl)).catch();
+      } // Wait for the image whether just started loading or reused promise
+
+
+      await gImageLoading.get(imageUrl); // Only update state if we're still waiting to load the original image
+
+      if (content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_6__["ScreenshotUtils"].isRemoteImageLocal(this.state.cardImage, this.props.link.image) && !this.state.imageLoaded) {
+        this.setState({
+          imageLoaded: true
+        });
+      }
+    }
+  }
+  /**
+   * Helper to obtain the next state based on nextProps and prevState.
+   *
+   * NOTE: Rename this method to getDerivedStateFromProps when we update React
+   *       to >= 16.3. We will need to update tests as well. We cannot rename this
+   *       method to getDerivedStateFromProps now because there is a mismatch in
+   *       the React version that we are using for both testing and production.
+   *       (i.e. react-test-render => "16.3.2", react => "16.2.0").
+   *
+   * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43.
+   */
+
+
+  static getNextStateFromProps(nextProps, prevState) {
+    const {
+      image
+    } = nextProps.link;
+    const imageInState = content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_6__["ScreenshotUtils"].isRemoteImageLocal(prevState.cardImage, image);
+    let nextState = null; // Image is updating.
+
+    if (!imageInState && nextProps.link) {
+      nextState = {
+        imageLoaded: false
+      };
+    }
+
+    if (imageInState) {
+      return nextState;
+    } // Since image was updated, attempt to revoke old image blob URL, if it exists.
+
+
+    content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_6__["ScreenshotUtils"].maybeRevokeBlobObjectURL(prevState.cardImage);
+    nextState = nextState || {};
+    nextState.cardImage = content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_6__["ScreenshotUtils"].createLocalImageObject(image);
+    return nextState;
+  }
+
+  onMenuButtonUpdate(isOpen) {
+    if (isOpen) {
+      this.setState({
+        activeCard: this.props.index
+      });
+    } else {
+      this.setState({
+        activeCard: null
+      });
+    }
+  }
+  /**
+   * Report to telemetry additional information about the item.
+   */
+
+
+  _getTelemetryInfo() {
+    // Filter out "history" type for being the default
+    if (this.props.link.type !== "history") {
+      return {
+        value: {
+          card_type: this.props.link.type
+        }
+      };
+    }
+
+    return null;
+  }
+
+  onLinkClick(event) {
+    event.preventDefault();
+
+    if (this.props.link.type === "download") {
+      this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
+        type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SHOW_DOWNLOAD_FILE,
+        data: this.props.link
+      }));
+    } else {
+      const {
+        altKey,
+        button,
+        ctrlKey,
+        metaKey,
+        shiftKey
+      } = event;
+      this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
+        type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].OPEN_LINK,
+        data: Object.assign(this.props.link, {
+          event: {
+            altKey,
+            button,
+            ctrlKey,
+            metaKey,
+            shiftKey
+          }
+        })
+      }));
+    }
+
+    if (this.props.isWebExtension) {
+      this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].WebExtEvent(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].WEBEXT_CLICK, {
+        source: this.props.eventSource,
+        url: this.props.link.url,
+        action_position: this.props.index
+      }));
+    } else {
+      this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent(Object.assign({
+        event: "CLICK",
+        source: this.props.eventSource,
+        action_position: this.props.index
+      }, this._getTelemetryInfo())));
+
+      if (this.props.shouldSendImpressionStats) {
+        this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ImpressionStats({
+          source: this.props.eventSource,
+          click: 0,
+          tiles: [{
+            id: this.props.link.guid,
+            pos: this.props.index
+          }]
+        }));
+      }
+    }
+  }
+
+  componentDidMount() {
+    this.maybeLoadImage();
+  }
+
+  componentDidUpdate() {
+    this.maybeLoadImage();
+  } // NOTE: Remove this function when we update React to >= 16.3 since React will
+  //       call getDerivedStateFromProps automatically. We will also need to
+  //       rename getNextStateFromProps to getDerivedStateFromProps.
+
+
+  componentWillMount() {
+    const nextState = _Card.getNextStateFromProps(this.props, this.state);
+
+    if (nextState) {
+      this.setState(nextState);
+    }
+  } // NOTE: Remove this function when we update React to >= 16.3 since React will
+  //       call getDerivedStateFromProps automatically. We will also need to
+  //       rename getNextStateFromProps to getDerivedStateFromProps.
+
+
+  componentWillReceiveProps(nextProps) {
+    const nextState = _Card.getNextStateFromProps(nextProps, this.state);
+
+    if (nextState) {
+      this.setState(nextState);
+    }
+  }
+
+  componentWillUnmount() {
+    content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_6__["ScreenshotUtils"].maybeRevokeBlobObjectURL(this.state.cardImage);
+  }
+
+  render() {
+    const {
+      index,
+      className,
+      link,
+      dispatch,
+      contextMenuOptions,
+      eventSource,
+      shouldSendImpressionStats
+    } = this.props;
+    const {
+      props
+    } = this;
+    const title = link.title || link.hostname;
+    const isContextMenuOpen = this.state.activeCard === index; // Display "now" as "trending" until we have new strings #3402
+
+    const {
+      icon,
+      fluentID
+    } = _types__WEBPACK_IMPORTED_MODULE_1__["cardContextTypes"][link.type === "now" ? "trending" : link.type] || {};
+    const hasImage = this.state.cardImage || link.hasImage;
+    const imageStyle = {
+      backgroundImage: this.state.cardImage ? `url(${this.state.cardImage.url})` : "none"
+    };
+    const outerClassName = ["card-outer", className, isContextMenuOpen && "active", props.placeholder && "placeholder"].filter(v => v).join(" ");
+    return react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("li", {
+      className: outerClassName
+    }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("a", {
+      href: link.type === "pocket" ? link.open_url : link.url,
+      onClick: !props.placeholder ? this.onLinkClick : undefined
+    }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: "card"
+    }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: "card-preview-image-outer"
+    }, hasImage && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: `card-preview-image${this.state.imageLoaded ? " loaded" : ""}`,
+      style: imageStyle
+    })), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: "card-details"
+    }, link.type === "download" && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: "card-host-name alternate",
+      "data-l10n-id": "newtab-menu-show-file"
+    }), link.hostname && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: "card-host-name"
+    }, link.hostname.slice(0, 100), link.type === "download" && `  \u2014 ${link.description}`), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: ["card-text", icon ? "" : "no-context", link.description ? "" : "no-description", link.hostname ? "" : "no-host-name"].join(" ")
+    }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("h4", {
+      className: "card-title",
+      dir: "auto"
+    }, link.title), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("p", {
+      className: "card-description",
+      dir: "auto"
+    }, link.description)), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: "card-context"
+    }, icon && !link.context && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("span", {
+      "aria-haspopup": "true",
+      className: `card-context-icon icon icon-${icon}`
+    }), link.icon && link.context && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("span", {
+      "aria-haspopup": "true",
+      className: "card-context-icon icon",
+      style: {
+        backgroundImage: `url('${link.icon}')`
+      }
+    }), fluentID && !link.context && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: "card-context-label",
+      "data-l10n-id": fluentID
+    }), link.context && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement("div", {
+      className: "card-context-label"
+    }, link.context))))), !props.placeholder && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(content_src_components_ContextMenu_ContextMenuButton__WEBPACK_IMPORTED_MODULE_3__["ContextMenuButton"], {
+      tooltip: "newtab-menu-content-tooltip",
+      tooltipArgs: {
+        title
+      },
+      onUpdate: this.onMenuButtonUpdate
+    }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(content_src_components_LinkMenu_LinkMenu__WEBPACK_IMPORTED_MODULE_4__["LinkMenu"], {
+      dispatch: dispatch,
+      index: index,
+      source: eventSource,
+      options: link.contextMenuOptions || contextMenuOptions,
+      site: link,
+      siteInfo: this._getTelemetryInfo(),
+      shouldSendImpressionStats: shouldSendImpressionStats
+    })));
+  }
+
+}
+_Card.defaultProps = {
+  link: {}
+};
+const Card = Object(react_redux__WEBPACK_IMPORTED_MODULE_2__["connect"])(state => ({
+  platform: state.Prefs.values.platform
+}))(_Card);
+const PlaceholderCard = props => react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(Card, {
+  placeholder: true,
+  className: props.className
+});
+
+/***/ }),
+/* 43 */
 /***/ (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; });
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -5238,24 +5763,24 @@ const ScreenshotUtils = {
 
     return !remoteImage && !localImage;
   }
 
 };
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 41 */
+/* 44 */
 /***/ (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; });
 /* 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__(42);
+/* harmony import */ var common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(45);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
  // Currently record only a fixed set of sections. This will prevent data
@@ -5418,17 +5943,17 @@ class ComponentPerfTimer extends react__
     }
 
     return this.props.children;
   }
 
 }
 
 /***/ }),
-/* 42 */
+/* 45 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_PerfService", function() { return _PerfService; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "perfService", function() { return perfService; });
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -5550,17 +6075,17 @@ function _PerfService(options) {
     let mostRecentEntry = entries[entries.length - 1];
     return this._perf.timeOrigin + mostRecentEntry.startTime;
   }
 
 };
 var perfService = new _PerfService();
 
 /***/ }),
-/* 43 */
+/* 46 */
 /***/ (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__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 /* This Source Code Form is subject to the terms of the Mozilla Public
@@ -5582,17 +6107,17 @@ class MoreRecommendations extends react_
     }
 
     return null;
   }
 
 }
 
 /***/ }),
-/* 44 */
+/* 47 */
 /***/ (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__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_0__);
@@ -5625,17 +6150,17 @@ class _PocketLoggedInCta extends react__
   }
 
 }
 const PocketLoggedInCta = Object(react_redux__WEBPACK_IMPORTED_MODULE_0__["connect"])(state => ({
   Pocket: state.Pocket
 }))(_PocketLoggedInCta);
 
 /***/ }),
-/* 45 */
+/* 48 */
 /***/ (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__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
@@ -5670,36 +6195,36 @@ class Topics extends react__WEBPACK_IMPO
       url: t.url,
       name: t.name
     }))));
   }
 
 }
 
 /***/ }),
-/* 46 */
+/* 49 */
 /***/ (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__(47);
-/* 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__(41);
+/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(50);
+/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(35);
+/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(44);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
 /* harmony import */ var _asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(21);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
-/* harmony import */ var _SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(48);
-/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(56);
-/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(59);
-/* harmony import */ var _TopSite__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(49);
+/* harmony import */ var _SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(51);
+/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(59);
+/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(62);
+/* harmony import */ var _TopSite__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(52);
 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); }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
@@ -5896,17 +6421,17 @@ class _TopSites extends react__WEBPACK_I
 const TopSites = Object(react_redux__WEBPACK_IMPORTED_MODULE_4__["connect"])(state => ({
   TopSites: state.TopSites,
   Prefs: state.Prefs,
   TopSitesRows: state.Prefs.values.topSitesRows
 }))(_TopSites);
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 47 */
+/* 50 */
 /***/ (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; });
@@ -5919,27 +6444,27 @@ const TOP_SITES_CONTEXT_MENU_OPTIONS = [
 
 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;
 
 /***/ }),
-/* 48 */
+/* 51 */
 /***/ (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__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
-/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(47);
+/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(50);
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
 class SelectableSearchShortcut extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
   render() {
@@ -6110,32 +6635,32 @@ class SearchShortcutsForm extends react_
       onClick: this.onSaveButtonClick,
       "data-l10n-id": "newtab-topsites-save-button"
     })));
   }
 
 }
 
 /***/ }),
-/* 49 */
+/* 52 */
 /***/ (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 import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(47);
-/* harmony import */ var content_src_components_LinkMenu_LinkMenu__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(60);
+/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(50);
+/* harmony import */ var content_src_components_LinkMenu_LinkMenu__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(61);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
-/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(40);
-/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(56);
+/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(43);
+/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(59);
 /* harmony import */ var content_src_components_ContextMenu_ContextMenuButton__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(31);
 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); }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
@@ -6753,17 +7278,17 @@ class TopSiteList extends react__WEBPACK
     return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("ul", {
       className: `top-sites-list${this.state.draggedSite ? " dnd-active" : ""}`
     }, topSitesUI);
   }
 
 }
 
 /***/ }),
-/* 50 */
+/* 53 */
 /***/ (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_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27);
@@ -6941,24 +7466,24 @@ class _Search extends react__WEBPACK_IMP
       ref: this.onInputMount
     })));
   }
 
 }
 const Search = Object(react_redux__WEBPACK_IMPORTED_MODULE_1__["connect"])()(_Search);
 
 /***/ }),
-/* 51 */
+/* 54 */
 /***/ (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__(42);
+/* harmony import */ var common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(45);
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 const VISIBLE = "visible";
 const VISIBILITY_CHANGE_EVENT = "visibilitychange";
 class DetectUserSessionStart {
@@ -7023,17 +7548,17 @@ class DetectUserSessionStart {
       this.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
     }
   }
 
 }
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 52 */
+/* 55 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: ./common/Actions.jsm
 var Actions = __webpack_require__(2);
 
@@ -7163,17 +7688,17 @@ DSImage_DSImage.defaultProps = {
   rawSource: null,
   // Unadulterated image URL to filter through Thumbor
   extraClassNames: null,
   // Additional classnames to append to component
   optimize: true // Measure parent container to request exact sizes
 
 };
 // EXTERNAL MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx + 1 modules
-var LinkMenu = __webpack_require__(60);
+var LinkMenu = __webpack_require__(61);
 
 // EXTERNAL MODULE: ./content-src/components/ContextMenu/ContextMenuButton.jsx
 var ContextMenuButton = __webpack_require__(31);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -7326,26 +7851,83 @@ class SafeAnchor_SafeAnchor extends exte
     return external_React_default.a.createElement("a", {
       href: this.safeURI(url),
       className: className,
       onClick: this.onClick
     }, this.props.children);
   }
 
 }
+// EXTERNAL MODULE: ./content-src/components/Card/types.js
+var types = __webpack_require__(33);
+
+// EXTERNAL MODULE: external "ReactTransitionGroup"
+var external_ReactTransitionGroup_ = __webpack_require__(34);
+
+// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+ // Animation time is mirrored in DSContextFooter.scss
+
+const ANIMATION_DURATION = 3000;
+
+const StatusMessage = ({
+  icon,
+  fluentID
+}) => external_React_default.a.createElement("div", {
+  className: "status-message"
+}, external_React_default.a.createElement("span", {
+  "aria-haspopup": "true",
+  className: `story-badge-icon icon icon-${icon}`
+}), external_React_default.a.createElement("div", {
+  className: "story-context-label",
+  "data-l10n-id": fluentID
+}));
+
+class DSContextFooter_DSContextFooter extends external_React_default.a.PureComponent {
+  render() {
+    const {
+      context,
+      context_type
+    } = this.props;
+    const {
+      icon,
+      fluentID
+    } = types["cardContextTypes"][context_type] || {};
+    return external_React_default.a.createElement("div", {
+      className: "story-footer"
+    }, context && external_React_default.a.createElement("p", {
+      className: "story-sponsored-label"
+    }, context), external_React_default.a.createElement(external_ReactTransitionGroup_["TransitionGroup"], {
+      component: null
+    }, !context && context_type && external_React_default.a.createElement(external_ReactTransitionGroup_["CSSTransition"], {
+      key: fluentID,
+      timeout: ANIMATION_DURATION,
+      classNames: "story-animate"
+    }, external_React_default.a.createElement(StatusMessage, {
+      icon: icon,
+      fluentID: fluentID
+    }))));
+  }
+
+}
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
 
 
 
+
 class DSCard_DSCard extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onLinkClick = this.onLinkClick.bind(this);
   }
 
   onLinkClick(event) {
     if (this.props.dispatch) {
@@ -7387,19 +7969,20 @@ class DSCard_DSCard extends external_Rea
     }, external_React_default.a.createElement("div", {
       className: "info-wrap"
     }, external_React_default.a.createElement("p", {
       className: "source clamp"
     }, this.props.source), external_React_default.a.createElement("header", {
       className: "title clamp"
     }, this.props.title), this.props.excerpt && external_React_default.a.createElement("p", {
       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"], {
+    }, this.props.excerpt)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
+      context_type: this.props.context_type,
+      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 && this.props.shim.impression ? {
           shim: this.props.shim.impression
         } : {})
       }],
@@ -7544,16 +8127,17 @@ class CardGrid_CardGrid extends external
         url: rec.url,
         id: rec.id,
         shim: rec.shim,
         type: this.props.type,
         context: rec.context,
         dispatch: this.props.dispatch,
         source: rec.domain,
         pocket_id: rec.pocket_id,
+        context_type: rec.context_type,
         bookmarkGuid: rec.bookmarkGuid
       }));
     }
 
     let divisibility = ``;
 
     if (this.props.items % 4 === 0) {
       divisibility = `divisible-by-4`;
@@ -7590,17 +8174,17 @@ class CardGrid_CardGrid extends external
 
 }
 CardGrid_CardGrid.defaultProps = {
   border: `border`,
   items: 4 // Number of stories to display
 
 };
 // EXTERNAL MODULE: ./content-src/components/CollapsibleSection/CollapsibleSection.jsx
-var CollapsibleSection = __webpack_require__(33);
+var CollapsibleSection = __webpack_require__(35);
 
 // EXTERNAL MODULE: external "ReactRedux"
 var external_ReactRedux_ = __webpack_require__(27);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage.jsx
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -7963,17 +8547,17 @@ 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__(39);
+var Sections = __webpack_require__(41);
 
 // 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); }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
@@ -8274,17 +8858,17 @@ const selectLayoutRender = (state, prefs
   }
 
   return {
     spocsFill,
     layoutRender
   };
 };
 // EXTERNAL MODULE: ./content-src/components/TopSites/TopSites.jsx
-var TopSites = __webpack_require__(46);
+var TopSites = __webpack_require__(49);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/TopSites/TopSites.jsx
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
@@ -8603,17 +9187,17 @@ class DiscoveryStreamBase_DiscoveryStrea
 }
 const DiscoveryStreamBase = Object(external_ReactRedux_["connect"])(state => ({
   DiscoveryStream: state.DiscoveryStream,
   Prefs: state.Prefs,
   Sections: state.Sections
 }))(DiscoveryStreamBase_DiscoveryStreamBase);
 
 /***/ }),
-/* 53 */
+/* 56 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(9);
 
@@ -9400,17 +9984,17 @@ localized_Localized.propTypes = {
  * components for more information.
  */
 
 
 
 
 
 /***/ }),
-/* 54 */
+/* 57 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(9);
 var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
@@ -10456,30 +11040,30 @@ class SimpleBelowSearchSnippet_SimpleBel
     const {
       title
     } = this.props.content;
     return title ? external_React_default.a.createElement("h3", {
       className: `title ${this._shouldRenderButton() ? "title-inline" : ""}`
     }, title) : null;
   }
 
-  onButtonClick() {
+  async onButtonClick() {
     if (this.props.provider !== "preview") {
       this.props.sendUserActionTelemetry({
         event: "CLICK_BUTTON",
         id: this.props.UISurface
       });
     }
 
     const {
       button_url
     } = this.props.content; // If button_url is defined handle it as OPEN_URL action
 
     const type = this.props.content.button_action || button_url && "OPEN_URL";
-    this.props.onAction({
+    await this.props.onAction({
       type,
       data: {
         args: this.props.content.button_action_args || button_url
       }
     });
 
     if (!this.props.content.do_not_autoblock) {
       this.props.onBlock();
@@ -10564,17 +11148,17 @@ const SnippetsTemplates = {
   newsletter_snippet: NewsletterSnippet,
   fxa_signup_snippet: FXASignupSnippet,
   send_to_device_snippet: SendToDeviceSnippet,
   eoy_snippet: EOYSnippet,
   simple_below_search_snippet: SimpleBelowSearchSnippet_SimpleBelowSearchSnippet
 };
 
 /***/ }),
-/* 55 */
+/* 58 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // CONCATENATED MODULE: ./node_modules/fluent/src/types.js
 /* global Intl */
 
@@ -11984,17 +12568,17 @@ function generateBundles(content) {
     }
 
     bundle.addMessages(`${key} = ${string}`);
   });
   return [bundle];
 }
 
 /***/ }),
-/* 56 */
+/* 59 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: ./common/Actions.jsm
 var Actions = __webpack_require__(2);
 
@@ -12753,17 +13337,18 @@ function DiscoveryStream(prevState = INI
     case Actions["actionTypes"].DISCOVERY_STREAM_LINK_BLOCKED:
       return isNotReady() ? prevState : nextState(items => items.filter(item => item.url !== action.data.url));
 
     case Actions["actionTypes"].PLACES_SAVED_TO_POCKET:
       const addPocketInfo = item => {
         if (item.url === action.data.url) {
           return Object.assign({}, item, {
             open_url: action.data.open_url,
-            pocket_id: action.data.pocket_id
+            pocket_id: action.data.pocket_id,
+            context_type: "pocket"
           });
         }
 
         return item;
       };
 
       return isNotReady() ? prevState : nextState(items => items.map(addPocketInfo));
 
@@ -12777,32 +13362,38 @@ function DiscoveryStream(prevState = INI
           const {
             bookmarkGuid,
             bookmarkTitle,
             dateAdded
           } = action.data;
           return Object.assign({}, item, {
             bookmarkGuid,
             bookmarkTitle,
-            bookmarkDateCreated: dateAdded
+            bookmarkDateCreated: dateAdded,
+            context_type: "bookmark"
           });
         }
 
         return item;
       };
 
       return isNotReady() ? prevState : nextState(items => items.map(updateBookmarkInfo));
 
     case Actions["actionTypes"].PLACES_BOOKMARK_REMOVED:
       const removeBookmarkInfo = item => {
         if (item.url === action.data.url) {
           const newSite = Object.assign({}, item);
           delete newSite.bookmarkGuid;
           delete newSite.bookmarkTitle;
           delete newSite.bookmarkDateCreated;
+
+          if (!newSite.context_type || newSite.context_type === "bookmark") {
+            newSite.context_type = "removedBookmark";
+          }
+
           return newSite;
         }
 
         return item;
       };
 
       return isNotReady() ? prevState : nextState(items => items.map(removeBookmarkInfo));
 
@@ -12843,400 +13434,17 @@ var reducers = {
   Dialog,
   Sections,
   Pocket,
   DiscoveryStream,
   Search
 };
 
 /***/ }),
-/* 57 */
-/***/ (function(module, __webpack_exports__, __webpack_require__) {
-
-"use strict";
-__webpack_require__.r(__webpack_exports__);
-
-// EXTERNAL MODULE: ./common/Actions.jsm
-var Actions = __webpack_require__(2);
-
-// CONCATENATED MODULE: ./content-src/components/Card/types.js
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-const cardContextTypes = {
-  history: {
-    fluentID: "newtab-label-visited",
-    icon: "history-item"
-  },
-  bookmark: {
-    fluentID: "newtab-label-bookmarked",
-    icon: "bookmark-added"
-  },
-  trending: {
-    fluentID: "newtab-label-recommended",
-    icon: "trending"
-  },
-  pocket: {
-    fluentID: "newtab-label-saved",
-    icon: "pocket"
-  },
-  download: {
-    fluentID: "newtab-label-download",
-    icon: "download"
-  }
-};
-// EXTERNAL MODULE: external "ReactRedux"
-var external_ReactRedux_ = __webpack_require__(27);
-
-// EXTERNAL MODULE: ./content-src/components/ContextMenu/ContextMenuButton.jsx
-var ContextMenuButton = __webpack_require__(31);
-
-// EXTERNAL MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx + 1 modules
-var LinkMenu = __webpack_require__(60);
-
-// EXTERNAL MODULE: external "React"
-var external_React_ = __webpack_require__(9);
-var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
-
-// EXTERNAL MODULE: ./content-src/lib/screenshot-utils.js
-var screenshot_utils = __webpack_require__(40);
-
-// 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; });
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-
-
-
-
-
- // Keep track of pending image loads to only request once
-
-const gImageLoading = new Map();
-/**
- * Card component.
- * Cards are found within a Section component and contain information about a link such
- * as preview image, page title, page description, and some context about if the page
- * was visited, bookmarked, trending etc...
- * Each Section can make an unordered list of Cards which will create one instane of
- * this class. Each card will then get a context menu which reflects the actions that
- * can be done on this Card.
- */
-
-class Card_Card extends external_React_default.a.PureComponent {
-  constructor(props) {
-    super(props);
-    this.state = {
-      activeCard: null,
-      imageLoaded: false,
-      cardImage: null
-    };
-    this.onMenuButtonUpdate = this.onMenuButtonUpdate.bind(this);
-    this.onLinkClick = this.onLinkClick.bind(this);
-  }
-  /**
-   * Helper to conditionally load an image and update state when it loads.
-   */
-
-
-  async maybeLoadImage() {
-    // No need to load if it's already loaded or no image
-    const {
-      cardImage
-    } = this.state;
-
-    if (!cardImage) {
-      return;
-    }
-
-    const imageUrl = cardImage.url;
-
-    if (!this.state.imageLoaded) {
-      // Initialize a promise to share a load across multiple card updates
-      if (!gImageLoading.has(imageUrl)) {
-        const loaderPromise = new Promise((resolve, reject) => {
-          const loader = new Image();
-          loader.addEventListener("load", resolve);
-          loader.addEventListener("error", reject);
-          loader.src = imageUrl;
-        }); // Save and remove the promise only while it's pending
-
-        gImageLoading.set(imageUrl, loaderPromise);
-        loaderPromise.catch(ex => ex).then(() => gImageLoading.delete(imageUrl)).catch();
-      } // Wait for the image whether just started loading or reused promise
-
-
-      await gImageLoading.get(imageUrl); // Only update state if we're still waiting to load the original image
-
-      if (screenshot_utils["ScreenshotUtils"].isRemoteImageLocal(this.state.cardImage, this.props.link.image) && !this.state.imageLoaded) {
-        this.setState({
-          imageLoaded: true
-        });
-      }
-    }
-  }
-  /**
-   * Helper to obtain the next state based on nextProps and prevState.
-   *
-   * NOTE: Rename this method to getDerivedStateFromProps when we update React
-   *       to >= 16.3. We will need to update tests as well. We cannot rename this
-   *       method to getDerivedStateFromProps now because there is a mismatch in
-   *       the React version that we are using for both testing and production.
-   *       (i.e. react-test-render => "16.3.2", react => "16.2.0").
-   *
-   * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43.
-   */
-
-
-  static getNextStateFromProps(nextProps, prevState) {
-    const {
-      image
-    } = nextProps.link;
-    const imageInState = screenshot_utils["ScreenshotUtils"].isRemoteImageLocal(prevState.cardImage, image);
-    let nextState = null; // Image is updating.
-
-    if (!imageInState && nextProps.link) {
-      nextState = {
-        imageLoaded: false
-      };
-    }
-
-    if (imageInState) {
-      return nextState;
-    } // Since image was updated, attempt to revoke old image blob URL, if it exists.
-
-
-    screenshot_utils["ScreenshotUtils"].maybeRevokeBlobObjectURL(prevState.cardImage);
-    nextState = nextState || {};
-    nextState.cardImage = screenshot_utils["ScreenshotUtils"].createLocalImageObject(image);
-    return nextState;
-  }
-
-  onMenuButtonUpdate(isOpen) {
-    if (isOpen) {
-      this.setState({
-        activeCard: this.props.index
-      });
-    } else {
-      this.setState({
-        activeCard: null
-      });
-    }
-  }
-  /**
-   * Report to telemetry additional information about the item.
-   */
-
-
-  _getTelemetryInfo() {
-    // Filter out "history" type for being the default
-    if (this.props.link.type !== "history") {
-      return {
-        value: {
-          card_type: this.props.link.type
-        }
-      };
-    }
-
-    return null;
-  }
-
-  onLinkClick(event) {
-    event.preventDefault();
-
-    if (this.props.link.type === "download") {
-      this.props.dispatch(Actions["actionCreators"].OnlyToMain({
-        type: Actions["actionTypes"].SHOW_DOWNLOAD_FILE,
-        data: this.props.link
-      }));
-    } else {
-      const {
-        altKey,
-        button,
-        ctrlKey,
-        metaKey,
-        shiftKey
-      } = event;
-      this.props.dispatch(Actions["actionCreators"].OnlyToMain({
-        type: Actions["actionTypes"].OPEN_LINK,
-        data: Object.assign(this.props.link, {
-          event: {
-            altKey,
-            button,
-            ctrlKey,
-            metaKey,
-            shiftKey
-          }
-        })
-      }));
-    }
-
-    if (this.props.isWebExtension) {
-      this.props.dispatch(Actions["actionCreators"].WebExtEvent(Actions["actionTypes"].WEBEXT_CLICK, {
-        source: this.props.eventSource,
-        url: this.props.link.url,
-        action_position: this.props.index
-      }));
-    } else {
-      this.props.dispatch(Actions["actionCreators"].UserEvent(Object.assign({
-        event: "CLICK",
-        source: this.props.eventSource,
-        action_position: this.props.index
-      }, this._getTelemetryInfo())));
-
-      if (this.props.shouldSendImpressionStats) {
-        this.props.dispatch(Actions["actionCreators"].ImpressionStats({
-          source: this.props.eventSource,
-          click: 0,
-          tiles: [{
-            id: this.props.link.guid,
-            pos: this.props.index
-          }]
-        }));
-      }
-    }
-  }
-
-  componentDidMount() {
-    this.maybeLoadImage();
-  }
-
-  componentDidUpdate() {
-    this.maybeLoadImage();
-  } // NOTE: Remove this function when we update React to >= 16.3 since React will
-  //       call getDerivedStateFromProps automatically. We will also need to
-  //       rename getNextStateFromProps to getDerivedStateFromProps.
-
-
-  componentWillMount() {
-    const nextState = Card_Card.getNextStateFromProps(this.props, this.state);
-
-    if (nextState) {
-      this.setState(nextState);
-    }
-  } // NOTE: Remove this function when we update React to >= 16.3 since React will
-  //       call getDerivedStateFromProps automatically. We will also need to
-  //       rename getNextStateFromProps to getDerivedStateFromProps.
-
-
-  componentWillReceiveProps(nextProps) {
-    const nextState = Card_Card.getNextStateFromProps(nextProps, this.state);
-
-    if (nextState) {
-      this.setState(nextState);
-    }
-  }
-
-  componentWillUnmount() {
-    screenshot_utils["ScreenshotUtils"].maybeRevokeBlobObjectURL(this.state.cardImage);
-  }
-
-  render() {
-    const {
-      index,
-      className,
-      link,
-      dispatch,
-      contextMenuOptions,
-      eventSource,
-      shouldSendImpressionStats
-    } = this.props;
-    const {
-      props
-    } = this;
-    const title = link.title || link.hostname;
-    const isContextMenuOpen = this.state.activeCard === index; // Display "now" as "trending" until we have new strings #3402
-
-    const {
-      icon,
-      fluentID
-    } = cardContextTypes[link.type === "now" ? "trending" : link.type] || {};
-    const hasImage = this.state.cardImage || link.hasImage;
-    const imageStyle = {
-      backgroundImage: this.state.cardImage ? `url(${this.state.cardImage.url})` : "none"
-    };
-    const outerClassName = ["card-outer", className, isContextMenuOpen && "active", props.placeholder && "placeholder"].filter(v => v).join(" ");
-    return external_React_default.a.createElement("li", {
-      className: outerClassName
-    }, external_React_default.a.createElement("a", {
-      href: link.type === "pocket" ? link.open_url : link.url,
-      onClick: !props.placeholder ? this.onLinkClick : undefined
-    }, external_React_default.a.createElement("div", {
-      className: "card"
-    }, external_React_default.a.createElement("div", {
-      className: "card-preview-image-outer"
-    }, hasImage && external_React_default.a.createElement("div", {
-      className: `card-preview-image${this.state.imageLoaded ? " loaded" : ""}`,
-      style: imageStyle
-    })), external_React_default.a.createElement("div", {
-      className: "card-details"
-    }, link.type === "download" && external_React_default.a.createElement("div", {
-      className: "card-host-name alternate",
-      "data-l10n-id": "newtab-menu-show-file"
-    }), link.hostname && external_React_default.a.createElement("div", {
-      className: "card-host-name"
-    }, link.hostname.slice(0, 100), link.type === "download" && `  \u2014 ${link.description}`), external_React_default.a.createElement("div", {
-      className: ["card-text", icon ? "" : "no-context", link.description ? "" : "no-description", link.hostname ? "" : "no-host-name"].join(" ")
-    }, external_React_default.a.createElement("h4", {
-      className: "card-title",
-      dir: "auto"
-    }, link.title), external_React_default.a.createElement("p", {
-      className: "card-description",
-      dir: "auto"
-    }, link.description)), external_React_default.a.createElement("div", {
-      className: "card-context"
-    }, icon && !link.context && external_React_default.a.createElement("span", {
-      "aria-haspopup": "true",
-      className: `card-context-icon icon icon-${icon}`
-    }), link.icon && link.context && external_React_default.a.createElement("span", {
-      "aria-haspopup": "true",
-      className: "card-context-icon icon",
-      style: {
-        backgroundImage: `url('${link.icon}')`
-      }
-    }), fluentID && !link.context && external_React_default.a.createElement("div", {
-      className: "card-context-label",
-      "data-l10n-id": fluentID
-    }), link.context && external_React_default.a.createElement("div", {
-      className: "card-context-label"
-    }, link.context))))), !props.placeholder && external_React_default.a.createElement(ContextMenuButton["ContextMenuButton"], {
-      tooltip: "newtab-menu-content-tooltip",
-      tooltipArgs: {
-        title
-      },
-      onUpdate: this.onMenuButtonUpdate
-    }, external_React_default.a.createElement(LinkMenu["LinkMenu"], {
-      dispatch: dispatch,
-      index: index,
-      source: eventSource,
-      options: link.contextMenuOptions || contextMenuOptions,
-      site: link,
-      siteInfo: this._getTelemetryInfo(),
-      shouldSendImpressionStats: shouldSendImpressionStats
-    })));
-  }
-
-}
-Card_Card.defaultProps = {
-  link: {}
-};
-const Card = Object(external_ReactRedux_["connect"])(state => ({
-  platform: state.Prefs.values.platform
-}))(Card_Card);
-const PlaceholderCard = props => external_React_default.a.createElement(Card, {
-  placeholder: true,
-  className: props.className
-});
-
-/***/ }),
-/* 58 */
+/* 60 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(9);
 var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
@@ -13246,20 +13454,20 @@ var Trailhead = __webpack_require__(20);
 
 // EXTERNAL MODULE: ./content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
 var ReturnToAMO = __webpack_require__(23);
 
 // EXTERNAL MODULE: ./content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
 var StartupOverlay = __webpack_require__(24);
 
 // EXTERNAL MODULE: ./node_modules/fluent-react/src/index.js + 14 modules
-var src = __webpack_require__(53);
+var src = __webpack_require__(56);
 
 // EXTERNAL MODULE: ./content-src/asrouter/rich-text-strings.js + 8 modules
-var rich_text_strings = __webpack_require__(55);
+var rich_text_strings = __webpack_require__(58);
 
 // CONCATENATED MODULE: ./content-src/asrouter/templates/FirstRun/Interrupt.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); }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
@@ -13319,34 +13527,30 @@ class Interrupt_Interrupt extends extern
         throw new Error(`${message.template} is not a valid FirstRun message`);
     }
   }
 
 }
 // EXTERNAL MODULE: ./content-src/asrouter/templates/FirstRun/Triplets.jsx
 var Triplets = __webpack_require__(25);
 
-// EXTERNAL MODULE: ./common/Actions.jsm
-var Actions = __webpack_require__(2);
-
 // EXTERNAL MODULE: ./content-src/asrouter/templates/FirstRun/addUtmParams.js
 var addUtmParams = __webpack_require__(22);
 
 // CONCATENATED MODULE: ./content-src/asrouter/templates/FirstRun/FirstRun.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FLUENT_FILES", function() { return FLUENT_FILES; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helpers", function() { return helpers; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FirstRun", function() { return FirstRun_FirstRun; });
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
 
-
 const FLUENT_FILES = ["branding/brand.ftl", "browser/branding/brandings.ftl", "browser/branding/sync-brand.ftl", "browser/newtab/onboarding.ftl"];
 const helpers = {
   selectInterruptAndTriplets(message = {}) {
     const hasInterrupt = Boolean(message.content);
     const hasTriplets = Boolean(message.bundle && message.bundle.length);
     const UTMTerm = message.utm_term || "";
     return {
       hasTriplets,
@@ -13358,59 +13562,16 @@ const helpers = {
   },
 
   addFluent(document) {
     FLUENT_FILES.forEach(file => {
       const link = document.head.appendChild(document.createElement("link"));
       link.href = file;
       link.rel = "localization";
     });
-  },
-
-  async fetchFlowParams({
-    fxaEndpoint,
-    UTMTerm,
-    dispatch,
-    setFlowParams
-  }) {
-    try {
-      const url = new URL(`${fxaEndpoint}/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`);
-      Object(addUtmParams["addUtmParams"])(url, UTMTerm);
-      const response = await fetch(url, {
-        credentials: "omit"
-      });
-
-      if (response.status === 200) {
-        const {
-          deviceId,
-          flowId,
-          flowBeginTime
-        } = await response.json();
-        setFlowParams({
-          deviceId,
-          flowId,
-          flowBeginTime
-        });
-      } else {
-        dispatch(Actions["actionCreators"].OnlyToMain({
-          type: Actions["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
-          data: {
-            event: "FXA_METRICS_FETCH_ERROR",
-            value: response.status
-          }
-        }));
-      }
-    } catch (error) {
-      dispatch(Actions["actionCreators"].OnlyToMain({
-        type: Actions["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
-        data: {
-          event: "FXA_METRICS_ERROR"
-        }
-      }));
-    }
   }
 
 };
 class FirstRun_FirstRun extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.didLoadFlowParams = false;
     this.state = {
@@ -13454,34 +13615,34 @@ class FirstRun_FirstRun extends external
         isTripletsContentVisible: !(hasInterrupt || !hasTriplets),
         UTMTerm
       };
     }
 
     return null;
   }
 
-  fetchFlowParams() {
+  async fetchFlowParams() {
     const {
       fxaEndpoint,
-      dispatch
+      fetchFlowParams
     } = this.props;
     const {
       UTMTerm
     } = this.state;
 
     if (fxaEndpoint && UTMTerm && !this.didLoadFlowParams) {
       this.didLoadFlowParams = true;
-      helpers.fetchFlowParams({
-        fxaEndpoint,
-        UTMTerm,
-        dispatch,
-        setFlowParams: flowParams => this.setState({
-          flowParams
-        })
+      const flowParams = await fetchFlowParams({ ...addUtmParams["BASE_PARAMS"],
+        entrypoint: "activity-stream-firstrun",
+        form_type: "email",
+        utm_term: UTMTerm
+      });
+      this.setState({
+        flowParams
       });
     }
   }
 
   removeHideMain() {
     if (!this.state.hasInterrupt) {
       // We need to remove hide-main since we should show it underneath everything that has rendered
       this.props.document.body.classList.remove("hide-main", "welcome");
@@ -13558,446 +13719,17 @@ class FirstRun_FirstRun extends external
       flowParams: flowParams,
       onAction: executeAction
     }) : null);
   }
 
 }
 
 /***/ }),
-/* 59 */
-/***/ (function(module, __webpack_exports__, __webpack_require__) {
-
-"use strict";
-__webpack_require__.r(__webpack_exports__);
-
-// EXTERNAL MODULE: ./common/Actions.jsm
-var Actions = __webpack_require__(2);
-
-// EXTERNAL MODULE: ./content-src/components/A11yLinkButton/A11yLinkButton.jsx
-var A11yLinkButton = __webpack_require__(35);
-
-// EXTERNAL MODULE: external "React"
-var external_React_ = __webpack_require__(9);
-var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
-
-// EXTERNAL MODULE: ./content-src/components/TopSites/TopSitesConstants.js
-var TopSitesConstants = __webpack_require__(47);
-
-// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-class TopSiteFormInput_TopSiteFormInput extends external_React_default.a.PureComponent {
-  constructor(props) {
-    super(props);
-    this.state = {
-      validationError: this.props.validationError
-    };
-    this.onChange = this.onChange.bind(this);
-    this.onMount = this.onMount.bind(this);
-    this.onClearIconPress = this.onClearIconPress.bind(this);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    if (nextProps.shouldFocus && !this.props.shouldFocus) {
-      this.input.focus();
-    }
-
-    if (nextProps.validationError && !this.props.validationError) {
-      this.setState({
-        validationError: true
-      });
-    } // If the component is in an error state but the value was cleared by the parent
-
-
-    if (this.state.validationError && !nextProps.value) {
-      this.setState({
-        validationError: false
-      });
-    }
-  }
-
-  onClearIconPress(event) {
-    // If there is input in the URL or custom image URL fields,
-    // and we hit 'enter' while tabbed over the clear icon,
-    // we should execute the function to clear the field.
-    if (event.key === "Enter") {
-      this.props.onClear();
-    }
-  }
-
-  onChange(ev) {
-    if (this.state.validationError) {
-      this.setState({
-        validationError: false
-      });
-    }
-
-    this.props.onChange(ev);
-  }
-
-  onMount(input) {
-    this.input = input;
-  }
-
-  renderLoadingOrCloseButton() {
-    const showClearButton = this.props.value && this.props.onClear;
-
-    if (this.props.loading) {
-      return external_React_default.a.createElement("div", {
-        className: "loading-container"
-      }, external_React_default.a.createElement("div", {
-        className: "loading-animation"
-      }));
-    } else if (showClearButton) {
-      return external_React_default.a.createElement("button", {
-        type: "button",
-        className: "icon icon-clear-input icon-button-style",
-        onClick: this.props.onClear,
-        onKeyPress: this.onClearIconPress
-      });
-    }
-
-    return null;
-  }
-
-  render() {
-    const {
-      typeUrl
-    } = this.props;
-    const {
-      validationError
-    } = this.state;
-    return external_React_default.a.createElement("label", null, external_React_default.a.createElement("span", {
-      "data-l10n-id": this.props.titleId
-    }), external_React_default.a.createElement("div", {
-      className: `field ${typeUrl ? "url" : ""}${validationError ? " invalid" : ""}`
-    }, external_React_default.a.createElement("input", {
-      type: "text",
-      value: this.props.value,
-      ref: this.onMount,
-      onChange: this.onChange,
-      "data-l10n-id": this.props.placeholderId // Set focus on error if the url field is valid or when the input is first rendered and is empty
-      // eslint-disable-next-line jsx-a11y/no-autofocus
-      ,
-      autoFocus: this.props.shouldFocus,
-      disabled: this.props.loading
-    }), this.renderLoadingOrCloseButton(), validationError && external_React_default.a.createElement("aside", {
-      className: "error-tooltip",
-      "data-l10n-id": this.props.errorMessageId
-    })));
-  }
-
-}
-TopSiteFormInput_TopSiteFormInput.defaultProps = {
-  showClearButton: false,
-  value: "",
-  validationError: false
-};
-// EXTERNAL MODULE: ./content-src/components/TopSites/TopSite.jsx
-var TopSite = __webpack_require__(49);
-
-// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteForm.jsx
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteForm", function() { return TopSiteForm_TopSiteForm; });
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-
-
-
-
-
-class TopSiteForm_TopSiteForm extends external_React_default.a.PureComponent {
-  constructor(props) {
-    super(props);
-    const {
-      site
-    } = props;
-    this.state = {
-      label: site ? site.label || site.hostname : "",
-      url: site ? site.url : "",
-      validationError: false,
-      customScreenshotUrl: site ? site.customScreenshotURL : "",
-      showCustomScreenshotForm: site ? site.customScreenshotURL : false
-    };
-    this.onClearScreenshotInput = this.onClearScreenshotInput.bind(this);
-    this.onLabelChange = this.onLabelChange.bind(this);
-    this.onUrlChange = this.onUrlChange.bind(this);
-    this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
-    this.onClearUrlClick = this.onClearUrlClick.bind(this);
-    this.onDoneButtonClick = this.onDoneButtonClick.bind(this);
-    this.onCustomScreenshotUrlChange = this.onCustomScreenshotUrlChange.bind(this);
-    this.onPreviewButtonClick = this.onPreviewButtonClick.bind(this);
-    this.onEnableScreenshotUrlForm = this.onEnableScreenshotUrlForm.bind(this);
-    this.validateUrl = this.validateUrl.bind(this);
-  }
-
-  onLabelChange(event) {
-    this.setState({
-      label: event.target.value
-    });
-  }
-
-  onUrlChange(event) {
-    this.setState({
-      url: event.target.value,
-      validationError: false
-    });
-  }
-
-  onClearUrlClick() {
-    this.setState({
-      url: "",
-      validationError: false
-    });
-  }
-
-  onEnableScreenshotUrlForm() {
-    this.setState({
-      showCustomScreenshotForm: true
-    });
-  }
-
-  _updateCustomScreenshotInput(customScreenshotUrl) {
-    this.setState({
-      customScreenshotUrl,
-      validationError: false
-    });
-    this.props.dispatch({
-      type: Actions["actionTypes"].PREVIEW_REQUEST_CANCEL
-    });
-  }
-
-  onCustomScreenshotUrlChange(event) {
-    this._updateCustomScreenshotInput(event.target.value);
-  }
-
-  onClearScreenshotInput() {
-    this._updateCustomScreenshotInput("");
-  }
-
-  onCancelButtonClick(ev) {
-    ev.preventDefault();
-    this.props.onClose();
-  }
-
-  onDoneButtonClick(ev) {
-    ev.preventDefault();
-
-    if (this.validateForm()) {
-      const site = {
-        url: this.cleanUrl(this.state.url)
-      };
-      const {
-        index
-      } = this.props;
-
-      if (this.state.label !== "") {
-        site.label = this.state.label;
-      }
-
-      if (this.state.customScreenshotUrl) {
-        site.customScreenshotURL = this.cleanUrl(this.state.customScreenshotUrl);
-      } else if (this.props.site && this.props.site.customScreenshotURL) {
-        // Used to flag that previously cached screenshot should be removed
-        site.customScreenshotURL = null;
-      }
-
-      this.props.dispatch(Actions["actionCreators"].AlsoToMain({
-        type: Actions["actionTypes"].TOP_SITES_PIN,
-        data: {
-          site,
-          index
-        }
-      }));
-      this.props.dispatch(Actions["actionCreators"].UserEvent({
-        source: TopSitesConstants["TOP_SITES_SOURCE"],
-        event: "TOP_SITES_EDIT",
-        action_position: index
-      }));
-      this.props.onClose();
-    }
-  }
-
-  onPreviewButtonClick(event) {
-    event.preventDefault();
-
-    if (this.validateForm()) {
-      this.props.dispatch(Actions["actionCreators"].AlsoToMain({
-        type: Actions["actionTypes"].PREVIEW_REQUEST,
-        data: {
-          url: this.cleanUrl(this.state.customScreenshotUrl)
-        }
-      }));
-      this.props.dispatch(Actions["actionCreators"].UserEvent({
-        source: TopSitesConstants["TOP_SITES_SOURCE"],
-        event: "PREVIEW_REQUEST"
-      }));
-    }
-  }
-
-  cleanUrl(url) {
-    // If we are missing a protocol, prepend http://
-    if (!url.startsWith("http:") && !url.startsWith("https:")) {
-      return `http://${url}`;
-    }
-
-    return url;
-  }
-
-  _tryParseUrl(url) {
-    try {
-      return new URL(url);
-    } catch (e) {
-      return null;
-    }
-  }
-
-  validateUrl(url) {
-    const validProtocols = ["http:", "https:"];
-
-    const urlObj = this._tryParseUrl(url) || this._tryParseUrl(this.cleanUrl(url));
-
-    return urlObj && validProtocols.includes(urlObj.protocol);
-  }
-
-  validateCustomScreenshotUrl() {
-    const {
-      customScreenshotUrl
-    } = this.state;
-    return !customScreenshotUrl || this.validateUrl(customScreenshotUrl);
-  }
-
-  validateForm() {
-    const validate = this.validateUrl(this.state.url) && this.validateCustomScreenshotUrl();
-
-    if (!validate) {
-      this.setState({
-        validationError: true
-      });
-    }
-
-    return validate;
-  }
-
-  _renderCustomScreenshotInput() {
-    const {
-      customScreenshotUrl
-    } = this.state;
-    const requestFailed = this.props.previewResponse === "";
-    const validationError = this.state.validationError && !this.validateCustomScreenshotUrl() || requestFailed; // Set focus on error if the url field is valid or when the input is first rendered and is empty
-
-    const shouldFocus = validationError && this.validateUrl(this.state.url) || !customScreenshotUrl;
-    const isLoading = this.props.previewResponse === null && customScreenshotUrl && this.props.previewUrl === this.cleanUrl(customScreenshotUrl);
-
-    if (!this.state.showCustomScreenshotForm) {
-      return external_React_default.a.createElement(A11yLinkButton["A11yLinkButton"], {
-        onClick: this.onEnableScreenshotUrlForm,
-        className: "enable-custom-image-input",
-        "data-l10n-id": "newtab-topsites-use-image-link"
-      });
-    }
-
-    return external_React_default.a.createElement("div", {
-      className: "custom-image-input-container"
-    }, external_React_default.a.createElement(TopSiteFormInput_TopSiteFormInput, {
-      errorMessageId: requestFailed ? "newtab-topsites-image-validation" : "newtab-topsites-url-validation",
-      loading: isLoading,
-      onChange: this.onCustomScreenshotUrlChange,
-      onClear: this.onClearScreenshotInput,
-      shouldFocus: shouldFocus,
-      typeUrl: true,
-      value: customScreenshotUrl,
-      validationError: validationError,
-      titleId: "newtab-topsites-image-url-label",
-      placeholderId: "newtab-topsites-url-input"
-    }));
-  }
-
-  render() {
-    const {
-      customScreenshotUrl
-    } = this.state;
-    const requestFailed = this.props.previewResponse === ""; // For UI purposes, editing without an existing link is "add"
-
-    const showAsAdd = !this.props.site;
-    const previous = this.props.site && this.props.site.customScreenshotURL || "";
-    const changed = customScreenshotUrl && this.cleanUrl(customScreenshotUrl) !== previous; // Preview mode if changes were made to the custom screenshot URL and no preview was received yet
-    // or the request failed
-
-    const previewMode = changed && !this.props.previewResponse;
-    const previewLink = Object.assign({}, this.props.site);
-
-    if (this.props.previewResponse) {
-      previewLink.screenshot = this.props.previewResponse;
-      previewLink.customScreenshotURL = this.props.previewUrl;
-    } // Handles the form submit so an enter press performs the correct action
-
-
-    const onSubmit = previewMode ? this.onPreviewButtonClick : this.onDoneButtonClick;
-    return external_React_default.a.createElement("form", {
-      className: "topsite-form",
-      onSubmit: onSubmit
-    }, external_React_default.a.createElement("div", {
-      className: "form-input-container"
-    }, external_React_default.a.createElement("h3", {
-      className: "section-title grey-title",
-      "data-l10n-id": showAsAdd ? "newtab-topsites-add-topsites-header" : "newtab-topsites-edit-topsites-header"
-    }), external_React_default.a.createElement("div", {
-      className: "fields-and-preview"
-    }, external_React_default.a.createElement("div", {
-      className: "form-wrapper"
-    }, external_React_default.a.createElement(TopSiteFormInput_TopSiteFormInput, {
-      onChange: this.onLabelChange,
-      value: this.state.label,
-      titleId: "newtab-topsites-title-label",
-      placeholderId: "newtab-topsites-title-input"
-    }), external_React_default.a.createElement(TopSiteFormInput_TopSiteFormInput, {
-      onChange: this.onUrlChange,
-      shouldFocus: this.state.validationError && !this.validateUrl(this.state.url),
-      value: this.state.url,
-      onClear: this.onClearUrlClick,
-      validationError: this.state.validationError && !this.validateUrl(this.state.url),
-      titleId: "newtab-topsites-url-label",
-      typeUrl: true,
-      placeholderId: "newtab-topsites-url-input",
-      errorMessageId: "newtab-topsites-url-validation"
-    }), this._renderCustomScreenshotInput()), external_React_default.a.createElement(TopSite["TopSiteLink"], {
-      link: previewLink,
-      defaultStyle: requestFailed,
-      title: this.state.label
-    }))), external_React_default.a.createElement("section", {
-      className: "actions"
-    }, external_React_default.a.createElement("button", {
-      className: "cancel",
-      type: "button",
-      onClick: this.onCancelButtonClick,
-      "data-l10n-id": "newtab-topsites-cancel-button"
-    }), previewMode ? external_React_default.a.createElement("button", {
-      className: "done preview",
-      type: "submit",
-      "data-l10n-id": "newtab-topsites-preview-button"
-    }) : external_React_default.a.createElement("button", {
-      className: "done",
-      type: "submit",
-      "data-l10n-id": showAsAdd ? "newtab-topsites-add-button" : "newtab-topsites-save-button"
-    })));
-  }
-
-}
-TopSiteForm_TopSiteForm.defaultProps = {
-  site: null,
-  index: -1
-};
-
-/***/ }),
-/* 60 */
+/* 61 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: ./common/Actions.jsm
 var Actions = __webpack_require__(2);
 
@@ -14367,10 +14099,439 @@ class LinkMenu_LinkMenu extends external
 
 const getState = state => ({
   isPrivateBrowsingEnabled: state.Prefs.values.isPrivateBrowsingEnabled,
   platform: state.Prefs.values.platform
 });
 
 const LinkMenu = Object(external_ReactRedux_["connect"])(getState)(LinkMenu_LinkMenu);
 
+/***/ }),
+/* 62 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+
+// EXTERNAL MODULE: ./common/Actions.jsm
+var Actions = __webpack_require__(2);
+
+// EXTERNAL MODULE: ./content-src/components/A11yLinkButton/A11yLinkButton.jsx
+var A11yLinkButton = __webpack_require__(37);
+
+// EXTERNAL MODULE: external "React"
+var external_React_ = __webpack_require__(9);
+var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
+
+// EXTERNAL MODULE: ./content-src/components/TopSites/TopSitesConstants.js
+var TopSitesConstants = __webpack_require__(50);
+
+// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+class TopSiteFormInput_TopSiteFormInput extends external_React_default.a.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      validationError: this.props.validationError
+    };
+    this.onChange = this.onChange.bind(this);
+    this.onMount = this.onMount.bind(this);
+    this.onClearIconPress = this.onClearIconPress.bind(this);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.shouldFocus && !this.props.shouldFocus) {
+      this.input.focus();
+    }
+
+    if (nextProps.validationError && !this.props.validationError) {
+      this.setState({
+        validationError: true
+      });
+    } // If the component is in an error state but the value was cleared by the parent
+
+
+    if (this.state.validationError && !nextProps.value) {
+      this.setState({
+        validationError: false
+      });
+    }
+  }
+
+  onClearIconPress(event) {
+    // If there is input in the URL or custom image URL fields,
+    // and we hit 'enter' while tabbed over the clear icon,
+    // we should execute the function to clear the field.
+    if (event.key === "Enter") {
+      this.props.onClear();
+    }
+  }
+
+  onChange(ev) {
+    if (this.state.validationError) {
+      this.setState({
+        validationError: false
+      });
+    }
+
+    this.props.onChange(ev);
+  }
+
+  onMount(input) {
+    this.input = input;
+  }
+
+  renderLoadingOrCloseButton() {
+    const showClearButton = this.props.value && this.props.onClear;
+
+    if (this.props.loading) {
+      return external_React_default.a.createElement("div", {
+        className: "loading-container"
+      }, external_React_default.a.createElement("div", {
+        className: "loading-animation"
+      }));
+    } else if (showClearButton) {
+      return external_React_default.a.createElement("button", {
+        type: "button",
+        className: "icon icon-clear-input icon-button-style",
+        onClick: this.props.onClear,
+        onKeyPress: this.onClearIconPress
+      });
+    }
+
+    return null;
+  }
+
+  render() {
+    const {
+      typeUrl
+    } = this.props;
+    const {
+      validationError
+    } = this.state;
+    return external_React_default.a.createElement("label", null, external_React_default.a.createElement("span", {
+      "data-l10n-id": this.props.titleId
+    }), external_React_default.a.createElement("div", {
+      className: `field ${typeUrl ? "url" : ""}${validationError ? " invalid" : ""}`
+    }, external_React_default.a.createElement("input", {
+      type: "text",
+      value: this.props.value,
+      ref: this.onMount,
+      onChange: this.onChange,
+      "data-l10n-id": this.props.placeholderId // Set focus on error if the url field is valid or when the input is first rendered and is empty
+      // eslint-disable-next-line jsx-a11y/no-autofocus
+      ,
+      autoFocus: this.props.shouldFocus,
+      disabled: this.props.loading
+    }), this.renderLoadingOrCloseButton(), validationError && external_React_default.a.createElement("aside", {
+      className: "error-tooltip",
+      "data-l10n-id": this.props.errorMessageId
+    })));
+  }
+
+}
+TopSiteFormInput_TopSiteFormInput.defaultProps = {
+  showClearButton: false,
+  value: "",
+  validationError: false
+};
+// EXTERNAL MODULE: ./content-src/components/TopSites/TopSite.jsx
+var TopSite = __webpack_require__(52);
+
+// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteForm.jsx
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteForm", function() { return TopSiteForm_TopSiteForm; });
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+class TopSiteForm_TopSiteForm extends external_React_default.a.PureComponent {
+  constructor(props) {
+    super(props);
+    const {
+      site
+    } = props;
+    this.state = {
+      label: site ? site.label || site.hostname : "",
+      url: site ? site.url : "",
+      validationError: false,
+      customScreenshotUrl: site ? site.customScreenshotURL : "",
+      showCustomScreenshotForm: site ? site.customScreenshotURL : false
+    };
+    this.onClearScreenshotInput = this.onClearScreenshotInput.bind(this);
+    this.onLabelChange = this.onLabelChange.bind(this);
+    this.onUrlChange = this.onUrlChange.bind(this);
+    this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
+    this.onClearUrlClick = this.onClearUrlClick.bind(this);
+    this.onDoneButtonClick = this.onDoneButtonClick.bind(this);
+    this.onCustomScreenshotUrlChange = this.onCustomScreenshotUrlChange.bind(this);
+    this.onPreviewButtonClick = this.onPreviewButtonClick.bind(this);
+    this.onEnableScreenshotUrlForm = this.onEnableScreenshotUrlForm.bind(this);
+    this.validateUrl = this.validateUrl.bind(this);
+  }
+
+  onLabelChange(event) {
+    this.setState({
+      label: event.target.value
+    });
+  }
+
+  onUrlChange(event) {
+    this.setState({
+      url: event.target.value,
+      validationError: false
+    });
+  }
+
+  onClearUrlClick() {
+    this.setState({
+      url: "",
+      validationError: false
+    });
+  }
+
+  onEnableScreenshotUrlForm() {
+    this.setState({
+      showCustomScreenshotForm: true
+    });
+  }
+
+  _updateCustomScreenshotInput(customScreenshotUrl) {
+    this.setState({
+      customScreenshotUrl,
+      validationError: false
+    });
+    this.props.dispatch({
+      type: Actions["actionTypes"].PREVIEW_REQUEST_CANCEL
+    });
+  }
+
+  onCustomScreenshotUrlChange(event) {
+    this._updateCustomScreenshotInput(event.target.value);
+  }
+
+  onClearScreenshotInput() {
+    this._updateCustomScreenshotInput("");
+  }
+
+  onCancelButtonClick(ev) {
+    ev.preventDefault();
+    this.props.onClose();
+  }
+
+  onDoneButtonClick(ev) {
+    ev.preventDefault();
+
+    if (this.validateForm()) {
+      const site = {
+        url: this.cleanUrl(this.state.url)
+      };
+      const {
+        index
+      } = this.props;
+
+      if (this.state.label !== "") {
+        site.label = this.state.label;
+      }
+
+      if (this.state.customScreenshotUrl) {
+        site.customScreenshotURL = this.cleanUrl(this.state.customScreenshotUrl);
+      } else if (this.props.site && this.props.site.customScreenshotURL) {
+        // Used to flag that previously cached screenshot should be removed
+        site.customScreenshotURL = null;
+      }
+
+      this.props.dispatch(Actions["actionCreators"].AlsoToMain({
+        type: Actions["actionTypes"].TOP_SITES_PIN,
+        data: {
+          site,
+          index
+        }
+      }));
+      this.props.dispatch(Actions["actionCreators"].UserEvent({
+        source: TopSitesConstants["TOP_SITES_SOURCE"],
+        event: "TOP_SITES_EDIT",
+        action_position: index
+      }));
+      this.props.onClose();
+    }
+  }
+
+  onPreviewButtonClick(event) {
+    event.preventDefault();
+
+    if (this.validateForm()) {
+      this.props.dispatch(Actions["actionCreators"].AlsoToMain({
+        type: Actions["actionTypes"].PREVIEW_REQUEST,
+        data: {
+          url: this.cleanUrl(this.state.customScreenshotUrl)
+        }
+      }));
+      this.props.dispatch(Actions["actionCreators"].UserEvent({
+        source: TopSitesConstants["TOP_SITES_SOURCE"],
+        event: "PREVIEW_REQUEST"
+      }));
+    }
+  }
+
+  cleanUrl(url) {
+    // If we are missing a protocol, prepend http://
+    if (!url.startsWith("http:") && !url.startsWith("https:")) {
+      return `http://${url}`;
+    }
+
+    return url;
+  }
+
+  _tryParseUrl(url) {
+    try {
+      return new URL(url);
+    } catch (e) {
+      return null;
+    }
+  }
+
+  validateUrl(url) {
+    const validProtocols = ["http:", "https:"];
+
+    const urlObj = this._tryParseUrl(url) || this._tryParseUrl(this.cleanUrl(url));
+
+    return urlObj && validProtocols.includes(urlObj.protocol);
+  }
+
+  validateCustomScreenshotUrl() {
+    const {
+      customScreenshotUrl
+    } = this.state;
+    return !customScreenshotUrl || this.validateUrl(customScreenshotUrl);
+  }
+
+  validateForm() {
+    const validate = this.validateUrl(this.state.url) && this.validateCustomScreenshotUrl();
+
+    if (!validate) {
+      this.setState({
+        validationError: true
+      });
+    }
+
+    return validate;
+  }
+
+  _renderCustomScreenshotInput() {
+    const {
+      customScreenshotUrl
+    } = this.state;
+    const requestFailed = this.props.previewResponse === "";
+    const validationError = this.state.validationError && !this.validateCustomScreenshotUrl() || requestFailed; // Set focus on error if the url field is valid or when the input is first rendered and is empty
+
+    const shouldFocus = validationError && this.validateUrl(this.state.url) || !customScreenshotUrl;
+    const isLoading = this.props.previewResponse === null && customScreenshotUrl && this.props.previewUrl === this.cleanUrl(customScreenshotUrl);
+
+    if (!this.state.showCustomScreenshotForm) {
+      return external_React_default.a.createElement(A11yLinkButton["A11yLinkButton"], {
+        onClick: this.onEnableScreenshotUrlForm,
+        className: "enable-custom-image-input",
+        "data-l10n-id": "newtab-topsites-use-image-link"
+      });
+    }
+
+    return external_React_default.a.createElement("div", {
+      className: "custom-image-input-container"
+    }, external_React_default.a.createElement(TopSiteFormInput_TopSiteFormInput, {
+      errorMessageId: requestFailed ? "newtab-topsites-image-validation" : "newtab-topsites-url-validation",
+      loading: isLoading,
+      onChange: this.onCustomScreenshotUrlChange,
+      onClear: this.onClearScreenshotInput,
+      shouldFocus: shouldFocus,
+      typeUrl: true,
+      value: customScreenshotUrl,
+      validationError: validationError,
+      titleId: "newtab-topsites-image-url-label",
+      placeholderId: "newtab-topsites-url-input"
+    }));
+  }
+
+  render() {
+    const {
+      customScreenshotUrl
+    } = this.state;
+    const requestFailed = this.props.previewResponse === ""; // For UI purposes, editing without an existing link is "add"
+
+    const showAsAdd = !this.props.site;
+    const previous = this.props.site && this.props.site.customScreenshotURL || "";
+    const changed = customScreenshotUrl && this.cleanUrl(customScreenshotUrl) !== previous; // Preview mode if changes were made to the custom screenshot URL and no preview was received yet
+    // or the request failed
+
+    const previewMode = changed && !this.props.previewResponse;
+    const previewLink = Object.assign({}, this.props.site);
+
+    if (this.props.previewResponse) {
+      previewLink.screenshot = this.props.previewResponse;
+      previewLink.customScreenshotURL = this.props.previewUrl;
+    } // Handles the form submit so an enter press performs the correct action
+
+
+    const onSubmit = previewMode ? this.onPreviewButtonClick : this.onDoneButtonClick;
+    return external_React_default.a.createElement("form", {
+      className: "topsite-form",
+      onSubmit: onSubmit
+    }, external_React_default.a.createElement("div", {
+      className: "form-input-container"
+    }, external_React_default.a.createElement("h3", {
+      className: "section-title grey-title",
+      "data-l10n-id": showAsAdd ? "newtab-topsites-add-topsites-header" : "newtab-topsites-edit-topsites-header"
+    }), external_React_default.a.createElement("div", {
+      className: "fields-and-preview"
+    }, external_React_default.a.createElement("div", {
+      className: "form-wrapper"
+    }, external_React_default.a.createElement(TopSiteFormInput_TopSiteFormInput, {
+      onChange: this.onLabelChange,
+      value: this.state.label,
+      titleId: "newtab-topsites-title-label",
+      placeholderId: "newtab-topsites-title-input"
+    }), external_React_default.a.createElement(TopSiteFormInput_TopSiteFormInput, {
+      onChange: this.onUrlChange,
+      shouldFocus: this.state.validationError && !this.validateUrl(this.state.url),
+      value: this.state.url,
+      onClear: this.onClearUrlClick,
+      validationError: this.state.validationError && !this.validateUrl(this.state.url),
+      titleId: "newtab-topsites-url-label",
+      typeUrl: true,
+      placeholderId: "newtab-topsites-url-input",
+      errorMessageId: "newtab-topsites-url-validation"
+    }), this._renderCustomScreenshotInput()), external_React_default.a.createElement(TopSite["TopSiteLink"], {
+      link: previewLink,
+      defaultStyle: requestFailed,
+      title: this.state.label
+    }))), external_React_default.a.createElement("section", {
+      className: "actions"
+    }, external_React_default.a.createElement("button", {
+      className: "cancel",
+      type: "button",
+      onClick: this.onCancelButtonClick,
+      "data-l10n-id": "newtab-topsites-cancel-button"
+    }), previewMode ? external_React_default.a.createElement("button", {
+      className: "done preview",
+      type: "submit",
+      "data-l10n-id": "newtab-topsites-preview-button"
+    }) : external_React_default.a.createElement("button", {
+      className: "done",
+      type: "submit",
+      "data-l10n-id": showAsAdd ? "newtab-topsites-add-button" : "newtab-topsites-save-button"
+    })));
+  }
+
+}
+TopSiteForm_TopSiteForm.defaultProps = {
+  site: null,
+  index: -1
+};
+
 /***/ })
 /******/ ]);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/icon-removed-bookmark.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M15.845 6.063A1.1 1.1 0 0 0 15 5.331l-1.125-.2-1.729 1.723.872.156-2.45 2.635.5 3.572L8 11.618l-1.291.673-3.468 3.468a1.057 1.057 0 0 0 1.066.038L8 13.874l3.688 1.921a1.1 1.1 0 0 0 1.6-1.126l-.609-4.358 2.926-3.147a1.1 1.1 0 0 0 .24-1.101zm-1.138-4.77a1 1 0 0 0-1.414 0L10.6 3.983 8.984.733A1.093 1.093 0 0 0 8 .124a1.1 1.1 0 0 0-.985.609L5.089 4.6l-4.082.729a1.1 1.1 0 0 0-.614 1.833L3.32 10.31l-.155 1.111-1.872 1.872a1 1 0 1 0 1.414 1.414l12-12a1 1 0 0 0 0-1.414zM2.981 7.01l3.449-.617L8 3.243l1.111 2.232L5.2 9.391z"></path></svg>
\ No newline at end of file
--- a/browser/components/newtab/jar.mn
+++ b/browser/components/newtab/jar.mn
@@ -9,16 +9,17 @@ browser.jar:
   res/activity-stream/vendor/Redux.jsm (./vendor/Redux.jsm)
   res/activity-stream/vendor/react.js (./vendor/react.js)
   res/activity-stream/vendor/react-dom.js (./vendor/react-dom.js)
 #ifndef RELEASE_OR_BETA
   res/activity-stream/vendor/react-dev.js (./vendor/react-dev.js)
   res/activity-stream/vendor/react-dom-dev.js (./vendor/react-dom-dev.js)
 #endif
   res/activity-stream/vendor/prop-types.js (./vendor/prop-types.js)
+  res/activity-stream/vendor/react-transition-group.js (./vendor/react-transition-group.js)
   res/activity-stream/vendor/redux.js (./vendor/redux.js)
   res/activity-stream/vendor/react-redux.js (./vendor/react-redux.js)
   res/activity-stream/data/content/assets/ (./data/content/assets/*)
   res/activity-stream/data/content/tippytop/ (./data/content/tippytop/*)
   res/activity-stream/data/content/activity-stream.bundle.js (./data/content/activity-stream.bundle.js)
 #ifdef XP_MACOSX
   res/activity-stream/css/activity-stream.css (./css/activity-stream-mac.css)
 #elifdef XP_WIN
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -489,16 +489,17 @@ class _ASRouter {
       trailheadInterrupt: "",
       trailheadTriplet: "",
       messages: [],
       errors: [],
     };
     this._triggerHandler = this._triggerHandler.bind(this);
     this._localProviders = localProviders;
     this.blockMessageById = this.blockMessageById.bind(this);
+    this.unblockMessageById = this.unblockMessageById.bind(this);
     this.onMessage = this.onMessage.bind(this);
     this.handleMessageRequest = this.handleMessageRequest.bind(this);
     this.addImpression = this.addImpression.bind(this);
     this._handleTargetingError = this._handleTargetingError.bind(this);
     this.onPrefChange = this.onPrefChange.bind(this);
     this.dispatch = this.dispatch.bind(this);
   }
 
@@ -1481,36 +1482,48 @@ class _ASRouter {
       }
     });
     if (needsUpdate) {
       this._storage.set(impressionsString, impressions);
     }
     return impressions;
   }
 
-  handleMessageRequest({ triggerId, template, provider, returnAll = false }) {
+  handleMessageRequest({
+    triggerId,
+    triggerParam,
+    template,
+    provider,
+    returnAll = false,
+  }) {
     const msgs = this._getUnblockedMessages().filter(m => {
       if (provider && m.provider !== provider) {
         return false;
       }
       if (template && m.template !== template) {
         return false;
       }
       if (m.trigger && m.trigger.id !== triggerId) {
         return false;
       }
 
       return true;
     });
 
     if (returnAll) {
-      return this._findAllMessages(msgs, triggerId && { id: triggerId });
+      return this._findAllMessages(
+        msgs,
+        triggerId && { id: triggerId, param: triggerParam }
+      );
     }
 
-    return this._findMessage(msgs, triggerId && { id: triggerId });
+    return this._findMessage(
+      msgs,
+      triggerId && { id: triggerId, param: triggerParam }
+    );
   }
 
   async setMessageById(id, target, force = true, action = {}) {
     await this.setState({ lastMessageId: id });
     const newMessage = this.getMessageById(id);
 
     await this._sendMessageToTarget(newMessage, target, action.data, force);
   }
@@ -1830,17 +1843,20 @@ class _ASRouter {
         Services.prefs.setBoolPref(
           TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF,
           true
         );
         await this.setupTrailhead();
       }
     }
 
-    const message = await this.handleMessageRequest({ triggerId: trigger.id });
+    const message = await this.handleMessageRequest({
+      triggerId: trigger.id,
+      triggerParam: trigger.param,
+    });
 
     await this.setState({ lastMessageId: message ? message.id : null });
     await this._sendMessageToTarget(message, target, trigger);
   }
 
   /* eslint-disable complexity */
   async onMessage({ data: action, target }) {
     switch (action.type) {
--- a/browser/components/newtab/lib/ActivityStream.jsm
+++ b/browser/components/newtab/lib/ActivityStream.jsm
@@ -5,21 +5,16 @@
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(
   this,
   "AppConstants",
   "resource://gre/modules/AppConstants.jsm"
 );
-ChromeUtils.defineModuleGetter(
-  this,
-  "UpdateUtils",
-  "resource://gre/modules/UpdateUtils.jsm"
-);
 
 // NB: Eagerly load modules that will be loaded/constructed/initialized in the
 // common case to avoid the overhead of wrapping and detecting lazy loading.
 const { actionCreators: ac, actionTypes: at } = ChromeUtils.import(
   "resource://activity-stream/common/Actions.jsm"
 );
 const { AboutPreferences } = ChromeUtils.import(
   "resource://activity-stream/lib/AboutPreferences.jsm"
@@ -99,19 +94,16 @@ const DEFAULT_SITES = new Map([
   ],
   [
     "FR",
     "https://www.youtube.com/,https://www.facebook.com/,https://www.wikipedia.org/,https://www.amazon.fr/,https://www.leboncoin.fr/,https://twitter.com/",
   ],
 ]);
 const GEO_PREF = "browser.search.region";
 const SPOCS_GEOS = ["US"];
-const IS_NIGHTLY_OR_UNBRANDED_BUILD = ["nightly", "default"].includes(
-  UpdateUtils.getUpdateChannel(true)
-);
 
 // Determine if spocs should be shown for a geo/locale
 function showSpocs({ geo }) {
   return SPOCS_GEOS.includes(geo);
 }
 
 // Configure default Activity Stream prefs with a plain `value` or a `getValue`
 // that computes a value. A `value_local_dev` is used for development defaults.
@@ -440,29 +432,26 @@ const PREFS_CONFIG = new Map([
     },
   ],
   // See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.
   [
     "discoverystream.config",
     {
       title: "Configuration for the new pocket new tab",
       getValue: ({ geo, locale }) => {
-        // XXX hardcoded_layout only works for en-*, so fix before adding locales
-        const locales = {
-          US: ["en-CA", "en-GB", "en-US", "en-ZA"],
-          CA: ["en-CA", "en-GB", "en-US", "en-ZA"],
-        }[geo];
+        // PLEASE NOTE:
+        // hardcoded_layout in `lib/DiscoveryStreamFeed.jsm` only works for en-* and requires refactoring for non english locales
+        const dsEnablementMatrix = {
+          US: ["en-CA", "en-GB", "en-US"],
+          CA: ["en-CA", "en-GB", "en-US"],
+        };
 
-        // Enable for US/en-US in all channels.
-        // Enable for specific geos and locales for Nightly.
+        // Verify that the current geo & locale combination is enabled
         const isEnabled =
-          (geo === `US` && locale === `en-US`) ||
-          (IS_NIGHTLY_OR_UNBRANDED_BUILD &&
-            locales &&
-            locales.includes(locale));
+          !!dsEnablementMatrix[geo] && dsEnablementMatrix[geo].includes(locale);
 
         return JSON.stringify({
           api_key_pref: "extensions.pocket.oAuthConsumerKey",
           collapsible: true,
           enabled: isEnabled,
           show_spocs: showSpocs({ geo }),
           hardcoded_layout: true,
           personalized: false,
--- a/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
+++ b/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
@@ -137,16 +137,25 @@ this.DiscoveryStreamFeed = class Discove
       // istanbul ignore next
       Cu.reportError(
         `Could not parse preference. Try resetting ${PREF_CONFIG} in about:config. ${e}`
       );
     }
     return this._prefCache.config;
   }
 
+  resetConfigDefauts() {
+    this.store.dispatch({
+      type: at.CLEAR_PREF,
+      data: {
+        name: PREF_CONFIG,
+      },
+    });
+  }
+
   get showSpocs() {
     // Combine user-set sponsored opt-out with Mozilla-set config
     return (
       this.store.getState().Prefs.values[PREF_SHOW_SPONSORED] &&
       this.config.show_spocs
     );
   }
 
@@ -1130,16 +1139,19 @@ this.DiscoveryStreamFeed = class Discove
             PREF_CONFIG,
             JSON.stringify({
               ...JSON.parse(this.store.getState().Prefs.values[PREF_CONFIG]),
               [action.data.name]: action.data.value,
             })
           )
         );
         break;
+      case at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS:
+        this.resetConfigDefauts();
+        break;
       case at.DISCOVERY_STREAM_RETRY_FEED:
         this.retryFeed(action.data.feed);
         break;
       case at.DISCOVERY_STREAM_CONFIG_CHANGE:
         // When the config pref changes, load or unload data as needed.
         await this.onPrefChange();
         break;
       case at.DISCOVERY_STREAM_IMPRESSION_STATS:
--- a/browser/components/newtab/lib/PrefsFeed.jsm
+++ b/browser/components/newtab/lib/PrefsFeed.jsm
@@ -106,16 +106,19 @@ this.PrefsFeed = class PrefsFeed {
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
         break;
       case at.UNINIT:
         this.removeListeners();
         break;
+      case at.CLEAR_PREF:
+        Services.prefs.clearUserPref(this._prefs._branchStr + action.data.name);
+        break;
       case at.SET_PREF:
         this._prefs.set(action.data.name, action.data.value);
         break;
       case at.UPDATE_SECTION_PREFS:
         this._setIndexedDBPref(action.data.id, action.data.value);
         break;
     }
   }
--- a/browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
+++ b/browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
@@ -86,16 +86,29 @@ const MESSAGES = () => [
       button_label: "Get one now!",
       button_url: "https://www.mozilla.org/en-US/firefox/accounts",
       text:
         "Sync it, link it, take it with you. All this and more with a Firefox Account.",
       block_button_text: "Block",
     },
   },
   {
+    id: "SIMPLE_TEST_BUTTON_ACTION_1",
+    template: "simple_snippet",
+    content: {
+      icon: TEST_ICON,
+      icon_dark_theme: TEST_ICON_BW,
+      button_label: "Open about:config",
+      button_action: "OPEN_ABOUT_PAGE",
+      button_action_args: "config",
+      text: "Testing the OPEN_ABOUT_PAGE action",
+      block_button_text: "Block",
+    },
+  },
+  {
     id: "SIMPLE_WITH_TITLE_TEST_1",
     template: "simple_snippet",
     content: {
       icon: TEST_ICON,
       icon_dark_theme: TEST_ICON_BW,
       title: "Ready to sync?",
       text: "Get connected with a <syncLink>Firefox account</syncLink>.",
       links: {
@@ -397,16 +410,37 @@ const MESSAGES = () => [
       icon_dark_theme: TEST_ICON_BW,
       button_label: "Find Out Now",
       button_url: "https://www.mozilla.org/en-US/firefox/accounts",
       title: "See if you've been part of an online data breach.",
       text: "Firefox Monitor tells you what hackers already know about you.",
       block_button_text: "Block",
     },
   },
+  {
+    id: "SPECIAL_SNIPPET_MONITOR",
+    template: "simple_below_search_snippet",
+    content: {
+      icon: TEST_ICON,
+      title: "See if you've been part of an online data breach.",
+      text: "Firefox Monitor tells you what hackers already know about you.",
+      button_label: "Get monitor",
+      button_action: "ENABLE_FIREFOX_MONITOR",
+      button_action_args: {
+        url:
+          "https://monitor.firefox.com/oauth/init?utm_source=snippets&utm_campaign=monitor-snippet-test&form_type=email&entrypoint=newtab",
+        flowRequestParams: {
+          entrypoint: "snippets",
+          utm_term: "monitor",
+          form_type: "email",
+        },
+      },
+      block_button_text: "Block",
+    },
+  },
 ];
 
 const SnippetsTestMessageProvider = {
   getMessages() {
     return (
       MESSAGES()
         // Ensures we never actually show test except when triggered by debug tools
         .map(message => ({
--- a/browser/components/newtab/lib/ToolbarBadgeHub.jsm
+++ b/browser/components/newtab/lib/ToolbarBadgeHub.jsm
@@ -51,17 +51,16 @@ ChromeUtils.defineModuleGetter(
 
 // Frequency at which to check for new messages
 const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000;
 let notificationsByWindow = new WeakMap();
 
 class _ToolbarBadgeHub {
   constructor() {
     this.id = "toolbar-badge-hub";
-    this.template = "toolbar_badge";
     this.state = null;
     this.prefs = {
       WHATSNEW_TOOLBAR_PANEL: "browser.messaging-system.whatsNewPanel.enabled",
       HOMEPAGE_OVERRIDE_PREF: "browser.startup.homepage_override.once",
     };
     this.removeAllNotifications = this.removeAllNotifications.bind(this);
     this.removeToolbarNotification = this.removeToolbarNotification.bind(this);
     this.addToolbarNotification = this.addToolbarNotification.bind(this);
@@ -88,17 +87,20 @@ class _ToolbarBadgeHub {
   ) {
     this._handleMessageRequest = handleMessageRequest;
     this._blockMessageById = blockMessageById;
     this._unblockMessageById = unblockMessageById;
     this._addImpression = addImpression;
     this._dispatch = dispatch;
     // Need to wait for ASRouter to initialize before trying to fetch messages
     await waitForInitialized;
-    this.messageRequest("toolbarBadgeUpdate");
+    this.messageRequest({
+      triggerId: "toolbarBadgeUpdate",
+      template: "toolbar_badge",
+    });
     // Listen for pref changes that could trigger new badges
     Services.prefs.addObserver(this.prefs.WHATSNEW_TOOLBAR_PANEL, this);
     const _intervalId = setInterval(
       () => this.checkHomepageOverridePref(),
       SYSTEM_TICK_INTERVAL
     );
     this.state = { _intervalId };
   }
@@ -122,23 +124,29 @@ class _ToolbarBadgeHub {
       try {
         message_id = JSON.parse(prefValue).message_id;
       } catch (e) {}
       if (message_id) {
         this._unblockMessageById(message_id);
       }
     }
 
-    this.messageRequest("momentsUpdate");
+    this.messageRequest({
+      triggerId: "momentsUpdate",
+      template: "update_action",
+    });
   }
 
   observe(aSubject, aTopic, aPrefName) {
     switch (aPrefName) {
       case this.prefs.WHATSNEW_TOOLBAR_PANEL:
-        this.messageRequest("toolbarBadgeUpdate");
+        this.messageRequest({
+          triggerId: "toolbarBadgeUpdate",
+          template: "toolbar_badge",
+        });
         break;
     }
   }
 
   executeAction({ id, data, message_id }) {
     switch (id) {
       case "show-whatsnew-button":
         ToolbarPanelHub.enableToolbarButton();
@@ -249,16 +257,22 @@ class _ToolbarBadgeHub {
   }
 
   registerBadgeToAllWindows(message) {
     // Impression should be added when the badge becomes visible
     this._addImpression(message);
     // Send a telemetry ping when adding the notification badge
     this.sendUserEventTelemetry("IMPRESSION", message);
 
+    if (message.template === "update_action") {
+      this.executeAction({ ...message.content.action, message_id: message.id });
+      // No badge to set only an action to execute
+      return;
+    }
+
     EveryWindow.registerCallback(
       this.id,
       win => {
         if (notificationsByWindow.has(win)) {
           // nothing to do
           return;
         }
         const el = this.addToolbarNotification(win, message);
@@ -285,20 +299,20 @@ class _ToolbarBadgeHub {
       this.state.showBadgeTimeoutId = setTimeout(() => {
         this.registerBadgeToAllWindows(message);
       }, message.content.delay);
     } else {
       this.registerBadgeToAllWindows(message);
     }
   }
 
-  async messageRequest(triggerId) {
+  async messageRequest({ triggerId, template }) {
     const message = await this._handleMessageRequest({
       triggerId,
-      template: this.template,
+      template,
     });
     if (message) {
       this.registerBadgeNotificationListener(message);
     }
   }
 
   _sendTelemetry(ping) {
     this._dispatch({
--- a/browser/components/newtab/locales-src/newtab.ftl
+++ b/browser/components/newtab/locales-src/newtab.ftl
@@ -108,16 +108,17 @@ newtab-menu-show-file =
 newtab-menu-open-file = Open File
 
 ## Card Labels: These labels are associated to pages to give
 ## context on how the element is related to the user, e.g. type indicates that
 ## the page is bookmarked, or is currently open on another device.
 
 newtab-label-visited = Visited
 newtab-label-bookmarked = Bookmarked
+newtab-label-removed-bookmark = Bookmark removed
 newtab-label-recommended = Trending
 newtab-label-saved = Saved to { -pocket-brand-name }
 newtab-label-download = Downloaded
 
 ## Section Menu: These strings are displayed in the section context menu and are
 ## meant as a call to action for the given section.
 
 newtab-section-menu-remove-section = Remove Section
--- a/browser/components/newtab/package-lock.json
+++ b/browser/components/newtab/package-lock.json
@@ -1713,17 +1713,28 @@
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/cached-iterable/-/cached-iterable-0.2.1.tgz",
       "integrity": "sha512-8zAVjMjdn/S/QXJaOnqsko0+ZJzXT2Dum2u9TMGg5YR9fxONPrUjuO9VYqnb1AoldXeYVAcNJLgT5Q8WaIJSgA=="
     },
     "caller-path": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
       "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "callsites": "^0.2.0"
+      },
+      "dependencies": {
+        "callsites": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz",
+          "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
+          "dev": true
+        }
+      }
     },
     "callsite": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
       "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
       "dev": true
     },
     "callsites": {
@@ -2475,16 +2486,24 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
       "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
       "dev": true,
       "requires": {
         "esutils": "^2.0.2"
       }
     },
+    "dom-helpers": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
+      "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
+      "requires": {
+        "@babel/runtime": "^7.1.2"
+      }
+    },
     "dom-serialize": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
       "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
       "dev": true,
       "requires": {
         "custom-event": "~1.0.0",
         "ent": "~2.2.0",
@@ -8622,16 +8641,37 @@
         "react-is": {
           "version": "16.8.6",
           "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
           "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
           "dev": true
         }
       }
     },
+    "react-transition-group": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.2.1.tgz",
+      "integrity": "sha512-IXrPr93VzCPupwm2O6n6C2kJIofJ/Rp5Ltihhm9UfE8lkuVX2ng/SUUl/oWjblybK9Fq2Io7LGa6maVqPB762Q==",
+      "requires": {
+        "@babel/runtime": "^7.4.5",
+        "dom-helpers": "^3.4.0",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "dependencies": {
+        "@babel/runtime": {
+          "version": "7.5.5",
+          "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz",
+          "integrity": "sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==",
+          "requires": {
+            "regenerator-runtime": "^0.13.2"
+          }
+        }
+      }
+    },
     "read-pkg": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
       "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
       "dev": true,
       "requires": {
         "load-json-file": "^2.0.0",
         "normalize-package-data": "^2.3.2",
--- a/browser/components/newtab/package.json
+++ b/browser/components/newtab/package.json
@@ -7,16 +7,17 @@
     "url": "https://github.com/mozilla/activity-stream/issues"
   },
   "dependencies": {
     "fluent": "0.12.0",
     "fluent-react": "0.8.4",
     "react": "16.8.6",
     "react-dom": "16.8.6",
     "react-redux": "7.0.3",
+    "react-transition-group": "4.2.1",
     "redux": "4.0.1",
     "reselect": "4.0.0"
   },
   "devDependencies": {
     "@babel/core": "7.4.5",
     "@babel/plugin-proposal-async-generator-functions": "7.2.0",
     "@babel/preset-react": "7.0.0",
     "acorn": "6.1.1",
--- a/browser/components/newtab/prerendered/activity-stream-debug.html
+++ b/browser/components/newtab/prerendered/activity-stream-debug.html
@@ -20,11 +20,12 @@
     <div id="footer-asrouter-container" role="presentation"></div>
     <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="chrome://browser/content/contentTheme.js"></script>
     <script src="resource://activity-stream/vendor/react-dev.js"></script>
     <script src="resource://activity-stream/vendor/react-dom-dev.js"></script>
     <script src="resource://activity-stream/vendor/prop-types.js"></script>
     <script src="resource://activity-stream/vendor/redux.js"></script>
     <script src="resource://activity-stream/vendor/react-redux.js"></script>
+    <script src="resource://activity-stream/vendor/react-transition-group.js"></script>
     <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
   </body>
 </html>
--- a/browser/components/newtab/prerendered/activity-stream.html
+++ b/browser/components/newtab/prerendered/activity-stream.html
@@ -20,11 +20,12 @@
     <div id="footer-asrouter-container" role="presentation"></div>
     <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="chrome://browser/content/contentTheme.js"></script>
     <script src="resource://activity-stream/vendor/react.js"></script>
     <script src="resource://activity-stream/vendor/react-dom.js"></script>
     <script src="resource://activity-stream/vendor/prop-types.js"></script>
     <script src="resource://activity-stream/vendor/redux.js"></script>
     <script src="resource://activity-stream/vendor/react-redux.js"></script>
+    <script src="resource://activity-stream/vendor/react-transition-group.js"></script>
     <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
   </body>
 </html>
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -924,16 +924,40 @@ describe("ASRouter", () => {
       const result = await Router.handleMessageRequest({
         template: "whatsnew-panel",
         triggerId: "whatsNewPanelOpened",
         returnAll: true,
       });
 
       assert.deepEqual(result, [message2, message1]);
     });
+    it("should forward trigger param info", async () => {
+      const trigger = { triggerId: "foo", triggerParam: "bar" };
+      const message1 = {
+        id: "1",
+        campaign: "foocampaign",
+        trigger: { id: "foo" },
+      };
+      const message2 = {
+        id: "2",
+        campaign: "foocampaign",
+        trigger: { id: "bar" },
+      };
+      await Router.setState({ messages: [message2, message1] });
+      // Just return the first message provided as arg
+      const stub = sandbox.stub(Router, "_findMessage");
+
+      Router.handleMessageRequest(trigger);
+
+      assert.calledOnce(stub);
+      assert.calledWithExactly(stub, sinon.match.array, {
+        id: trigger.triggerId,
+        param: trigger.triggerParam,
+      });
+    });
   });
 
   describe("#uninit", () => {
     it("should remove the message listener on the RemotePageManager", () => {
       const [, listenerAdded] = channel.addMessageListener.firstCall.args;
       assert.isFunction(listenerAdded);
 
       Router.uninit();
@@ -1519,16 +1543,17 @@ describe("ASRouter", () => {
           type: "TRIGGER",
           data: { trigger: { id: "firstRun" } },
         });
         await Router.onMessage(msg);
 
         assert.calledOnce(Router._findMessage);
         assert.deepEqual(Router._findMessage.firstCall.args[1], {
           id: "firstRun",
+          param: undefined,
         });
       });
       it("consider the trigger when picking a message", async () => {
         const messages = [
           {
             id: "foo1",
             template: "simple_template",
             bundled: 1,
--- a/browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
@@ -4,16 +4,17 @@ import {
 } from "content-src/asrouter/asrouter-content";
 import { GlobalOverrider } from "test/unit/utils";
 import { OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME } from "content-src/lib/init-store";
 import { FAKE_LOCAL_MESSAGES } from "./constants";
 import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
 import React from "react";
 import { mount } from "enzyme";
 import { Trailhead } from "../../../content-src/asrouter/templates/Trailhead/Trailhead";
+import { actionCreators as ac } from "common/Actions.jsm";
 
 let [FAKE_MESSAGE] = FAKE_LOCAL_MESSAGES;
 const FAKE_NEWSLETTER_SNIPPET = FAKE_LOCAL_MESSAGES.find(
   msg => msg.id === "newsletter"
 );
 const FAKE_FXA_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === "fxa");
 const FAKE_BELOW_SEARCH_SNIPPET = FAKE_LOCAL_MESSAGES.find(
   msg => msg.id === "belowsearch"
@@ -43,27 +44,33 @@ describe("ASRouterUtils", () => {
     const [, payload] = fakeSendAsyncMessage.firstCall.args;
     assert.propertyVal(payload.data, "id", 1);
     assert.propertyVal(payload.data, "event", "CLICK");
   });
 });
 
 describe("ASRouterUISurface", () => {
   let wrapper;
-  let global;
+  let globalO;
   let sandbox;
   let headerPortal;
   let footerPortal;
   let fakeDocument;
+  let fetchStub;
 
   beforeEach(() => {
     sandbox = sinon.createSandbox();
     headerPortal = document.createElement("div");
     footerPortal = document.createElement("div");
     sandbox.stub(footerPortal, "querySelector").returns(footerPortal);
+    fetchStub = sandbox.stub(global, "fetch").resolves({
+      ok: true,
+      status: 200,
+      json: () => Promise.resolve({}),
+    });
     fakeDocument = {
       location: { href: "" },
       _listeners: new Set(),
       _visibilityState: "hidden",
       head: {
         appendChild(el) {
           return el;
         },
@@ -94,31 +101,31 @@ describe("ASRouterUISurface", () => {
           default:
             return footerPortal;
         }
       },
       createElement(tag) {
         return document.createElement(tag);
       },
     };
-    global = new GlobalOverrider();
-    global.set({
+    globalO = new GlobalOverrider();
+    globalO.set({
       RPMAddMessageListener: sandbox.stub(),
       RPMRemoveMessageListener: sandbox.stub(),
       RPMSendAsyncMessage: sandbox.stub(),
     });
 
     sandbox.stub(ASRouterUtils, "sendTelemetry");
 
     wrapper = mount(<ASRouterUISurface document={fakeDocument} />);
   });
 
   afterEach(() => {
     sandbox.restore();
-    global.restore();
+    globalO.restore();
   });
 
   it("should render the component if a message id is defined", () => {
     wrapper.setState({ message: FAKE_MESSAGE });
     assert.isTrue(wrapper.exists());
   });
 
   it("should pass in the correct form_method for newsletter snippets", () => {
@@ -362,9 +369,149 @@ describe("ASRouterUISurface", () => {
       assert.propertyVal(
         payload,
         "action",
         `${FAKE_MESSAGE.provider}_user_event`
       );
       assert.propertyVal(payload, "source", "NEWTAB_FOOTER_BAR");
     });
   });
+
+  describe(".fetchFlowParams", () => {
+    let dispatchStub;
+    const assertCalledWithURL = url =>
+      assert.calledWith(fetchStub, new URL(url).toString(), {
+        credentials: "omit",
+      });
+    beforeEach(() => {
+      dispatchStub = sandbox.stub();
+      wrapper = mount(
+        <ASRouterUISurface
+          dispatch={dispatchStub}
+          fxaEndpoint="https://accounts.firefox.com"
+        />
+      );
+    });
+    it("should use the base url returned from the endpoint pref", async () => {
+      wrapper = mount(
+        <ASRouterUISurface
+          dispatch={dispatchStub}
+          fxaEndpoint="https://foo.com"
+        />
+      );
+      await wrapper.instance().fetchFlowParams();
+
+      assertCalledWithURL("https://foo.com/metrics-flow");
+    });
+    it("should add given search params to the URL", async () => {
+      const params = { foo: "1", bar: "2" };
+
+      await wrapper.instance().fetchFlowParams(params);
+
+      assertCalledWithURL(
+        "https://accounts.firefox.com/metrics-flow?foo=1&bar=2"
+      );
+    });
+    it("should return flowId, flowBeginTime, deviceId on a 200 response", async () => {
+      const flowInfo = { flowId: "foo", flowBeginTime: 123, deviceId: "bar" };
+      fetchStub.withArgs("https://accounts.firefox.com/metrics-flow").resolves({
+        ok: true,
+        status: 200,
+        json: () => Promise.resolve(flowInfo),
+      });
+
+      const result = await wrapper.instance().fetchFlowParams();
+      assert.deepEqual(result, flowInfo);
+    });
+    it("should return {} and dispatch a TELEMETRY_UNDESIRED_EVENT on a non-200 response", async () => {
+      fetchStub.withArgs("https://accounts.firefox.com/metrics-flow").resolves({
+        ok: false,
+        status: 400,
+        statusText: "Client error",
+        url: "https://accounts.firefox.com/metrics-flow",
+      });
+
+      const result = await wrapper.instance().fetchFlowParams();
+      assert.deepEqual(result, {});
+      assert.calledWith(
+        dispatchStub,
+        ac.OnlyToMain({
+          type: "TELEMETRY_UNDESIRED_EVENT",
+          data: {
+            event: "FXA_METRICS_FETCH_ERROR",
+            value: 400,
+          },
+        })
+      );
+    });
+    it("should return {} and dispatch a TELEMETRY_UNDESIRED_EVENT on a parsing erorr", async () => {
+      fetchStub.withArgs("https://accounts.firefox.com/metrics-flow").resolves({
+        ok: false,
+        status: 200,
+        // No json to parse, throws an error
+      });
+
+      const result = await wrapper.instance().fetchFlowParams();
+      assert.deepEqual(result, {});
+      assert.calledWith(
+        dispatchStub,
+        ac.OnlyToMain({
+          type: "TELEMETRY_UNDESIRED_EVENT",
+          data: { event: "FXA_METRICS_ERROR" },
+        })
+      );
+    });
+
+    describe(".onUserAction", () => {
+      it("if the action.type is ENABLE_FIREFOX_MONITOR, it should generate the right monitor URL given some flowParams", async () => {
+        const flowInfo = { flowId: "foo", flowBeginTime: 123, deviceId: "bar" };
+        fetchStub
+          .withArgs(
+            "https://accounts.firefox.com/metrics-flow?utm_term=avocado"
+          )
+          .resolves({
+            ok: true,
+            status: 200,
+            json: () => Promise.resolve(flowInfo),
+          });
+
+        sandbox.spy(ASRouterUtils, "executeAction");
+
+        const msg = {
+          type: "ENABLE_FIREFOX_MONITOR",
+          data: {
+            args: {
+              url: "https://monitor.firefox.com?foo=bar",
+              flowRequestParams: {
+                utm_term: "avocado",
+              },
+            },
+          },
+        };
+
+        await wrapper.instance().onUserAction(msg);
+
+        assertCalledWithURL(
+          "https://accounts.firefox.com/metrics-flow?utm_term=avocado"
+        );
+        assert.calledWith(ASRouterUtils.executeAction, {
+          type: "OPEN_URL",
+          data: {
+            args: new URL(
+              "https://monitor.firefox.com?foo=bar&deviceId=bar&flowId=foo&flowBeginTime=123"
+            ).toString(),
+          },
+        });
+      });
+      it("if the action.type is not ENABLE_FIREFOX_MONITOR, it should just call ASRouterUtils.executeAction", async () => {
+        const msg = {
+          type: "FOO",
+          data: {
+            args: "bar",
+          },
+        };
+        sandbox.spy(ASRouterUtils, "executeAction");
+        await wrapper.instance().onUserAction(msg);
+        assert.calledWith(ASRouterUtils.executeAction, msg);
+      });
+    });
+  });
 });
--- a/browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx
@@ -1,10 +1,9 @@
 import {
-  helpers,
   FirstRun,
   FLUENT_FILES,
 } from "content-src/asrouter/templates/FirstRun/FirstRun";
 import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt";
 import { Triplets } from "content-src/asrouter/templates/FirstRun/Triplets";
 import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
 import { mount } from "enzyme";
 import React from "react";
@@ -129,51 +128,58 @@ describe("<FirstRun>", () => {
       message = { type: "FOO_123" };
       wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
 
       assert.isTrue(wrapper.isEmptyRender());
     });
   });
 
   it("should load flow params on mount if fxaEndpoint is defined", () => {
-    const spy = sandbox.spy(helpers, "fetchFlowParams");
+    const stub = sandbox.stub();
     wrapper = mount(
       <FirstRun
         message={message}
         document={fakeDoc}
         dispatch={() => {}}
+        fetchFlowParams={stub}
         fxaEndpoint="https://foo.com"
       />
     );
-    assert.calledOnce(spy);
+    assert.calledOnce(stub);
   });
 
   it("should load flow params onUpdate if fxaEndpoint is not defined on mount and then later defined", () => {
-    const spy = sandbox.spy(helpers, "fetchFlowParams");
+    const stub = sandbox.stub();
     wrapper = mount(
-      <FirstRun message={message} document={fakeDoc} dispatch={() => {}} />
+      <FirstRun
+        message={message}
+        document={fakeDoc}
+        fetchFlowParams={stub}
+        dispatch={() => {}}
+      />
     );
-    assert.notCalled(spy);
+    assert.notCalled(stub);
     wrapper.setProps({ fxaEndpoint: "https://foo.com" });
-    assert.calledOnce(spy);
+    assert.calledOnce(stub);
   });
 
   it("should not load flow params again onUpdate if they were already set", () => {
-    const spy = sandbox.spy(helpers, "fetchFlowParams");
+    const stub = sandbox.stub();
     wrapper = mount(
       <FirstRun
         message={message}
         document={fakeDoc}
         dispatch={() => {}}
+        fetchFlowParams={stub}
         fxaEndpoint="https://foo.com"
       />
     );
     wrapper.setProps({ foo: "bar" });
     wrapper.setProps({ foo: "baz" });
-    assert.calledOnce(spy);
+    assert.calledOnce(stub);
   });
 
   it("should load fluent files on mount", () => {
     assert.lengthOf(fakeDoc.head.querySelectorAll("link"), FLUENT_FILES.length);
   });
 
   it("should hide the interrupt and show the triplets when onNextScene is called", () => {
     // Simulate calling next scene
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
@@ -0,0 +1,66 @@
+import { DSContextFooter } from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter";
+import React from "react";
+import { mount } from "enzyme";
+import { cardContextTypes } from "content-src/components/Card/types.js";
+
+describe("<DSContextFooter>", () => {
+  let wrapper;
+  let sandbox;
+  const bookmarkBadge = "bookmark";
+  const removeBookmarkBadge = "removedBookmark";
+
+  beforeEach(() => {
+    wrapper = mount(<DSContextFooter />);
+    sandbox = sinon.createSandbox();
+  });
+
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  it("should render", () => {
+    assert.isTrue(wrapper.exists());
+    assert.isOk(wrapper.find(".story-footer"));
+  });
+  it("should render a badge if a proper badge prop is passed", () => {
+    wrapper = mount(<DSContextFooter context_type={bookmarkBadge} />);
+    const { fluentID } = cardContextTypes[bookmarkBadge];
+
+    const statusLabel = wrapper.find(".story-context-label");
+    assert.isOk(statusLabel);
+    assert.equal(statusLabel.prop("data-l10n-id"), fluentID);
+  });
+  it("should only render a sponsored context if pass a sponsored context", async () => {
+    wrapper = mount(
+      <DSContextFooter
+        context_type={bookmarkBadge}
+        context="Sponsored by Babel"
+      />
+    );
+
+    assert.isFalse(wrapper.find(".status-message").exists());
+    assert.isTrue(wrapper.find(".story-sponsored-label").exists());
+  });
+  it("should render a new badge if props change from an old badge to a new one", async () => {
+    wrapper = mount(<DSContextFooter context_type={bookmarkBadge} />);
+
+    const { fluentID: bookmarkFluentID } = cardContextTypes[bookmarkBadge];
+    const bookmarkStatusMessage = wrapper.find(
+      `div[data-l10n-id='${bookmarkFluentID}']`
+    );
+    assert.isTrue(bookmarkStatusMessage.exists());
+
+    const { fluentID: removeBookmarkFluentID } = cardContextTypes[
+      removeBookmarkBadge
+    ];
+
+    wrapper.setProps({ context_type: removeBookmarkBadge });
+    await wrapper.update();
+
+    assert.isEmpty(bookmarkStatusMessage);
+    const removedBookmarkStatusMessage = wrapper.find(
+      `div[data-l10n-id='${removeBookmarkFluentID}']`
+    );
+    assert.isTrue(removedBookmarkStatusMessage.exists());
+  });
+});
--- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
@@ -1484,16 +1484,32 @@ describe("DiscoveryStreamFeed", () => {
             layout_endpoint: "foo.com",
           }),
         },
         type: at.SET_PREF,
       });
     });
   });
 
+  describe("#onAction: DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS", async () => {
+    it("Should dispatch CLEAR_PREF with pref name", async () => {
+      sandbox.spy(feed.store, "dispatch");
+      await feed.onAction({
+        type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS,
+      });
+
+      assert.calledWithMatch(feed.store.dispatch, {
+        data: {
+          name: CONFIG_PREF_NAME,
+        },
+        type: at.CLEAR_PREF,
+      });
+    });
+  });
+
   describe("#onAction: DISCOVERY_STREAM_RETRY_FEED", async () => {
     it("should call retryFeed", async () => {
       sandbox.spy(feed, "retryFeed");
       feed.onAction({
         type: at.DISCOVERY_STREAM_RETRY_FEED,
         data: { feed: { url: "https://feed.com" } },
       });
       assert.calledOnce(feed.retryFeed);
--- a/browser/components/newtab/test/unit/lib/PrefsFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/PrefsFeed.test.js
@@ -3,28 +3,36 @@ import { GlobalOverrider } from "test/un
 import { PrefsFeed } from "lib/PrefsFeed.jsm";
 
 let overrider = new GlobalOverrider();
 
 describe("PrefsFeed", () => {
   let feed;
   let FAKE_PREFS;
   let sandbox;
+  let ServicesStub;
   beforeEach(() => {
     sandbox = sinon.createSandbox();
     FAKE_PREFS = new Map([
       ["foo", 1],
       ["bar", 2],
       ["baz", { value: 1, skipBroadcast: true }],
     ]);
     feed = new PrefsFeed(FAKE_PREFS);
     const storage = {
       getAll: sandbox.stub().resolves(),
       set: sandbox.stub().resolves(),
     };
+    ServicesStub = {
+      prefs: {
+        clearUserPref: sinon.spy(),
+        getStringPref: sinon.spy(),
+        getBoolPref: sinon.spy(),
+      },
+    };
     feed.store = {
       dispatch: sinon.spy(),
       getState() {
         return this.state;
       },
       dbStorage: { getDbTable: sandbox.stub().returns(storage) },
     };
     // Setup for tests that don't call `init`
@@ -32,28 +40,36 @@ describe("PrefsFeed", () => {
     feed._prefs = {
       get: sinon.spy(item => FAKE_PREFS.get(item)),
       set: sinon.spy((name, value) => FAKE_PREFS.set(name, value)),
       observe: sinon.spy(),
       observeBranch: sinon.spy(),
       ignore: sinon.spy(),
       ignoreBranch: sinon.spy(),
       reset: sinon.stub(),
+      _branchStr: "branch.str.",
     };
-    overrider.set({ PrivateBrowsingUtils: { enabled: true } });
+    overrider.set({
+      PrivateBrowsingUtils: { enabled: true },
+      Services: ServicesStub,
+    });
   });
   afterEach(() => {
     overrider.restore();
     sandbox.restore();
   });
 
   it("should set a pref when a SET_PREF action is received", () => {
     feed.onAction(ac.SetPref("foo", 2));
     assert.calledWith(feed._prefs.set, "foo", 2);
   });
+  it("should call clearUserPref with action CLEAR_PREF", () => {
+    feed.onAction({ type: at.CLEAR_PREF, data: { name: "pref.test" } });
+    assert.calledWith(ServicesStub.prefs.clearUserPref, "branch.str.pref.test");
+  });
   it("should dispatch PREFS_INITIAL_VALUES on init with pref values and .isPrivateBrowsingEnabled", () => {
     feed.onAction({ type: at.INIT });
     assert.calledOnce(feed.store.dispatch);
     assert.equal(
       feed.store.dispatch.firstCall.args[0].type,
       at.PREFS_INITIAL_VALUES
     );
     const [{ data }] = feed.store.dispatch.firstCall.args;
--- a/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
+++ b/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
@@ -96,17 +96,20 @@ describe("ToolbarBadgeHub", () => {
   });
   describe("#init", () => {
     it("should make a messageRequest on init", async () => {
       sandbox.stub(instance, "messageRequest");
       const waitForInitialized = sandbox.stub().resolves();
 
       await instance.init(waitForInitialized, {});
       assert.calledOnce(instance.messageRequest);
-      assert.calledWithExactly(instance.messageRequest, "toolbarBadgeUpdate");
+      assert.calledWithExactly(instance.messageRequest, {
+        template: "toolbar_badge",
+        triggerId: "toolbarBadgeUpdate",
+      });
     });
     it("should add a pref observer", async () => {
       await instance.init(sandbox.stub().resolves(), {});
 
       assert.calledOnce(addObserverStub);
       assert.calledWithExactly(
         addObserverStub,
         instance.prefs.WHATSNEW_TOOLBAR_PANEL,
@@ -162,22 +165,25 @@ describe("ToolbarBadgeHub", () => {
     beforeEach(() => {
       handleMessageRequestStub = sandbox.stub().returns(fxaMessage);
       sandbox
         .stub(instance, "_handleMessageRequest")
         .value(handleMessageRequestStub);
       sandbox.stub(instance, "registerBadgeNotificationListener");
     });
     it("should fetch a message with the provided trigger and template", async () => {
-      await instance.messageRequest("trigger");
+      await instance.messageRequest({
+        triggerId: "trigger",
+        template: "template",
+      });
 
       assert.calledOnce(handleMessageRequestStub);
       assert.calledWithExactly(handleMessageRequestStub, {
         triggerId: "trigger",
-        template: instance.template,
+        template: "template",
       });
     });
     it("should call addToolbarNotification with browser window and message", async () => {
       await instance.messageRequest("trigger");
 
       assert.calledOnce(instance.registerBadgeNotificationListener);
       assert.calledWithExactly(
         instance.registerBadgeNotificationListener,
@@ -318,16 +324,25 @@ describe("ToolbarBadgeHub", () => {
       );
     });
     it("should unregister notifications when forcing a badge via devtools", () => {
       instance.registerBadgeNotificationListener(msg_no_delay, { force: true });
 
       assert.calledOnce(everyWindowStub.unregisterCallback);
       assert.calledWithExactly(everyWindowStub.unregisterCallback, instance.id);
     });
+    it("should only call executeAction for 'update_action' messages", () => {
+      const stub = sandbox.stub(instance, "executeAction");
+      const updateActionMsg = { ...msg_no_delay, template: "update_action" };
+
+      instance.registerBadgeNotificationListener(updateActionMsg);
+
+      assert.notCalled(everyWindowStub.registerCallback);
+      assert.calledOnce(stub);
+    });
   });
   describe("executeAction", () => {
     let blockMessageByIdStub;
     beforeEach(async () => {
       blockMessageByIdStub = sandbox.stub();
       await instance.init(sandbox.stub().resolves(), {
         blockMessageById: blockMessageByIdStub,
       });
@@ -399,17 +414,17 @@ describe("ToolbarBadgeHub", () => {
     });
   });
   describe("removeToolbarNotification", () => {
     it("should remove the notification", () => {
       instance.removeToolbarNotification(fakeElement);
 
       assert.calledOnce(fakeElement.removeAttribute);
       assert.calledWithExactly(fakeElement.removeAttribute, "badged");
-      assert.calledTwice(fakeElement.classList.remove);
+      assert.calledOnce(fakeElement.classList.remove);
       assert.calledWithExactly(fakeElement.classList.remove, "feature-callout");
     });
   });
   describe("removeAllNotifications", () => {
     let blockMessageByIdStub;
     let fakeEvent;
     beforeEach(async () => {
       await instance.init(sandbox.stub().resolves(), {
@@ -575,17 +590,20 @@ describe("ToolbarBadgeHub", () => {
   });
   describe("#observe", () => {
     it("should make a message request when the whats new pref is changed", () => {
       sandbox.stub(instance, "messageRequest");
 
       instance.observe("", "", instance.prefs.WHATSNEW_TOOLBAR_PANEL);
 
       assert.calledOnce(instance.messageRequest);
-      assert.calledWithExactly(instance.messageRequest, "toolbarBadgeUpdate");
+      assert.calledWithExactly(instance.messageRequest, {
+        template: "toolbar_badge",
+        triggerId: "toolbarBadgeUpdate",
+      });
     });
     it("should not react to other pref changes", () => {
       sandbox.stub(instance, "messageRequest");
 
       instance.observe("", "", "foo");
 
       assert.notCalled(instance.messageRequest);
     });
@@ -627,12 +645,15 @@ describe("ToolbarBadgeHub", () => {
     });
     it("should catch parse errors", () => {
       getStringPrefStub.returns({});
 
       instance.checkHomepageOverridePref();
 
       assert.notCalled(unblockMessageByIdStub);
       assert.calledOnce(messageRequestStub);
-      assert.calledWithExactly(messageRequestStub, "momentsUpdate");
+      assert.calledWithExactly(messageRequestStub, {
+        template: "update_action",
+        triggerId: "momentsUpdate",
+      });
     });
   });
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/vendor/REACT_TRANSITION_GROUP_LICENSE
@@ -0,0 +1,30 @@
+BSD 3-Clause License
+
+Copyright (c) 2018, React Community
+Forked from React (https://github.com/facebook/react) Copyright 2013-present, Facebook, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/vendor/react-transition-group.js
@@ -0,0 +1,1 @@
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react"),require("react-dom")):"function"==typeof define&&define.amd?define(["exports","react","react-dom"],t):t((e=e||self).ReactTransitionGroup={},e.React,e.ReactDOM)}(this,function(e,t,n){"use strict";var r="default"in t?t.default:t,i="default"in n?n.default:n;function o(){return(o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e}).apply(this,arguments)}function a(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r<o.length;r++)n=o[r],t.indexOf(n)>=0||(i[n]=e[n]);return i}function s(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.__proto__=t}function l(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function c(e,t){return e(t={exports:{}},t.exports),t.exports}var u=Object.getOwnPropertySymbols,p=Object.prototype.hasOwnProperty,d=Object.prototype.propertyIsEnumerable;(function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map(function(e){return t[e]}).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(e){r[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}})()&&Object.assign;var f="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";function h(){}var E=c(function(e){e.exports=function(){function e(e,t,n,r,i,o){if(o!==f){var a=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw a.name="Invariant Violation",a}}function t(){return e}e.isRequired=e;var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t};return n.checkPropTypes=h,n.PropTypes=n,n}()}),m=(E.object,E.oneOfType,E.element,E.bool,E.func,c(function(e){e.exports=function(e){return e&&e.__esModule?e:{default:e}}}));l(m);var x=c(function(e,t){t.__esModule=!0,t.default=function(e,t){return e.classList?!!t&&e.classList.contains(t):-1!==(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")},e.exports=t.default});l(x);var v=l(c(function(e,t){t.__esModule=!0,t.default=function(e,t){e.classList?e.classList.add(t):(0,n.default)(e,t)||("string"==typeof e.className?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))};var n=m(x);e.exports=t.default}));function y(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}var g={disabled:!1},b=r.createContext(null),C="unmounted",O="exited",S="entering",N="entered",k=function(e){function t(t,n){var r;r=e.call(this,t,n)||this;var i,o=n&&!n.isMounting?t.enter:t.appear;return r.appearStatus=null,t.in?o?(i=O,r.appearStatus=S):i=N:i=t.unmountOnExit||t.mountOnEnter?C:O,r.state={status:i},r.nextCallback=null,r}s(t,e),t.getDerivedStateFromProps=function(e,t){return e.in&&t.status===C?{status:O}:null};var n=t.prototype;return n.componentDidMount=function(){this.updateStatus(!0,this.appearStatus)},n.componentDidUpdate=function(e){var t=null;if(e!==this.props){var n=this.state.status;this.props.in?n!==S&&n!==N&&(t=S):n!==S&&n!==N||(t="exiting")}this.updateStatus(!1,t)},n.componentWillUnmount=function(){this.cancelNextCallback()},n.getTimeouts=function(){var e,t,n,r=this.props.timeout;return e=t=n=r,null!=r&&"number"!=typeof r&&(e=r.exit,t=r.enter,n=void 0!==r.appear?r.appear:t),{exit:e,enter:t,appear:n}},n.updateStatus=function(e,t){if(void 0===e&&(e=!1),null!==t){this.cancelNextCallback();var n=i.findDOMNode(this);t===S?this.performEnter(n,e):this.performExit(n)}else this.props.unmountOnExit&&this.state.status===O&&this.setState({status:C})},n.performEnter=function(e,t){var n=this,r=this.props.enter,i=this.context?this.context.isMounting:t,o=this.getTimeouts(),a=i?o.appear:o.enter;!t&&!r||g.disabled?this.safeSetState({status:N},function(){n.props.onEntered(e)}):(this.props.onEnter(e,i),this.safeSetState({status:S},function(){n.props.onEntering(e,i),n.onTransitionEnd(e,a,function(){n.safeSetState({status:N},function(){n.props.onEntered(e,i)})})}))},n.performExit=function(e){var t=this,n=this.props.exit,r=this.getTimeouts();n&&!g.disabled?(this.props.onExit(e),this.safeSetState({status:"exiting"},function(){t.props.onExiting(e),t.onTransitionEnd(e,r.exit,function(){t.safeSetState({status:O},function(){t.props.onExited(e)})})})):this.safeSetState({status:O},function(){t.props.onExited(e)})},n.cancelNextCallback=function(){null!==this.nextCallback&&(this.nextCallback.cancel(),this.nextCallback=null)},n.safeSetState=function(e,t){t=this.setNextCallback(t),this.setState(e,t)},n.setNextCallback=function(e){var t=this,n=!0;return this.nextCallback=function(r){n&&(n=!1,t.nextCallback=null,e(r))},this.nextCallback.cancel=function(){n=!1},this.nextCallback},n.onTransitionEnd=function(e,t,n){this.setNextCallback(n);var r=null==t&&!this.props.addEndListener;e&&!r?(this.props.addEndListener&&this.props.addEndListener(e,this.nextCallback),null!=t&&setTimeout(this.nextCallback,t)):setTimeout(this.nextCallback,0)},n.render=function(){var e=this.state.status;if(e===C)return null;var t=this.props,n=t.children,i=a(t,["children"]);if(delete i.in,delete i.mountOnEnter,delete i.unmountOnExit,delete i.appear,delete i.enter,delete i.exit,delete i.timeout,delete i.addEndListener,delete i.onEnter,delete i.onEntering,delete i.onEntered,delete i.onExit,delete i.onExiting,delete i.onExited,"function"==typeof n)return r.createElement(b.Provider,{value:null},n(e,i));var o=r.Children.only(n);return r.createElement(b.Provider,{value:null},r.cloneElement(o,i))},t}(r.Component);function T(){}k.contextType=b,k.propTypes={},k.defaultProps={in:!1,mountOnEnter:!1,unmountOnExit:!1,appear:!1,enter:!0,exit:!0,onEnter:T,onEntering:T,onEntered:T,onExit:T,onExiting:T,onExited:T},k.UNMOUNTED=0,k.EXITED=1,k.ENTERING=2,k.ENTERED=3,k.EXITING=4;var j=function(e,t){return e&&t&&t.split(" ").forEach(function(t){return r=t,void((n=e).classList?n.classList.remove(r):"string"==typeof n.className?n.className=y(n.className,r):n.setAttribute("class",y(n.className&&n.className.baseVal||"",r)));var n,r})},P=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),i=0;i<n;i++)r[i]=arguments[i];return(t=e.call.apply(e,[this].concat(r))||this).appliedClasses={appear:{},enter:{},exit:{}},t.onEnter=function(e,n){t.removeClasses(e,"exit"),t.addClass(e,n?"appear":"enter","base"),t.props.onEnter&&t.props.onEnter(e,n)},t.onEntering=function(e,n){var r=n?"appear":"enter";t.addClass(e,r,"active"),t.props.onEntering&&t.props.onEntering(e,n)},t.onEntered=function(e,n){var r=n?"appear":"enter";t.removeClasses(e,r),t.addClass(e,r,"done"),t.props.onEntered&&t.props.onEntered(e,n)},t.onExit=function(e){t.removeClasses(e,"appear"),t.removeClasses(e,"enter"),t.addClass(e,"exit","base"),t.props.onExit&&t.props.onExit(e)},t.onExiting=function(e){t.addClass(e,"exit","active"),t.props.onExiting&&t.props.onExiting(e)},t.onExited=function(e){t.removeClasses(e,"exit"),t.addClass(e,"exit","done"),t.props.onExited&&t.props.onExited(e)},t.getClassNames=function(e){var n=t.props.classNames,r="string"==typeof n,i=r?""+(r&&n?n+"-":"")+e:n[e];return{baseClassName:i,activeClassName:r?i+"-active":n[e+"Active"],doneClassName:r?i+"-done":n[e+"Done"]}},t}s(t,e);var n=t.prototype;return n.addClass=function(e,t,n){var r=this.getClassNames(t)[n+"ClassName"];"appear"===t&&"done"===n&&(r+=" "+this.getClassNames("enter").doneClassName),"active"===n&&e&&e.scrollTop,this.appliedClasses[t][n]=r,function(e,t){e&&t&&t.split(" ").forEach(function(t){return v(e,t)})}(e,r)},n.removeClasses=function(e,t){var n=this.appliedClasses[t],r=n.base,i=n.active,o=n.done;this.appliedClasses[t]={},r&&j(e,r),i&&j(e,i),o&&j(e,o)},n.render=function(){var e=this.props,t=(e.classNames,a(e,["classNames"]));return r.createElement(k,o({},t,{onEnter:this.onEnter,onEntered:this.onEntered,onEntering:this.onEntering,onExit:this.onExit,onExiting:this.onExiting,onExited:this.onExited}))},t}(r.Component);function _(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function w(e,n){var r=Object.create(null);return e&&t.Children.map(e,function(e){return e}).forEach(function(e){r[e.key]=function(e){return n&&t.isValidElement(e)?n(e):e}(e)}),r}function L(e,t,n){return null!=n[t]?n[t]:e.props[t]}function M(e,n,r){var i=w(e.children),o=function(e,t){function n(n){return n in t?t[n]:e[n]}e=e||{},t=t||{};var r,i=Object.create(null),o=[];for(var a in e)a in t?o.length&&(i[a]=o,o=[]):o.push(a);var s={};for(var l in t){if(i[l])for(r=0;r<i[l].length;r++){var c=i[l][r];s[i[l][r]]=n(c)}s[l]=n(l)}for(r=0;r<o.length;r++)s[o[r]]=n(o[r]);return s}(n,i);return Object.keys(o).forEach(function(a){var s=o[a];if(t.isValidElement(s)){var l=a in n,c=a in i,u=n[a],p=t.isValidElement(u)&&!u.props.in;!c||l&&!p?c||!l||p?c&&l&&t.isValidElement(u)&&(o[a]=t.cloneElement(s,{onExited:r.bind(null,s),in:u.props.in,exit:L(s,"exit",e),enter:L(s,"enter",e)})):o[a]=t.cloneElement(s,{in:!1}):o[a]=t.cloneElement(s,{onExited:r.bind(null,s),in:!0,exit:L(s,"exit",e),enter:L(s,"enter",e)})}}),o}P.defaultProps={classNames:""},P.propTypes={};var D=Object.values||function(e){return Object.keys(e).map(function(t){return e[t]})},R=function(e){function n(t,n){var r,i=(r=e.call(this,t,n)||this).handleExited.bind(_(_(r)));return r.state={contextValue:{isMounting:!0},handleExited:i,firstRender:!0},r}s(n,e);var i=n.prototype;return i.componentDidMount=function(){this.mounted=!0,this.setState({contextValue:{isMounting:!1}})},i.componentWillUnmount=function(){this.mounted=!1},n.getDerivedStateFromProps=function(e,n){var r,i,o=n.children,a=n.handleExited;return{children:n.firstRender?(r=e,i=a,w(r.children,function(e){return t.cloneElement(e,{onExited:i.bind(null,e),in:!0,appear:L(e,"appear",r),enter:L(e,"enter",r),exit:L(e,"exit",r)})})):M(e,o,a),firstRender:!1}},i.handleExited=function(e,t){var n=w(this.props.children);e.key in n||(e.props.onExited&&e.props.onExited(t),this.mounted&&this.setState(function(t){var n=o({},t.children);return delete n[e.key],{children:n}}))},i.render=function(){var e=this.props,t=e.component,n=e.childFactory,i=a(e,["component","childFactory"]),o=this.state.contextValue,s=D(this.state.children).map(n);return delete i.appear,delete i.enter,delete i.exit,null===t?r.createElement(b.Provider,{value:o},s):r.createElement(b.Provider,{value:o},r.createElement(t,i,s))},n}(r.Component);R.propTypes={},R.defaultProps={component:"div",childFactory:function(e){return e}};var A,V,I=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),i=0;i<n;i++)r[i]=arguments[i];return(t=e.call.apply(e,[this].concat(r))||this).handleEnter=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle("onEnter",0,n)},t.handleEntering=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle("onEntering",0,n)},t.handleEntered=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle("onEntered",0,n)},t.handleExit=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle("onExit",1,n)},t.handleExiting=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle("onExiting",1,n)},t.handleExited=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle("onExited",1,n)},t}s(t,e);var i=t.prototype;return i.handleLifecycle=function(e,t,i){var o,a=this.props.children,s=r.Children.toArray(a)[t];s.props[e]&&(o=s.props)[e].apply(o,i),this.props[e]&&this.props[e](n.findDOMNode(this))},i.render=function(){var e=this.props,t=e.children,n=e.in,i=a(e,["children","in"]),o=r.Children.toArray(t),s=o[0],l=o[1];return delete i.onEnter,delete i.onEntering,delete i.onEntered,delete i.onExit,delete i.onExiting,delete i.onExited,r.createElement(R,i,n?r.cloneElement(s,{key:"first",onEnter:this.handleEnter,onEntering:this.handleEntering,onEntered:this.handleEntered}):r.cloneElement(l,{key:"second",onEnter:this.handleExit,onEntering:this.handleExiting,onEntered:this.handleExited}))},t}(r.Component);I.propTypes={};var F="out-in",U="in-out",q=function(e,t,n){return function(){var r;e.props[t]&&(r=e.props)[t].apply(r,arguments),n()}},G=((A={})[F]=function(e){var t=e.current,n=e.changeState;return r.cloneElement(t,{in:!1,onExited:q(t,"onExited",function(){n(S,null)})})},A[U]=function(e){var t=e.current,n=e.changeState,i=e.children;return[t,r.cloneElement(i,{in:!0,onEntered:q(i,"onEntered",function(){n(S)})})]},A),W=((V={})[F]=function(e){var t=e.children,n=e.changeState;return r.cloneElement(t,{in:!0,onEntered:q(t,"onEntered",function(){n(N,r.cloneElement(t,{in:!0}))})})},V[U]=function(e){var t=e.current,n=e.children,i=e.changeState;return[r.cloneElement(t,{in:!1,onExited:q(t,"onExited",function(){i(N,r.cloneElement(n,{in:!0}))})}),r.cloneElement(n,{in:!0})]},V),$=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),i=0;i<n;i++)r[i]=arguments[i];return(t=e.call.apply(e,[this].concat(r))||this).state={status:N,current:null},t.appeared=!1,t.changeState=function(e,n){void 0===n&&(n=t.state.current),t.setState({status:e,current:n})},t}s(t,e);var n=t.prototype;return n.componentDidMount=function(){this.appeared=!0},t.getDerivedStateFromProps=function(e,t){return null==e.children?{current:null}:t.status===S&&e.mode===U?{status:S}:!t.current||(n=t.current,i=e.children,n===i||r.isValidElement(n)&&r.isValidElement(i)&&null!=n.key&&n.key===i.key)?{current:r.cloneElement(e.children,{in:!0})}:{status:"exiting"};var n,i},n.render=function(){var e,t=this.props,n=t.children,i=t.mode,o=this.state,a=o.status,s=o.current,l={children:n,current:s,changeState:this.changeState,status:a};switch(a){case S:e=W[i](l);break;case"exiting":e=G[i](l);break;case N:e=s}return r.createElement(b.Provider,{value:{isMounting:!this.appeared}},e)},t}(r.Component);$.propTypes={},$.defaultProps={mode:F},e.CSSTransition=P,e.ReplaceTransition=I,e.SwitchTransition=$,e.Transition=k,e.TransitionGroup=R,e.config=g,Object.defineProperty(e,"__esModule",{value:!0})});
--- a/browser/components/newtab/webpack.system-addon.config.js
+++ b/browser/components/newtab/webpack.system-addon.config.js
@@ -61,10 +61,11 @@ module.exports = (env = {}) => ({
     modules: ["node_modules", "."],
   },
   externals: {
     "prop-types": "PropTypes",
     react: "React",
     "react-dom": "ReactDOM",
     redux: "Redux",
     "react-redux": "ReactRedux",
+    "react-transition-group": "ReactTransitionGroup",
   },
 });
--- a/browser/locales/en-US/browser/newtab/newtab.ftl
+++ b/browser/locales/en-US/browser/newtab/newtab.ftl
@@ -108,16 +108,17 @@ newtab-menu-show-file =
 newtab-menu-open-file = Open File
 
 ## Card Labels: These labels are associated to pages to give
 ## context on how the element is related to the user, e.g. type indicates that
 ## the page is bookmarked, or is currently open on another device.
 
 newtab-label-visited = Visited
 newtab-label-bookmarked = Bookmarked
+newtab-label-removed-bookmark = Bookmark removed
 newtab-label-recommended = Trending
 newtab-label-saved = Saved to { -pocket-brand-name }
 newtab-label-download = Downloaded
 
 ## Section Menu: These strings are displayed in the section context menu and are
 ## meant as a call to action for the given section.
 
 newtab-section-menu-remove-section = Remove Section