Bug 1574334 - Add lazy cards, story engagements and bug fixes to New Tab Page r=pdahiya,fluent-reviewers,flod
authorEd Lee <edilee@mozilla.com>
Fri, 16 Aug 2019 05:54:16 +0000
changeset 488437 cf52300e04c94da713159e613f6bfaf9aa0a6da4
parent 488436 2213833fcec3e1b7444a29d1ed038f39a3141cf7
child 488438 0c769cff316532f09812f8315e97787e7cc67131
push id113908
push userccoroiu@mozilla.com
push dateFri, 16 Aug 2019 09:57:53 +0000
treeherdermozilla-inbound@83fad6abe38a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspdahiya, fluent-reviewers, flod
bugs1574334
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1574334 - Add lazy cards, story engagements and bug fixes to New Tab Page r=pdahiya,fluent-reviewers,flod Differential Revision: https://phabricator.services.mozilla.com/D42229
browser/components/newtab/content-src/asrouter/components/Button/_Button.scss
browser/components/newtab/content-src/asrouter/docs/user-actions.md
browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
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/components/DiscoveryStreamComponents/Hero/Hero.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/_Hero.scss
browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
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/lib/OnboardingMessageProvider.jsm
browser/components/newtab/lib/ToolbarBadgeHub.jsm
browser/components/newtab/lib/ToolbarPanelHub.jsm
browser/components/newtab/locales-src/asrouter.ftl
browser/components/newtab/locales-src/onboarding.ftl
browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx
browser/components/newtab/test/unit/asrouter/templates/OnboardingMessage.test.jsx
browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
browser/components/newtab/test/unit/asrouter/templates/Triplets.test.jsx
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
browser/locales/en-US/browser/newtab/asrouter.ftl
browser/locales/en-US/browser/newtab/onboarding.ftl
--- a/browser/components/newtab/content-src/asrouter/components/Button/_Button.scss
+++ b/browser/components/newtab/content-src/asrouter/components/Button/_Button.scss
@@ -33,16 +33,20 @@
 
     &:hover {
       background-color: $grey-90-20;
     }
 
     &:active {
       background-color: $grey-90-30;
     }
+
+    &:focus {
+      box-shadow: 0 0 0 1px $blue-50 inset, 0 0 0 1px $blue-50, 0 0 0 4px $blue-50-30;
+    }
   }
 }
 
 [lwt-newtab-brighttext] {
   .secondary {
     background-color: $grey-10-10;
 
     &:hover {
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/docs/user-actions.md
@@ -0,0 +1,141 @@
+# User Actions
+
+A subset of actions are available to messages via fields like `button_action` for snippets, or `primary_action` for CFRs.
+
+## Usage
+
+For snippets, you should add the action type in `button_action` and any additional parameters in `button_action_args. For example:
+
+```json
+{
+  "button_action": "OPEN_ABOUT_PAGE",
+  "button_action_args": "config"
+}
+```
+
+## Available Actions
+
+### `OPEN_APPLICATIONS_MENU`
+
+* args: (none)
+
+Opens the applications menu.
+
+### `OPEN_PRIVATE_BROWSER_WINDOW`
+
+* args: (none)
+
+Opens a new private browsing window.
+
+
+### `OPEN_URL`
+
+* args: `string` (a url)
+
+Opens a given url.
+
+Example:
+
+```json
+{
+  "button_action": "OPEN_URL",
+  "button_action_args": "https://foo.com"
+}
+```
+
+### `OPEN_ABOUT_PAGE`
+
+* args: `string` (a valid about page without the `about:` prefix)
+
+Opens a given about page
+
+Example:
+
+```json
+{
+  "button_action": "OPEN_ABOUT_PAGE",
+  "button_action_args": "config"
+}
+```
+
+### `OPEN_PREFERENCES_PAGE`
+
+* args: `string` (a category accessible via a `#`)
+
+Opens `about:preferences` with an optional category accessible via a `#` in the URL (e.g. `about:preferences#home`).
+
+Example:
+
+```json
+{
+  "button_action": "OPEN_PREFERENCES_PAGE",
+  "button_action_args": "home"
+}
+```
+
+### `SHOW_FIREFOX_ACCOUNTS`
+
+* args: (none)
+
+Opens Firefox accounts sign-up page. Encodes some information that the origin was from snippets by default.
+
+### `PIN_CURRENT_TAB`
+
+* args: (none)
+
+Pins the currently focused tab.
+
+### `ENABLE_FIREFOX_MONITOR`
+
+* args:
+```ts
+{
+  url: string;
+  flowRequestParams: {
+    entrypoint: string;
+    utm_term: string;
+    form_type: string;
+  }
+}
+```
+
+Opens an oauth flow to enable Firefox Monitor at a given `url` and adds Firefox metrics that user given a set of `flowRequestParams`.
+
+### `url`
+
+The URL should start with `https://monitor.firefox.com/oauth/init` and add various metrics tags as search params, including:
+
+* `utm_source`
+* `utm_campaign`
+* `form_type`
+* `entrypoint`
+
+You should verify the values of these search params with whoever is doing the data analysis (e.g. Leif Oines).
+
+### `flowRequestParams`
+
+These params are used by Firefox to add information specific to that individual user to the final oauth URL. You should include:
+
+* `entrypoint`
+* `utm_term`
+* `form_type`
+
+The `entrypoint` and `form_type` values should match the encoded values in your `url`.
+
+You should verify the values with whoever is doing the data analysis (e.g. Leif Oines).
+
+### Example
+
+```json
+{
+  "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"
+      }
+  }
+}
+```
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
@@ -24,16 +24,17 @@
       min-height: 60px;
       border-radius: 4px;
 
       &:hover {
         background-color: var(--newtab-element-hover-color);
 
         .blockButton {
           display: block;
+          opacity: 1;
 
           // larger inset if discovery stream is enabled.
           .ds-outer-wrapper-breakpoint-override & {
             inset-inline-end: -8%;
 
             @media (max-width: 865px) {
               inset-inline-end: 2%;
             }
@@ -100,16 +101,21 @@
     }
   }
 
   .blockButton {
     display: block;
     inset-inline-end: 20px;
     opacity: 1;
     top: 50%;
+
+    &:focus {
+      box-shadow: 0 0 0 1px $blue-50 inset, 0 0 0 1px $blue-50, 0 0 0 4px $blue-50-30;
+      border-radius: 2px;
+    }
   }
 
   .title {
     font-size: inherit;
     margin: 0;
   }
 
   .title-inline {
@@ -144,22 +150,27 @@
 
   &.withButton {
     line-height: 20px;
     margin-bottom: 10px;
     min-height: 60px;
     background-color: transparent;
 
     .blockButton {
-      display: none;
+      display: block;
       inset-inline-end: -15%;
-      opacity: 1;
+      opacity: 0;
       margin: auto;
       top: unset;
 
+      &:focus {
+        opacity: 1;
+        box-shadow: none;
+      }
+
       @media (max-width: 1120px) {
         inset-inline-end: 2%;
       }
 
       @media (max-width: 865px) {
         margin-top: 10px;
       }
 
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
@@ -31,16 +31,17 @@ export class CardGrid extends React.Pure
             type={this.props.type}
             context={rec.context}
             sponsor={rec.sponsor}
             dispatch={this.props.dispatch}
             source={rec.domain}
             pocket_id={rec.pocket_id}
             context_type={rec.context_type}
             bookmarkGuid={rec.bookmarkGuid}
+            engagement={rec.engagement}
             cta={rec.cta}
             cta_variant={this.props.cta_variant}
           />
         )
       );
     }
 
     let divisibility = ``;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
@@ -12,62 +12,79 @@ import { DSContextFooter } from "../DSCo
 
 export const DefaultMeta = ({
   source,
   title,
   excerpt,
   context,
   context_type,
   cta,
+  engagement,
 }) => (
   <div className="meta">
     <div className="info-wrap">
       <p className="source clamp">{source}</p>
       <header className="title clamp">{title}</header>
       {excerpt && <p className="excerpt clamp">{excerpt}</p>}
       {cta && (
         <div role="link" className="cta-link icon icon-arrow" tabIndex="0">
           {cta}
         </div>
       )}
     </div>
-    <DSContextFooter context_type={context_type} context={context} />
+    <DSContextFooter
+      context_type={context_type}
+      context={context}
+      engagement={engagement}
+    />
   </div>
 );
 
 export const VariantMeta = ({
   source,
   title,
   excerpt,
   context,
   context_type,
   cta,
+  engagement,
   sponsor,
 }) => (
   <div className="meta">
     <div className="info-wrap">
       <p className="source clamp">
         {sponsor ? sponsor : source}
         {context && ` · Sponsored`}
       </p>
       <header className="title clamp">{title}</header>
       {excerpt && <p className="excerpt clamp">{excerpt}</p>}
     </div>
-    {cta && <button className="button cta-button">{cta}</button>}
+    {context && cta && <button className="button cta-button">{cta}</button>}
     {!context && (
-      <DSContextFooter context_type={context_type} context={context} />
+      <DSContextFooter
+        context_type={context_type}
+        context={context}
+        engagement={engagement}
+      />
     )}
   </div>
 );
 
 export class DSCard extends React.PureComponent {
   constructor(props) {
     super(props);
 
     this.onLinkClick = this.onLinkClick.bind(this);
+    this.setPlaceholderRef = element => {
+      this.placholderElement = element;
+    };
+
+    this.state = {
+      isSeen: false,
+    };
   }
 
   onLinkClick(event) {
     if (this.props.dispatch) {
       this.props.dispatch(
         ac.UserEvent({
           event: "CLICK",
           source: this.props.type.toUpperCase(),
@@ -88,19 +105,55 @@ export class DSCard extends React.PureCo
                 : {}),
             },
           ],
         })
       );
     }
   }
 
+  onSeen(entries) {
+    if (this.state) {
+      const entry = entries.find(e => e.isIntersecting);
+
+      if (entry) {
+        if (this.placholderElement) {
+          this.observer.unobserve(this.placholderElement);
+        }
+
+        // Stop observing since element has been seen
+        this.setState({
+          isSeen: true,
+        });
+      }
+    }
+  }
+
+  componentDidMount() {
+    if (this.placholderElement) {
+      this.observer = new IntersectionObserver(this.onSeen.bind(this));
+      this.observer.observe(this.placholderElement);
+    }
+  }
+
+  componentWillUnmount() {
+    // Remove observer on unmount
+    if (this.observer && this.placholderElement) {
+      this.observer.unobserve(this.placholderElement);
+    }
+  }
+
   render() {
+    if (this.props.placeholder || !this.state.isSeen) {
+      return (
+        <div className="ds-card placeholder" ref={this.setPlaceholderRef} />
+      );
+    }
     return (
-      <div className={`ds-card${this.props.placeholder ? " placeholder" : ""}`}>
+      <div className="ds-card">
         <SafeAnchor
           className="ds-card-link"
           dispatch={this.props.dispatch}
           onLinkClick={!this.props.placeholder ? this.onLinkClick : undefined}
           url={this.props.url}
         >
           <div className="img-wrapper">
             <DSImage
@@ -111,26 +164,28 @@ export class DSCard extends React.PureCo
           </div>
           {this.props.cta_variant && (
             <VariantMeta
               source={this.props.source}
               title={this.props.title}
               excerpt={this.props.excerpt}
               context={this.props.context}
               context_type={this.props.context_type}
+              engagement={this.props.engagement}
               cta={this.props.cta}
               sponsor={this.props.sponsor}
             />
           )}
           {!this.props.cta_variant && (
             <DefaultMeta
               source={this.props.source}
               title={this.props.title}
               excerpt={this.props.excerpt}
               context={this.props.context}
+              engagement={this.props.engagement}
               context_type={this.props.context_type}
               cta={this.props.cta}
             />
           )}
           <ImpressionStats
             campaignId={this.props.campaignId}
             rows={[
               {
@@ -140,27 +195,25 @@ export class DSCard extends React.PureCo
                   ? { shim: this.props.shim.impression }
                   : {}),
               },
             ]}
             dispatch={this.props.dispatch}
             source={this.props.type}
           />
         </SafeAnchor>
-        {!this.props.placeholder && (
-          <DSLinkMenu
-            id={this.props.id}
-            index={this.props.pos}
-            dispatch={this.props.dispatch}
-            url={this.props.url}
-            title={this.props.title}
-            source={this.props.source}
-            type={this.props.type}
-            pocket_id={this.props.pocket_id}
-            shim={this.props.shim}
-            bookmarkGuid={this.props.bookmarkGuid}
-          />
-        )}
+        <DSLinkMenu
+          id={this.props.id}
+          index={this.props.pos}
+          dispatch={this.props.dispatch}
+          url={this.props.url}
+          title={this.props.title}
+          source={this.props.source}
+          type={this.props.type}
+          pocket_id={this.props.pocket_id}
+          shim={this.props.shim}
+          bookmarkGuid={this.props.bookmarkGuid}
+        />
       </div>
     );
   }
 }
 export const PlaceholderDSCard = props => <DSCard placeholder={true} />;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
@@ -8,24 +8,17 @@
   display: flex;
   flex-direction: column;
   position: relative;
 
   &.placeholder {
     background: transparent;
     box-shadow: inset $inner-box-shadow;
     border-radius: 4px;
-
-    .ds-card-link {
-      cursor: default;
-    }
-
-    .img-wrapper {
-      opacity: 0;
-    }
+    min-height: 300px;
   }
 
   .img-wrapper {
     width: 100%;
   }
 
   .img {
     height: 0;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx
@@ -16,29 +16,33 @@ export const StatusMessage = ({ icon, fl
       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 { context, context_type, engagement } = this.props;
     const { icon, fluentID } = cardContextTypes[context_type] || {};
 
     return (
       <div className="story-footer">
         {context && <p className="story-sponsored-label clamp">{context}</p>}
         <TransitionGroup component={null}>
-          {!context && context_type && (
+          {!context && (context_type || engagement) && (
             <CSSTransition
               key={fluentID}
               timeout={ANIMATION_DURATION}
               classNames="story-animate"
             >
-              <StatusMessage icon={icon} fluentID={fluentID} />
+              {engagement && !context_type ? (
+                <div className="story-view-count">{engagement}</div>
+              ) : (
+                <StatusMessage icon={icon} fluentID={fluentID} />
+              )}
             </CSSTransition>
           )}
         </TransitionGroup>
       </div>
     );
   }
 }
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss
@@ -3,16 +3,17 @@
 
 .story-footer {
   color: var(--newtab-text-secondary-color);
   inset-inline-start: 0;
   margin-top: 12px;
   position: relative;
 
   .story-sponsored-label,
+  .story-view-count,
   .status-message {
     @include dark-theme-only {
       color: $grey-40;
     }
 
     -webkit-line-clamp: 1;
     font-size: 13px;
     line-height: 24px;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
@@ -73,16 +73,17 @@ export class Hero extends React.PureComp
             pos={rec.pos}
             type={this.props.type}
             dispatch={this.props.dispatch}
             context={rec.context}
             context_type={rec.context_type}
             source={rec.domain}
             pocket_id={rec.pocket_id}
             bookmarkGuid={rec.bookmarkGuid}
+            engagement={rec.engagement}
           />
         )
       );
     }
 
     let heroCard = null;
 
     if (!heroRec || heroRec.placeholder) {
@@ -107,16 +108,17 @@ export class Hero extends React.PureComp
               <div className="header-and-excerpt">
                 <p className="source clamp">{heroRec.domain}</p>
                 <header className="clamp">{heroRec.title}</header>
                 <p className="excerpt clamp">{heroRec.excerpt}</p>
               </div>
               <DSContextFooter
                 context={heroRec.context}
                 context_type={heroRec.context_type}
+                engagement={heroRec.engagement}
               />
             </div>
             <ImpressionStats
               campaignId={heroRec.campaign_id}
               rows={[
                 {
                   id: heroRec.id,
                   pos: heroRec.pos,
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/_Hero.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/_Hero.scss
@@ -40,16 +40,17 @@
     .img-wrapper {
       margin: 0 0 12px;
     }
   }
 
   .ds-card.placeholder {
     margin-bottom: 20px;
     padding-bottom: 20px;
+    min-height: 180px;
   }
 
   .img-wrapper {
     margin: 0 0 12px;
   }
 
   .ds-hero-item {
     position: relative;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
@@ -76,16 +76,17 @@ export class ListItem extends React.Pure
                 <div className="ds-list-item-excerpt clamp">
                   {this.props.excerpt}
                 </div>
               )}
             </div>
             <DSContextFooter
               context={this.props.context}
               context_type={this.props.context_type}
+              engagement={this.props.engagement}
             />
           </div>
           <DSImage
             extraClassNames="ds-list-image"
             source={this.props.image_src}
             rawSource={this.props.raw_image_src}
           />
           <ImpressionStats
@@ -154,16 +155,17 @@ export function _List(props) {
             pos={rec.pos}
             title={rec.title}
             context={rec.context}
             context_type={rec.context_type}
             type={props.type}
             url={rec.url}
             pocket_id={rec.pocket_id}
             bookmarkGuid={rec.bookmarkGuid}
+            engagement={rec.engagement}
           />
         )
       );
     }
 
     const listStyles = [
       "ds-list",
       props.fullWidth ? "ds-list-full-width" : "",
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -1986,17 +1986,18 @@ main {
       box-shadow: none;
       border-radius: 0; }
     .ds-hero .ds-card:not(.placeholder) .meta {
       padding: 0; }
     .ds-hero .ds-card:not(.placeholder) .img-wrapper {
       margin: 0 0 12px; }
   .ds-hero .ds-card.placeholder {
     margin-bottom: 20px;
-    padding-bottom: 20px; }
+    padding-bottom: 20px;
+    min-height: 180px; }
   .ds-hero .img-wrapper {
     margin: 0 0 12px; }
   .ds-hero .ds-hero-item {
     position: relative; }
   .ds-hero .wrapper {
     border-top: 1px solid #D7D7DB;
     color: #737373;
     display: block;
@@ -2630,21 +2631,18 @@ main {
 
 .ds-card {
   display: flex;
   flex-direction: column;
   position: relative; }
   .ds-card.placeholder {
     background: transparent;
     box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color);
-    border-radius: 4px; }
-    .ds-card.placeholder .ds-card-link {
-      cursor: default; }
-    .ds-card.placeholder .img-wrapper {
-      opacity: 0; }
+    border-radius: 4px;
+    min-height: 300px; }
   .ds-card .img-wrapper {
     width: 100%; }
   .ds-card .img {
     height: 0;
     padding-top: 50%; }
     .ds-card .img img {
       border-radius: 4px;
       box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.15); }
@@ -2778,22 +2776,24 @@ main {
       color: #F9F9FA; }
 
 .story-footer {
   color: var(--newtab-text-secondary-color);
   inset-inline-start: 0;
   margin-top: 12px;
   position: relative; }
   .story-footer .story-sponsored-label,
+  .story-footer .story-view-count,
   .story-footer .status-message {
     -webkit-line-clamp: 1;
     font-size: 13px;
     line-height: 24px;
     color: #737373; }
     [lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
+    .story-footer .story-view-count, [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;
@@ -2998,16 +2998,18 @@ main {
     .ASRouterButton.primary:active {
       background-color: #002275; }
   .ASRouterButton.secondary {
     background-color: rgba(12, 12, 13, 0.1); }
     .ASRouterButton.secondary:hover {
       background-color: rgba(12, 12, 13, 0.2); }
     .ASRouterButton.secondary:active {
       background-color: rgba(12, 12, 13, 0.3); }
+    .ASRouterButton.secondary:focus {
+      box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3); }
 
 [lwt-newtab-brighttext] .secondary {
   background-color: rgba(249, 249, 250, 0.1); }
   [lwt-newtab-brighttext] .secondary:hover {
     background-color: rgba(249, 249, 250, 0.2); }
   [lwt-newtab-brighttext] .secondary:active {
     background-color: rgba(249, 249, 250, 0.3); }
 
@@ -3308,17 +3310,18 @@ body[lwt-newtab-brighttext] .scene2Icon 
         .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .buttonContainer {
           margin: auto; } }
     .below-search-snippet.withButton .snippet-hover-wrapper {
       min-height: 60px;
       border-radius: 4px; }
       .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; }
+          display: block;
+          opacity: 1; }
           .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
             inset-inline-end: -8%; }
             @media (max-width: 865px) {
               .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
                 inset-inline-end: 2%; } }
 
 .SimpleBelowSearchSnippet {
   background-color: transparent;
@@ -3366,16 +3369,19 @@ body[lwt-newtab-brighttext] .scene2Icon 
       padding: 0;
       text-align: inherit;
       margin: auto; }
   .SimpleBelowSearchSnippet .blockButton {
     display: block;
     inset-inline-end: 20px;
     opacity: 1;
     top: 50%; }
+    .SimpleBelowSearchSnippet .blockButton:focus {
+      box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3);
+      border-radius: 2px; }
   .SimpleBelowSearchSnippet .title {
     font-size: inherit;
     margin: 0; }
   .SimpleBelowSearchSnippet .title-inline {
     display: inline; }
   .SimpleBelowSearchSnippet .textContainer {
     margin: 10px;
     margin-inline-start: 0; }
@@ -3392,21 +3398,24 @@ body[lwt-newtab-brighttext] .scene2Icon 
       height: 24px;
       width: 24px; }
   .SimpleBelowSearchSnippet.withButton {
     line-height: 20px;
     margin-bottom: 10px;
     min-height: 60px;
     background-color: transparent; }
     .SimpleBelowSearchSnippet.withButton .blockButton {
-      display: none;
+      display: block;
       inset-inline-end: -15%;
-      opacity: 1;
+      opacity: 0;
       margin: auto;
       top: unset; }
+      .SimpleBelowSearchSnippet.withButton .blockButton:focus {
+        opacity: 1;
+        box-shadow: none; }
       @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%;
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -1989,17 +1989,18 @@ main {
       box-shadow: none;
       border-radius: 0; }
     .ds-hero .ds-card:not(.placeholder) .meta {
       padding: 0; }
     .ds-hero .ds-card:not(.placeholder) .img-wrapper {
       margin: 0 0 12px; }
   .ds-hero .ds-card.placeholder {
     margin-bottom: 20px;
-    padding-bottom: 20px; }
+    padding-bottom: 20px;
+    min-height: 180px; }
   .ds-hero .img-wrapper {
     margin: 0 0 12px; }
   .ds-hero .ds-hero-item {
     position: relative; }
   .ds-hero .wrapper {
     border-top: 1px solid #D7D7DB;
     color: #737373;
     display: block;
@@ -2633,21 +2634,18 @@ main {
 
 .ds-card {
   display: flex;
   flex-direction: column;
   position: relative; }
   .ds-card.placeholder {
     background: transparent;
     box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color);
-    border-radius: 4px; }
-    .ds-card.placeholder .ds-card-link {
-      cursor: default; }
-    .ds-card.placeholder .img-wrapper {
-      opacity: 0; }
+    border-radius: 4px;
+    min-height: 300px; }
   .ds-card .img-wrapper {
     width: 100%; }
   .ds-card .img {
     height: 0;
     padding-top: 50%; }
     .ds-card .img img {
       border-radius: 4px;
       box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.15); }
@@ -2781,22 +2779,24 @@ main {
       color: #F9F9FA; }
 
 .story-footer {
   color: var(--newtab-text-secondary-color);
   inset-inline-start: 0;
   margin-top: 12px;
   position: relative; }
   .story-footer .story-sponsored-label,
+  .story-footer .story-view-count,
   .story-footer .status-message {
     -webkit-line-clamp: 1;
     font-size: 13px;
     line-height: 24px;
     color: #737373; }
     [lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
+    .story-footer .story-view-count, [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;
@@ -3001,16 +3001,18 @@ main {
     .ASRouterButton.primary:active {
       background-color: #002275; }
   .ASRouterButton.secondary {
     background-color: rgba(12, 12, 13, 0.1); }
     .ASRouterButton.secondary:hover {
       background-color: rgba(12, 12, 13, 0.2); }
     .ASRouterButton.secondary:active {
       background-color: rgba(12, 12, 13, 0.3); }
+    .ASRouterButton.secondary:focus {
+      box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3); }
 
 [lwt-newtab-brighttext] .secondary {
   background-color: rgba(249, 249, 250, 0.1); }
   [lwt-newtab-brighttext] .secondary:hover {
     background-color: rgba(249, 249, 250, 0.2); }
   [lwt-newtab-brighttext] .secondary:active {
     background-color: rgba(249, 249, 250, 0.3); }
 
@@ -3311,17 +3313,18 @@ body[lwt-newtab-brighttext] .scene2Icon 
         .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .buttonContainer {
           margin: auto; } }
     .below-search-snippet.withButton .snippet-hover-wrapper {
       min-height: 60px;
       border-radius: 4px; }
       .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; }
+          display: block;
+          opacity: 1; }
           .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
             inset-inline-end: -8%; }
             @media (max-width: 865px) {
               .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
                 inset-inline-end: 2%; } }
 
 .SimpleBelowSearchSnippet {
   background-color: transparent;
@@ -3369,16 +3372,19 @@ body[lwt-newtab-brighttext] .scene2Icon 
       padding: 0;
       text-align: inherit;
       margin: auto; }
   .SimpleBelowSearchSnippet .blockButton {
     display: block;
     inset-inline-end: 20px;
     opacity: 1;
     top: 50%; }
+    .SimpleBelowSearchSnippet .blockButton:focus {
+      box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3);
+      border-radius: 2px; }
   .SimpleBelowSearchSnippet .title {
     font-size: inherit;
     margin: 0; }
   .SimpleBelowSearchSnippet .title-inline {
     display: inline; }
   .SimpleBelowSearchSnippet .textContainer {
     margin: 10px;
     margin-inline-start: 0; }
@@ -3395,21 +3401,24 @@ body[lwt-newtab-brighttext] .scene2Icon 
       height: 24px;
       width: 24px; }
   .SimpleBelowSearchSnippet.withButton {
     line-height: 20px;
     margin-bottom: 10px;
     min-height: 60px;
     background-color: transparent; }
     .SimpleBelowSearchSnippet.withButton .blockButton {
-      display: none;
+      display: block;
       inset-inline-end: -15%;
-      opacity: 1;
+      opacity: 0;
       margin: auto;
       top: unset; }
+      .SimpleBelowSearchSnippet.withButton .blockButton:focus {
+        opacity: 1;
+        box-shadow: none; }
       @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%;
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -1986,17 +1986,18 @@ main {
       box-shadow: none;
       border-radius: 0; }
     .ds-hero .ds-card:not(.placeholder) .meta {
       padding: 0; }
     .ds-hero .ds-card:not(.placeholder) .img-wrapper {
       margin: 0 0 12px; }
   .ds-hero .ds-card.placeholder {
     margin-bottom: 20px;
-    padding-bottom: 20px; }
+    padding-bottom: 20px;
+    min-height: 180px; }
   .ds-hero .img-wrapper {
     margin: 0 0 12px; }
   .ds-hero .ds-hero-item {
     position: relative; }
   .ds-hero .wrapper {
     border-top: 1px solid #D7D7DB;
     color: #737373;
     display: block;
@@ -2630,21 +2631,18 @@ main {
 
 .ds-card {
   display: flex;
   flex-direction: column;
   position: relative; }
   .ds-card.placeholder {
     background: transparent;
     box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color);
-    border-radius: 4px; }
-    .ds-card.placeholder .ds-card-link {
-      cursor: default; }
-    .ds-card.placeholder .img-wrapper {
-      opacity: 0; }
+    border-radius: 4px;
+    min-height: 300px; }
   .ds-card .img-wrapper {
     width: 100%; }
   .ds-card .img {
     height: 0;
     padding-top: 50%; }
     .ds-card .img img {
       border-radius: 4px;
       box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.15); }
@@ -2778,22 +2776,24 @@ main {
       color: #F9F9FA; }
 
 .story-footer {
   color: var(--newtab-text-secondary-color);
   inset-inline-start: 0;
   margin-top: 12px;
   position: relative; }
   .story-footer .story-sponsored-label,
+  .story-footer .story-view-count,
   .story-footer .status-message {
     -webkit-line-clamp: 1;
     font-size: 13px;
     line-height: 24px;
     color: #737373; }
     [lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
+    .story-footer .story-view-count, [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;
@@ -2998,16 +2998,18 @@ main {
     .ASRouterButton.primary:active {
       background-color: #002275; }
   .ASRouterButton.secondary {
     background-color: rgba(12, 12, 13, 0.1); }
     .ASRouterButton.secondary:hover {
       background-color: rgba(12, 12, 13, 0.2); }
     .ASRouterButton.secondary:active {
       background-color: rgba(12, 12, 13, 0.3); }
+    .ASRouterButton.secondary:focus {
+      box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3); }
 
 [lwt-newtab-brighttext] .secondary {
   background-color: rgba(249, 249, 250, 0.1); }
   [lwt-newtab-brighttext] .secondary:hover {
     background-color: rgba(249, 249, 250, 0.2); }
   [lwt-newtab-brighttext] .secondary:active {
     background-color: rgba(249, 249, 250, 0.3); }
 
@@ -3308,17 +3310,18 @@ body[lwt-newtab-brighttext] .scene2Icon 
         .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .buttonContainer {
           margin: auto; } }
     .below-search-snippet.withButton .snippet-hover-wrapper {
       min-height: 60px;
       border-radius: 4px; }
       .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; }
+          display: block;
+          opacity: 1; }
           .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
             inset-inline-end: -8%; }
             @media (max-width: 865px) {
               .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
                 inset-inline-end: 2%; } }
 
 .SimpleBelowSearchSnippet {
   background-color: transparent;
@@ -3366,16 +3369,19 @@ body[lwt-newtab-brighttext] .scene2Icon 
       padding: 0;
       text-align: inherit;
       margin: auto; }
   .SimpleBelowSearchSnippet .blockButton {
     display: block;
     inset-inline-end: 20px;
     opacity: 1;
     top: 50%; }
+    .SimpleBelowSearchSnippet .blockButton:focus {
+      box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3);
+      border-radius: 2px; }
   .SimpleBelowSearchSnippet .title {
     font-size: inherit;
     margin: 0; }
   .SimpleBelowSearchSnippet .title-inline {
     display: inline; }
   .SimpleBelowSearchSnippet .textContainer {
     margin: 10px;
     margin-inline-start: 0; }
@@ -3392,21 +3398,24 @@ body[lwt-newtab-brighttext] .scene2Icon 
       height: 24px;
       width: 24px; }
   .SimpleBelowSearchSnippet.withButton {
     line-height: 20px;
     margin-bottom: 10px;
     min-height: 60px;
     background-color: transparent; }
     .SimpleBelowSearchSnippet.withButton .blockButton {
-      display: none;
+      display: block;
       inset-inline-end: -15%;
-      opacity: 1;
+      opacity: 0;
       margin: auto;
       top: unset; }
+      .SimpleBelowSearchSnippet.withButton .blockButton:focus {
+        opacity: 1;
+        box-shadow: none; }
       @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%;
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -7890,33 +7890,36 @@ const StatusMessage = ({
 }), 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
+      context_type,
+      engagement
     } = 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 clamp"
     }, context), external_React_default.a.createElement(external_ReactTransitionGroup_["TransitionGroup"], {
       component: null
-    }, !context && context_type && external_React_default.a.createElement(external_ReactTransitionGroup_["CSSTransition"], {
+    }, !context && (context_type || engagement) && external_React_default.a.createElement(external_ReactTransitionGroup_["CSSTransition"], {
       key: fluentID,
       timeout: ANIMATION_DURATION,
       classNames: "story-animate"
-    }, external_React_default.a.createElement(StatusMessage, {
+    }, engagement && !context_type ? external_React_default.a.createElement("div", {
+      className: "story-view-count"
+    }, engagement) : 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
@@ -7930,63 +7933,75 @@ class DSContextFooter_DSContextFooter ex
 
 
 const DefaultMeta = ({
   source,
   title,
   excerpt,
   context,
   context_type,
-  cta
+  cta,
+  engagement
 }) => external_React_default.a.createElement("div", {
   className: "meta"
 }, external_React_default.a.createElement("div", {
   className: "info-wrap"
 }, external_React_default.a.createElement("p", {
   className: "source clamp"
 }, source), external_React_default.a.createElement("header", {
   className: "title clamp"
 }, title), excerpt && external_React_default.a.createElement("p", {
   className: "excerpt clamp"
 }, excerpt), cta && external_React_default.a.createElement("div", {
   role: "link",
   className: "cta-link icon icon-arrow",
   tabIndex: "0"
 }, cta)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
   context_type: context_type,
-  context: context
+  context: context,
+  engagement: engagement
 }));
 const VariantMeta = ({
   source,
   title,
   excerpt,
   context,
   context_type,
   cta,
+  engagement,
   sponsor
 }) => external_React_default.a.createElement("div", {
   className: "meta"
 }, external_React_default.a.createElement("div", {
   className: "info-wrap"
 }, external_React_default.a.createElement("p", {
   className: "source clamp"
 }, sponsor ? sponsor : source, context && ` · Sponsored`), external_React_default.a.createElement("header", {
   className: "title clamp"
 }, title), excerpt && external_React_default.a.createElement("p", {
   className: "excerpt clamp"
-}, excerpt)), cta && external_React_default.a.createElement("button", {
+}, excerpt)), context && cta && external_React_default.a.createElement("button", {
   className: "button cta-button"
 }, cta), !context && external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
   context_type: context_type,
-  context: context
+  context: context,
+  engagement: engagement
 }));
 class DSCard_DSCard extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onLinkClick = this.onLinkClick.bind(this);
+
+    this.setPlaceholderRef = element => {
+      this.placholderElement = element;
+    };
+
+    this.state = {
+      isSeen: false
+    };
   }
 
   onLinkClick(event) {
     if (this.props.dispatch) {
       this.props.dispatch(Actions["actionCreators"].UserEvent({
         event: "CLICK",
         source: this.props.type.toUpperCase(),
         action_position: this.props.pos
@@ -8000,19 +8015,57 @@ class DSCard_DSCard extends external_Rea
           ...(this.props.shim && this.props.shim.click ? {
             shim: this.props.shim.click
           } : {})
         }]
       }));
     }
   }
 
-  render() {
+  onSeen(entries) {
+    if (this.state) {
+      const entry = entries.find(e => e.isIntersecting);
+
+      if (entry) {
+        if (this.placholderElement) {
+          this.observer.unobserve(this.placholderElement);
+        } // Stop observing since element has been seen
+
+
+        this.setState({
+          isSeen: true
+        });
+      }
+    }
+  }
+
+  componentDidMount() {
+    if (this.placholderElement) {
+      this.observer = new IntersectionObserver(this.onSeen.bind(this));
+      this.observer.observe(this.placholderElement);
+    }
+  }
+
+  componentWillUnmount() {
+    // Remove observer on unmount
+    if (this.observer && this.placholderElement) {
+      this.observer.unobserve(this.placholderElement);
+    }
+  }
+
+  render() {
+    if (this.props.placeholder || !this.state.isSeen) {
+      return external_React_default.a.createElement("div", {
+        className: "ds-card placeholder",
+        ref: this.setPlaceholderRef
+      });
+    }
+
     return external_React_default.a.createElement("div", {
-      className: `ds-card${this.props.placeholder ? " placeholder" : ""}`
+      className: "ds-card"
     }, external_React_default.a.createElement(SafeAnchor_SafeAnchor, {
       className: "ds-card-link",
       dispatch: this.props.dispatch,
       onLinkClick: !this.props.placeholder ? this.onLinkClick : undefined,
       url: this.props.url
     }, external_React_default.a.createElement("div", {
       className: "img-wrapper"
     }, external_React_default.a.createElement(DSImage_DSImage, {
@@ -8020,37 +8073,39 @@ class DSCard_DSCard extends external_Rea
       source: this.props.image_src,
       rawSource: this.props.raw_image_src
     })), this.props.cta_variant && external_React_default.a.createElement(VariantMeta, {
       source: this.props.source,
       title: this.props.title,
       excerpt: this.props.excerpt,
       context: this.props.context,
       context_type: this.props.context_type,
+      engagement: this.props.engagement,
       cta: this.props.cta,
       sponsor: this.props.sponsor
     }), !this.props.cta_variant && external_React_default.a.createElement(DefaultMeta, {
       source: this.props.source,
       title: this.props.title,
       excerpt: this.props.excerpt,
       context: this.props.context,
+      engagement: this.props.engagement,
       context_type: this.props.context_type,
       cta: this.props.cta
     }), 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
         } : {})
       }],
       dispatch: this.props.dispatch,
       source: this.props.type
-    })), !this.props.placeholder && external_React_default.a.createElement(DSLinkMenu_DSLinkMenu, {
+    })), external_React_default.a.createElement(DSLinkMenu_DSLinkMenu, {
       id: this.props.id,
       index: this.props.pos,
       dispatch: this.props.dispatch,
       url: this.props.url,
       title: this.props.title,
       source: this.props.source,
       type: this.props.type,
       pocket_id: this.props.pocket_id,
@@ -8187,16 +8242,17 @@ class CardGrid_CardGrid extends external
         type: this.props.type,
         context: rec.context,
         sponsor: rec.sponsor,
         dispatch: this.props.dispatch,
         source: rec.domain,
         pocket_id: rec.pocket_id,
         context_type: rec.context_type,
         bookmarkGuid: rec.bookmarkGuid,
+        engagement: rec.engagement,
         cta: rec.cta,
         cta_variant: this.props.cta_variant
       }));
     }
 
     let divisibility = ``;
 
     if (this.props.items % 4 === 0) {
@@ -8331,17 +8387,18 @@ class List_ListItem extends external_Rea
     }, this.props.domain)), external_React_default.a.createElement("div", {
       className: "ds-list-item-body"
     }, external_React_default.a.createElement("div", {
       className: "ds-list-item-title clamp"
     }, this.props.title), this.props.excerpt && external_React_default.a.createElement("div", {
       className: "ds-list-item-excerpt clamp"
     }, this.props.excerpt)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
       context: this.props.context,
-      context_type: this.props.context_type
+      context_type: this.props.context_type,
+      engagement: this.props.engagement
     })), external_React_default.a.createElement(DSImage_DSImage, {
       extraClassNames: "ds-list-image",
       source: this.props.image_src,
       rawSource: this.props.raw_image_src
     }), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
       campaignId: this.props.campaignId,
       rows: [{
         id: this.props.id,
@@ -8395,17 +8452,18 @@ function _List(props) {
         raw_image_src: rec.raw_image_src,
         pos: rec.pos,
         title: rec.title,
         context: rec.context,
         context_type: rec.context_type,
         type: props.type,
         url: rec.url,
         pocket_id: rec.pocket_id,
-        bookmarkGuid: rec.bookmarkGuid
+        bookmarkGuid: rec.bookmarkGuid,
+        engagement: rec.engagement
       }));
     }
 
     const listStyles = ["ds-list", props.fullWidth ? "ds-list-full-width" : "", props.hasBorders ? "ds-list-borders" : "", props.hasImages ? "ds-list-images" : "", props.hasNumbers ? "ds-list-numbers" : ""];
     return external_React_default.a.createElement("ul", {
       className: listStyles.join(" ")
     }, recMarkup);
   };
@@ -8508,17 +8566,18 @@ class Hero_Hero extends external_React_d
         shim: rec.shim,
         pos: rec.pos,
         type: this.props.type,
         dispatch: this.props.dispatch,
         context: rec.context,
         context_type: rec.context_type,
         source: rec.domain,
         pocket_id: rec.pocket_id,
-        bookmarkGuid: rec.bookmarkGuid
+        bookmarkGuid: rec.bookmarkGuid,
+        engagement: rec.engagement
       }));
     }
 
     let heroCard = null;
 
     if (!heroRec || heroRec.placeholder) {
       heroCard = external_React_default.a.createElement(PlaceholderDSCard, null);
     } else {
@@ -8543,17 +8602,18 @@ class Hero_Hero extends external_React_d
       }, external_React_default.a.createElement("p", {
         className: "source clamp"
       }, heroRec.domain), external_React_default.a.createElement("header", {
         className: "clamp"
       }, heroRec.title), external_React_default.a.createElement("p", {
         className: "excerpt clamp"
       }, heroRec.excerpt)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
         context: heroRec.context,
-        context_type: heroRec.context_type
+        context_type: heroRec.context_type,
+        engagement: heroRec.engagement
       })), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
         campaignId: heroRec.campaign_id,
         rows: [{
           id: heroRec.id,
           pos: heroRec.pos,
           ...(heroRec.shim && heroRec.shim.impression ? {
             shim: heroRec.shim.impression
           } : {})
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -394,20 +394,19 @@ const ONBOARDING_MESSAGES = () => [
     // Never accessed the FxA panel && doesn't use Firefox sync & has FxA enabled
     targeting: `isFxABadgeEnabled && !hasAccessedFxAPanel && !usesFirefoxSync && isFxAEnabled == true`,
     trigger: { id: "toolbarBadgeUpdate" },
   },
   {
     id: "PROTECTIONS_PANEL_1",
     template: "protections_panel",
     content: {
-      title: "Browse without being followed",
-      body:
-        "Keep your data to yourself. Firefox protects you from many of the most common trackers that follow what you do online.",
-      link_text: "Learn more",
+      title: { string_id: "cfr-protections-panel-header" },
+      body: { string_id: "cfr-protections-panel-body" },
+      link_text: { string_id: "cfr-protections-panel-link-text" },
       cta_url: `${Services.urlFormatter.formatURLPref(
         "app.support.baseURL"
       )}etp-promotions?as=u&utm_source=inproduct`,
     },
     trigger: { id: "protectionsPanelOpen" },
   },
 ];
 
--- a/browser/components/newtab/lib/ToolbarBadgeHub.jsm
+++ b/browser/components/newtab/lib/ToolbarBadgeHub.jsm
@@ -43,16 +43,21 @@ ChromeUtils.defineModuleGetter(
   "setInterval",
   "resource://gre/modules/Timer.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "clearInterval",
   "resource://gre/modules/Timer.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "requestIdleCallback",
+  "resource://gre/modules/Timer.jsm"
+);
 
 // 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";
@@ -252,27 +257,27 @@ class _ToolbarBadgeHub {
 
       return toolbarbutton;
     }
 
     return null;
   }
 
   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;
     }
 
+    // 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);
+
     EveryWindow.registerCallback(
       this.id,
       win => {
         if (notificationsByWindow.has(win)) {
           // nothing to do
           return;
         }
         const el = this.addToolbarNotification(win, message);
@@ -292,17 +297,17 @@ class _ToolbarBadgeHub {
     // We need to clear any existing notifications and only show
     // the one set by devtools
     if (options.force) {
       this.removeAllNotifications();
     }
 
     if (message.content.delay) {
       this.state.showBadgeTimeoutId = setTimeout(() => {
-        this.registerBadgeToAllWindows(message);
+        requestIdleCallback(() => this.registerBadgeToAllWindows(message));
       }, message.content.delay);
     } else {
       this.registerBadgeToAllWindows(message);
     }
   }
 
   async messageRequest({ triggerId, template }) {
     const message = await this._handleMessageRequest({
--- a/browser/components/newtab/lib/ToolbarPanelHub.jsm
+++ b/browser/components/newtab/lib/ToolbarPanelHub.jsm
@@ -373,16 +373,18 @@ class _ToolbarPanelHub {
 
   /**
    * Inserts a message into the Protections Panel. The message is visible once
    * and afterwards set in a collapsed state. It can be shown again using the
    * info button in the panel header.
    */
   async insertProtectionPanelMessage(event) {
     const win = event.target.ownerGlobal;
+    this.maybeInsertFTL(win);
+
     const doc = event.target.ownerDocument;
     const container = doc.getElementById("messaging-system-message-container");
     const infoButton = doc.getElementById("protections-popup-info-button");
     const panelContainer = doc.getElementById("protections-popup");
     const toggleMessage = () => {
       container.toggleAttribute("disabled");
       infoButton.toggleAttribute("checked");
     };
--- a/browser/components/newtab/locales-src/asrouter.ftl
+++ b/browser/components/newtab/locales-src/asrouter.ftl
@@ -73,16 +73,22 @@ cfr-doorhanger-pintab-animation-resume =
 
 cfr-doorhanger-bookmark-fxa-header = Sync your bookmarks everywhere.
 cfr-doorhanger-bookmark-fxa-body = Great find! Now don’t be left without this bookmark on your mobile devices. Get Started with a { -fxaccount-brand-name }.
 cfr-doorhanger-bookmark-fxa-link-text = Sync bookmarks now…
 cfr-doorhanger-bookmark-fxa-close-btn-tooltip =
   .aria-label = Close button
   .title = Close
 
+## Protections panel
+
+cfr-protections-panel-header = Browse without being followed
+cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online.
+cfr-protections-panel-link-text = Learn more
+
 ## What's New toolbar button and panel
 
 cfr-whatsnew-button =
   .label = What’s New
   .tooltiptext = What’s New
 
 cfr-whatsnew-panel-header = What’s New
 
--- a/browser/components/newtab/locales-src/onboarding.ftl
+++ b/browser/components/newtab/locales-src/onboarding.ftl
@@ -6,17 +6,16 @@
 ### Various strings use a non-breaking space to avoid a single dangling /
 ### widowed word, so test on various window sizes if you also want this.
 
 ## These button action text can be split onto multiple lines, so use explicit
 ## newlines in translations to control where the line break appears (e.g., to
 ## avoid breaking quoted text).
 
 onboarding-button-label-learn-more = Learn More
-onboarding-button-label-try-now = Try It Now
 onboarding-button-label-get-started = Get Started
 
 ## Welcome modal dialog strings
 
 onboarding-welcome-header = Welcome to { -brand-short-name }
 onboarding-welcome-body = You’ve got the browser.<br/>Meet the rest of { -brand-product-name }.
 onboarding-welcome-learn-more = Learn more about the benefits.
 
@@ -71,31 +70,16 @@ onboarding-benefit-privacy-title = True 
 onboarding-benefit-privacy-text = Everything we do honors our Personal Data Promise: Take less. Keep it safe. No secrets.
 
 
 ## These strings belong to the individual onboarding messages.
 
 ## Each message has a title and a description of what the browser feature is.
 ## Each message also has an associated button for the user to try the feature.
 ## The string for the button is found above, in the UI strings section
-onboarding-private-browsing-title = Private Browsing
-onboarding-private-browsing-text = Browse by yourself. Private Browsing with Content Blocking blocks online trackers that follow you around the web.
-
-onboarding-screenshots-title = Screenshots
-onboarding-screenshots-text = Take, save and share screenshots - without leaving { -brand-short-name }. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing.
-
-onboarding-addons-title = Add-ons
-onboarding-addons-text = Add even more features that make { -brand-short-name } work harder for you. Compare prices, check the weather or express your personality with a custom theme.
-
-onboarding-ghostery-title = Ghostery
-onboarding-ghostery-text = Browse faster, smarter, or safer with extensions like Ghostery, which lets you block annoying ads.
-
-# Note: "Sync" in this case is a generic verb, as in "to synchronize"
-onboarding-fxa-title = Sync
-onboarding-fxa-text = Sign up for a { -fxaccount-brand-name } and sync your bookmarks, passwords, and open tabs everywhere you use { -brand-short-name }.
 
 onboarding-tracking-protection-title2 = Protection From Tracking
 onboarding-tracking-protection-text2 = { -brand-short-name } helps stop websites from tracking you online, making it harder for ads to follow you around the web.
 onboarding-tracking-protection-button2 = How it Works
 
 onboarding-data-sync-title = Take Your Settings with You
 # "Sync" is short for synchronize.
 onboarding-data-sync-text2 = Sync your bookmarks, passwords, and more everywhere you use { -brand-product-name }.
--- a/browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx
@@ -11,17 +11,17 @@ import React from "react";
 const FAKE_TRIPLETS = [
   {
     id: "CARD_1",
     content: {
       title: { string_id: "onboarding-private-browsing-title" },
       text: { string_id: "onboarding-private-browsing-text" },
       icon: "icon",
       primary_button: {
-        label: { string_id: "onboarding-button-label-try-now" },
+        label: { string_id: "onboarding-button-label-get-started" },
         action: {
           type: "OPEN_URL",
           data: { args: "https://example.com/" },
         },
       },
     },
   },
 ];
--- a/browser/components/newtab/test/unit/asrouter/templates/OnboardingMessage.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/OnboardingMessage.test.jsx
@@ -17,17 +17,17 @@ const DEFAULT_CONTENT = {
   },
 };
 
 const L10N_CONTENT = {
   title: { string_id: "onboarding-private-browsing-title" },
   text: { string_id: "onboarding-private-browsing-text" },
   icon: "icon",
   primary_button: {
-    label: { string_id: "onboarding-button-label-try-now" },
+    label: { string_id: "onboarding-button-label-get-started" },
     action: { type: "SOME_TYPE" },
   },
 };
 
 describe("OnboardingMessage", () => {
   let globals;
   let sandbox;
   beforeEach(() => {
--- a/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
@@ -6,17 +6,17 @@ import { Trailhead } from "content-src/a
 
 export const CARDS = [
   {
     content: {
       title: { string_id: "onboarding-private-browsing-title" },
       text: { string_id: "onboarding-private-browsing-text" },
       icon: "icon",
       primary_button: {
-        label: { string_id: "onboarding-button-label-try-now" },
+        label: { string_id: "onboarding-button-label-get-started" },
         action: {
           type: "OPEN_URL",
           data: { args: "https://example.com/" },
         },
       },
     },
   },
 ];
--- a/browser/components/newtab/test/unit/asrouter/templates/Triplets.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/Triplets.test.jsx
@@ -6,17 +6,17 @@ import React from "react";
 const CARDS = [
   {
     id: "CARD_1",
     content: {
       title: { string_id: "onboarding-private-browsing-title" },
       text: { string_id: "onboarding-private-browsing-text" },
       icon: "icon",
       primary_button: {
-        label: { string_id: "onboarding-button-label-try-now" },
+        label: { string_id: "onboarding-button-label-get-started" },
         action: {
           type: "OPEN_URL",
           data: { args: "https://example.com/" },
         },
       },
     },
   },
 ];
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
@@ -15,16 +15,17 @@ import { SafeAnchor } from "content-src/
 import { shallow, mount } from "enzyme";
 
 describe("<DSCard>", () => {
   let wrapper;
   let sandbox;
 
   beforeEach(() => {
     wrapper = shallow(<DSCard />);
+    wrapper.setState({ isSeen: true });
     sandbox = sinon.createSandbox();
   });
 
   afterEach(() => {
     sandbox.restore();
   });
 
   it("should render", () => {
@@ -74,36 +75,39 @@ describe("<DSCard>", () => {
   });
 
   it("should start with no .active class", () => {
     assert.equal(wrapper.find(".active").length, 0);
   });
 
   it("should render badges for pocket, bookmark when not a spoc element ", () => {
     wrapper = mount(<DSCard context_type="bookmark" />);
+    wrapper.setState({ isSeen: true });
     const contextFooter = wrapper.find(DSContextFooter);
 
     assert.lengthOf(contextFooter.find(StatusMessage), 1);
   });
 
   it("should render Sponsored Context for a spoc element", () => {
     const context = "Sponsored by Foo";
     wrapper = mount(<DSCard context_type="bookmark" context={context} />);
+    wrapper.setState({ isSeen: true });
     const contextFooter = wrapper.find(DSContextFooter);
 
     assert.lengthOf(contextFooter.find(StatusMessage), 0);
     assert.equal(contextFooter.find(".story-sponsored-label").text(), context);
   });
 
   describe("onLinkClick", () => {
     let dispatch;
 
     beforeEach(() => {
       dispatch = sandbox.stub();
       wrapper = shallow(<DSCard dispatch={dispatch} />);
+      wrapper.setState({ isSeen: true });
     });
 
     it("should call dispatch with the correct events", () => {
       wrapper.setProps({ id: "fooidx", pos: 1, type: "foo" });
 
       wrapper.instance().onLinkClick();
 
       assert.calledTwice(dispatch);
@@ -155,16 +159,17 @@ describe("<DSCard>", () => {
         })
       );
     });
   });
 
   describe("DSCard with CTA", () => {
     beforeEach(() => {
       wrapper = mount(<DSCard />);
+      wrapper.setState({ isSeen: true });
     });
 
     it("should render Default Meta", () => {
       const default_meta = wrapper.find(DefaultMeta);
       assert.ok(default_meta.exists());
     });
 
     it("should not render cta-link for item with no cta", () => {
@@ -173,19 +178,29 @@ describe("<DSCard>", () => {
     });
 
     it("should render cta-link by default when item has cta", () => {
       wrapper.setProps({ cta: "test" });
       const meta = wrapper.find(DefaultMeta);
       assert.equal(meta.find(".cta-link").text(), "test");
     });
 
-    it("should render cta-button when item has cta and cta button variant is true", () => {
+    it("should not render cta-button for non spoc content", () => {
       wrapper.setProps({ cta: "test", cta_variant: true });
       const meta = wrapper.find(VariantMeta);
+      assert.lengthOf(meta.find(".cta-button"), 0);
+    });
+
+    it("should render cta-button when item has cta and cta button variant is true and is spoc", () => {
+      wrapper.setProps({
+        cta: "test",
+        cta_variant: true,
+        context: "Sponsored by Foo",
+      });
+      const meta = wrapper.find(VariantMeta);
       assert.equal(meta.find(".cta-button").text(), "test");
     });
 
     it("should not render Sponsored by label in footer for spoc item with cta button variant", () => {
       wrapper.setProps({
         cta: "test",
         context: "Sponsored by test",
         cta_variant: true,
@@ -202,41 +217,79 @@ describe("<DSCard>", () => {
         cta_variant: true,
       });
 
       assert.ok(wrapper.find(VariantMeta).exists());
       const meta = wrapper.find(VariantMeta);
       assert.equal(meta.find(".source").text(), "Test · Sponsored");
     });
   });
+  describe("DSCard with Intersection Observer", () => {
+    beforeEach(() => {
+      wrapper = shallow(<DSCard />);
+    });
+
+    it("should render card when seen", () => {
+      let card = wrapper.find("div.ds-card.placeholder");
+      assert.lengthOf(card, 1);
+
+      wrapper.instance().observer = {
+        unobserve: sandbox.stub(),
+      };
+      wrapper.instance().placholderElement = "element";
+
+      wrapper.instance().onSeen([
+        {
+          isIntersecting: true,
+        },
+      ]);
+
+      assert.isTrue(wrapper.instance().state.isSeen);
+      card = wrapper.find("div.ds-card.placeholder");
+      assert.lengthOf(card, 0);
+      assert.lengthOf(wrapper.find(SafeAnchor), 1);
+      assert.calledOnce(wrapper.instance().observer.unobserve);
+      assert.calledWith(wrapper.instance().observer.unobserve, "element");
+    });
+
+    it("should setup proper placholder ref for isSeen", () => {
+      wrapper.instance().setPlaceholderRef("element");
+      assert.equal(wrapper.instance().placholderElement, "element");
+    });
+
+    it("should setup observer on componentDidMount", () => {
+      wrapper = mount(<DSCard />);
+      assert.isTrue(!!wrapper.instance().observer);
+    });
+  });
 });
 
 describe("<PlaceholderDSCard> component", () => {
   it("should have placeholder prop", () => {
     const wrapper = shallow(<PlaceholderDSCard />);
     const card = wrapper.find(DSCard);
     assert.lengthOf(card, 1);
 
     const placeholder = wrapper.find(DSCard).prop("placeholder");
     assert.isTrue(placeholder);
   });
 
   it("should contain placeholder div", () => {
     const wrapper = shallow(<DSCard placeholder={true} />);
+    wrapper.setState({ isSeen: true });
     const card = wrapper.find("div.ds-card.placeholder");
     assert.lengthOf(card, 1);
   });
 
   it("should not be clickable", () => {
     const wrapper = shallow(<DSCard placeholder={true} />);
+    wrapper.setState({ isSeen: true });
     const anchor = wrapper.find("SafeAnchor.ds-card-link");
-    assert.lengthOf(anchor, 1);
-
-    const linkClick = anchor.prop("onLinkClick");
-    assert.isUndefined(linkClick);
+    assert.lengthOf(anchor, 0);
   });
 
   it("should not have context menu", () => {
     const wrapper = shallow(<DSCard placeholder={true} />);
+    wrapper.setState({ isSeen: true });
     const linkMenu = wrapper.find(DSLinkMenu);
     assert.lengthOf(linkMenu, 0);
   });
 });
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
@@ -7,43 +7,57 @@ 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";
   const context = "Sponsored by Babel";
+  const engagement = "Popular";
 
   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 an engagement status if no badge and spoc passed", () => {
+    wrapper = mount(<DSContextFooter engagement={engagement} />);
+
+    const engagementLabel = wrapper.find(".story-view-count");
+    assert.equal(engagementLabel.text(), engagement);
+  });
   it("should render a badge if a proper badge prop is passed", () => {
-    wrapper = mount(<DSContextFooter context_type={bookmarkBadge} />);
+    wrapper = mount(
+      <DSContextFooter context_type={bookmarkBadge} engagement={engagement} />
+    );
     const { fluentID } = cardContextTypes[bookmarkBadge];
 
+    assert.lengthOf(wrapper.find(".story-view-count"), 0);
     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={context} />
+      <DSContextFooter
+        context_type={bookmarkBadge}
+        context={context}
+        engagement={engagement}
+      />
     );
 
+    assert.lengthOf(wrapper.find(".story-view-count"), 0);
     assert.lengthOf(wrapper.find(StatusMessage), 0);
     assert.equal(wrapper.find(".story-sponsored-label").text(), context);
   });
   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(
--- a/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
+++ b/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
@@ -18,16 +18,17 @@ describe("ToolbarBadgeHub", () => {
   let clearTimeoutStub;
   let setTimeoutStub;
   let setIntervalStub;
   let addObserverStub;
   let removeObserverStub;
   let getStringPrefStub;
   let clearUserPrefStub;
   let setStringPrefStub;
+  let requestIdleCallbackStub;
   beforeEach(async () => {
     globals = new GlobalOverrider();
     sandbox = sinon.createSandbox();
     instance = new _ToolbarBadgeHub();
     fakeAddImpression = sandbox.stub();
     fakeDispatch = sandbox.stub();
     isBrowserPrivateStub = sandbox.stub();
     const panelTestMsgs = await PanelTestProvider.getMessages();
@@ -62,17 +63,19 @@ describe("ToolbarBadgeHub", () => {
         },
       },
     };
     addObserverStub = sandbox.stub();
     removeObserverStub = sandbox.stub();
     getStringPrefStub = sandbox.stub();
     clearUserPrefStub = sandbox.stub();
     setStringPrefStub = sandbox.stub();
+    requestIdleCallbackStub = sandbox.stub().callsFake(fn => fn());
     globals.set({
+      requestIdleCallback: requestIdleCallbackStub,
       EveryWindow: everyWindowStub,
       PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub },
       setTimeout: setTimeoutStub,
       clearTimeout: clearTimeoutStub,
       setInterval: setIntervalStub,
       Services: {
         wm: {
           getMostRecentWindow: () => fakeWindow,
@@ -557,16 +560,18 @@ describe("ToolbarBadgeHub", () => {
 
       cb();
 
       assert.calledOnce(instance.registerBadgeToAllWindows);
       assert.calledWithExactly(
         instance.registerBadgeToAllWindows,
         msg_with_delay
       );
+      // Delayed actions should be executed inside requestIdleCallback
+      assert.calledOnce(requestIdleCallbackStub);
     });
   });
   describe("#sendUserEventTelemetry", () => {
     beforeEach(async () => {
       await instance.init(sandbox.stub().resolves(), {
         dispatch: fakeDispatch,
       });
     });
--- a/browser/locales/en-US/browser/newtab/asrouter.ftl
+++ b/browser/locales/en-US/browser/newtab/asrouter.ftl
@@ -73,16 +73,22 @@ cfr-doorhanger-pintab-animation-resume =
 
 cfr-doorhanger-bookmark-fxa-header = Sync your bookmarks everywhere.
 cfr-doorhanger-bookmark-fxa-body = Great find! Now don’t be left without this bookmark on your mobile devices. Get Started with a { -fxaccount-brand-name }.
 cfr-doorhanger-bookmark-fxa-link-text = Sync bookmarks now…
 cfr-doorhanger-bookmark-fxa-close-btn-tooltip =
   .aria-label = Close button
   .title = Close
 
+## Protections panel
+
+cfr-protections-panel-header = Browse without being followed
+cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online.
+cfr-protections-panel-link-text = Learn more
+
 ## What's New toolbar button and panel
 
 cfr-whatsnew-button =
   .label = What’s New
   .tooltiptext = What’s New
 
 cfr-whatsnew-panel-header = What’s New
 
--- a/browser/locales/en-US/browser/newtab/onboarding.ftl
+++ b/browser/locales/en-US/browser/newtab/onboarding.ftl
@@ -6,17 +6,16 @@
 ### Various strings use a non-breaking space to avoid a single dangling /
 ### widowed word, so test on various window sizes if you also want this.
 
 ## These button action text can be split onto multiple lines, so use explicit
 ## newlines in translations to control where the line break appears (e.g., to
 ## avoid breaking quoted text).
 
 onboarding-button-label-learn-more = Learn More
-onboarding-button-label-try-now = Try It Now
 onboarding-button-label-get-started = Get Started
 
 ## Welcome modal dialog strings
 
 onboarding-welcome-header = Welcome to { -brand-short-name }
 onboarding-welcome-body = You’ve got the browser.<br/>Meet the rest of { -brand-product-name }.
 onboarding-welcome-learn-more = Learn more about the benefits.
 
@@ -71,31 +70,16 @@ onboarding-benefit-privacy-title = True 
 onboarding-benefit-privacy-text = Everything we do honors our Personal Data Promise: Take less. Keep it safe. No secrets.
 
 
 ## These strings belong to the individual onboarding messages.
 
 ## Each message has a title and a description of what the browser feature is.
 ## Each message also has an associated button for the user to try the feature.
 ## The string for the button is found above, in the UI strings section
-onboarding-private-browsing-title = Private Browsing
-onboarding-private-browsing-text = Browse by yourself. Private Browsing with Content Blocking blocks online trackers that follow you around the web.
-
-onboarding-screenshots-title = Screenshots
-onboarding-screenshots-text = Take, save and share screenshots - without leaving { -brand-short-name }. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing.
-
-onboarding-addons-title = Add-ons
-onboarding-addons-text = Add even more features that make { -brand-short-name } work harder for you. Compare prices, check the weather or express your personality with a custom theme.
-
-onboarding-ghostery-title = Ghostery
-onboarding-ghostery-text = Browse faster, smarter, or safer with extensions like Ghostery, which lets you block annoying ads.
-
-# Note: "Sync" in this case is a generic verb, as in "to synchronize"
-onboarding-fxa-title = Sync
-onboarding-fxa-text = Sign up for a { -fxaccount-brand-name } and sync your bookmarks, passwords, and open tabs everywhere you use { -brand-short-name }.
 
 onboarding-tracking-protection-title2 = Protection From Tracking
 onboarding-tracking-protection-text2 = { -brand-short-name } helps stop websites from tracking you online, making it harder for ads to follow you around the web.
 onboarding-tracking-protection-button2 = How it Works
 
 onboarding-data-sync-title = Take Your Settings with You
 # "Sync" is short for synchronize.
 onboarding-data-sync-text2 = Sync your bookmarks, passwords, and more everywhere you use { -brand-product-name }.