Bug 1535460 - Add dark theme styles, Discovery stream blocking and bug fixes to Activity Stream r=r1cky
authork88hudson <k88hudson@gmail.com>
Thu, 14 Mar 2019 22:27:33 +0000
changeset 464115 a34bdd9a2ebcba3d22436687b0d59d4ed1557ceb
parent 464114 76a3b7b0c9d702f9671bcaf07bea8399357b7945
child 464116 8854d4a1652878c30a85eaf9178771efc8025dee
push id35709
push useropoprus@mozilla.com
push dateFri, 15 Mar 2019 09:39:17 +0000
treeherdermozilla-central@4d62ab0e31fd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersr1cky
bugs1535460
milestone67.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 1535460 - Add dark theme styles, Discovery stream blocking and bug fixes to Activity Stream r=r1cky Differential Revision: https://phabricator.services.mozilla.com/D23589
browser/components/newtab/common/Reducers.jsm
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/DSLinkMenu/DSLinkMenu.jsx
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/ActivityStream.jsm
browser/components/newtab/lib/DiscoveryStreamFeed.jsm
browser/components/newtab/locales-src/id/strings.properties
browser/components/newtab/locales-src/lij/strings.properties
browser/components/newtab/locales-src/pt-BR/strings.properties
browser/components/newtab/prerendered/locales/id/activity-stream-strings.js
browser/components/newtab/prerendered/locales/lij/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/lij/activity-stream-prerendered-noscripts.html
browser/components/newtab/prerendered/locales/lij/activity-stream-prerendered.html
browser/components/newtab/prerendered/locales/lij/activity-stream-strings.js
browser/components/newtab/prerendered/locales/lij/activity-stream.html
browser/components/newtab/prerendered/locales/pt-BR/activity-stream-strings.js
browser/components/newtab/test/browser/browser.ini
browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js
browser/components/newtab/test/unit/common/Reducers.test.js
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx
browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
--- a/browser/components/newtab/common/Reducers.jsm
+++ b/browser/components/newtab/common/Reducers.jsm
@@ -479,16 +479,44 @@ function DiscoveryStream(prevState = INI
     case at.DISCOVERY_STREAM_SPOCS_ENDPOINT:
       return {
         ...prevState,
         spocs: {
           ...INITIAL_STATE.DiscoveryStream.spocs,
           spocs_endpoint: action.data || INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint,
         },
       };
+    case at.PLACES_LINK_BLOCKED:
+      // Return if action data is empty, or spocs or feeds data is not loaded
+      if (!action.data || !prevState.spocs.loaded || !prevState.feeds.loaded) {
+        return prevState;
+      }
+      // Filter spocs and recommendations data inside feeds by removing action.data.url
+      // received on PLACES_LINK_BLOCKED triggered by dismiss link menu option
+      return {
+        ...prevState,
+        spocs: {
+          ...prevState.spocs,
+          data: prevState.spocs.data.spocs ? {
+            spocs: prevState.spocs.data.spocs.filter(s => s.url !== action.data.url),
+          } : {},
+        },
+        feeds: {
+          ...prevState.feeds,
+          data: Object.keys(prevState.feeds.data).reduce((accumulator, feed_url) => {
+            accumulator[feed_url] = {
+              data: {
+                ...prevState.feeds.data[feed_url].data,
+                recommendations: prevState.feeds.data[feed_url].data.recommendations.filter(r => r.url !== action.data.url),
+              },
+            };
+            return accumulator;
+          }, {}),
+        },
+      };
     case at.DISCOVERY_STREAM_SPOCS_UPDATE:
       if (action.data) {
         return {
           ...prevState,
           spocs: {
             ...prevState.spocs,
             lastUpdated: action.data.lastUpdated,
             data: action.data.spocs,
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
@@ -55,17 +55,18 @@ export class DSCard extends React.PureCo
           </div>
           <ImpressionStats
             campaignId={this.props.campaignId}
             rows={[{id: this.props.id, pos: this.props.pos}]}
             dispatch={this.props.dispatch}
             source={this.props.type} />
         </SafeAnchor>
         <DSLinkMenu
-          index={this.props.index}
+          id={this.props.id}
+          index={this.props.pos}
           dispatch={this.props.dispatch}
           intl={this.props.intl}
           url={this.props.url}
           title={this.props.title}
           source={this.props.source}
           type={this.props.type} />
       </div>
     );
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
@@ -16,16 +16,20 @@
       }
 
       color: $blue-60;
     }
   }
 
   &:active {
     header {
+      @include dark-theme-only {
+        color: $blue-50;
+      }
+
       color: $blue-70;
     }
   }
 
   .img-wrapper {
     width: 100%;
   }
 
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
@@ -37,41 +37,43 @@ export class _DSLinkMenu extends React.P
       dsLinkMenuHostDiv.parentElement.classList.add("last-item");
     }
     dsLinkMenuHostDiv.parentElement.classList.add("active");
   }
 
   render() {
     const {index, dispatch} = this.props;
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
-    const TOP_STORIES_SOURCE = "TOP_STORIES";
-    const TOP_STORIES_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow"];
+    const TOP_STORIES_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"];
     const title = this.props.title || this.props.source;
+    const type = this.props.type || "DISCOVERY_STREAM";
 
     return (<div>
       <button ref={this.contextMenuButtonRef}
               className="context-menu-button icon"
               title={this.props.intl.formatMessage({id: "context_menu_title"})}
               onClick={this.onMenuButtonClick}>
         <span className="sr-only">
           <FormattedMessage id="context_menu_button_sr" values={{title}} />
         </span>
       </button>
       {isContextMenuOpen &&
         <LinkMenu
           dispatch={dispatch}
           index={index}
-          source={TOP_STORIES_SOURCE}
+          source={type.toUpperCase()}
           onUpdate={this.onMenuUpdate}
           onShow={this.onMenuShow}
           options={TOP_STORIES_CONTEXT_MENU_OPTIONS}
+          shouldSendImpressionStats={true}
           site={{
             referrer: "https://getpocket.com/recommendations",
             title: this.props.title,
             type: this.props.type,
             url: this.props.url,
+            guid: this.props.id,
           }} />
       }
     </div>);
   }
 }
 
 export const DSLinkMenu = injectIntl(_DSLinkMenu);
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
@@ -27,17 +27,17 @@ export class Hero extends React.PureComp
       }));
     }
   }
 
   render() {
     const {data} = this.props;
 
     // Handle a render before feed has been fetched by displaying nothing
-    if (!data || !data.recommendations) {
+    if (!data || !data.recommendations || !data.recommendations.length) {
       return (
         <div />
       );
     }
 
     let [heroRec, ...otherRecs] = data.recommendations.slice(0, this.props.items);
     this.heroRec = heroRec;
 
@@ -92,17 +92,18 @@ export class Hero extends React.PureComp
               </div>
               <ImpressionStats
                 campaignId={heroRec.campaignId}
                 rows={[{id: heroRec.id, pos: heroRec.pos}]}
                 dispatch={this.props.dispatch}
                 source={this.props.type} />
             </SafeAnchor>
             <DSLinkMenu
-              index={this.props.index}
+              id={heroRec.id}
+              index={heroRec.pos}
               dispatch={this.props.dispatch}
               intl={this.props.intl}
               url={heroRec.url}
               title={heroRec.title}
               source={heroRec.domain}
               type={this.props.type} />
           </div>
           <div className={`${this.props.subComponentType}`}>
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/_Hero.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/_Hero.scss
@@ -167,16 +167,17 @@
         padding-top: 100%; // 1:1 aspect ratio
       }
     }
 
     .cards {
       display: grid;
       grid-template-columns: repeat(2, 1fr);
       grid-column-gap: 24px;
+      grid-auto-rows: min-content;
     }
   }
 
   // "Full width layout"
   .ds-column-9 &,
   .ds-column-10 &,
   .ds-column-11 &,
   .ds-column-12 & {
@@ -226,16 +227,17 @@
         }
       }
     }
 
     .cards {
       display: grid;
       grid-template-columns: repeat(2, 1fr);
       grid-column-gap: 24px;
+      grid-auto-rows: min-content;
 
       .ds-card {
         &:hover {
           @include dark-theme-only {
             background: none;
 
             .title {
               color: $blue-40;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
@@ -58,17 +58,18 @@ export class ListItem extends React.Pure
           <div className="ds-list-image" style={{backgroundImage: `url(${this.props.image_src})`}} />
           <ImpressionStats
             campaignId={this.props.campaignId}
             rows={[{id: this.props.id, pos: this.props.pos}]}
             dispatch={this.props.dispatch}
             source={this.props.type} />
         </SafeAnchor>
         <DSLinkMenu
-          index={this.props.index}
+          id={this.props.id}
+          index={this.props.pos}
           dispatch={this.props.dispatch}
           intl={this.props.intl}
           url={this.props.url}
           title={this.props.title}
           source={this.props.source}
           type={this.props.type} />
       </li>
     );
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -2060,17 +2060,18 @@ main {
       height: 0;
       padding-top: 100%; }
   .ds-column-5 .ds-hero .cards,
   .ds-column-6 .ds-hero .cards,
   .ds-column-7 .ds-hero .cards,
   .ds-column-8 .ds-hero .cards {
     display: grid;
     grid-template-columns: repeat(2, 1fr);
-    grid-column-gap: 24px; }
+    grid-column-gap: 24px;
+    grid-auto-rows: min-content; }
   .ds-column-9 .ds-hero,
   .ds-column-10 .ds-hero,
   .ds-column-11 .ds-hero,
   .ds-column-12 .ds-hero {
     display: grid;
     grid-template-columns: repeat(2, 1fr);
     grid-column-gap: 24px; }
     .ds-column-9 .ds-hero.ds-hero-border,
@@ -2138,17 +2139,18 @@ main {
         .ds-column-12 .ds-hero .wrapper .meta .source {
           margin-bottom: 0; }
     .ds-column-9 .ds-hero .cards,
     .ds-column-10 .ds-hero .cards,
     .ds-column-11 .ds-hero .cards,
     .ds-column-12 .ds-hero .cards {
       display: grid;
       grid-template-columns: repeat(2, 1fr);
-      grid-column-gap: 24px; }
+      grid-column-gap: 24px;
+      grid-auto-rows: min-content; }
       [lwt-newtab-brighttext]:not(.force-light-theme) .ds-column-9 .ds-hero .cards .ds-card:hover, [lwt-newtab-brighttext]:not(.force-light-theme)
       .ds-column-10 .ds-hero .cards .ds-card:hover, [lwt-newtab-brighttext]:not(.force-light-theme)
       .ds-column-11 .ds-hero .cards .ds-card:hover, [lwt-newtab-brighttext]:not(.force-light-theme)
       .ds-column-12 .ds-hero .cards .ds-card:hover {
         background: none; }
         [lwt-newtab-brighttext]:not(.force-light-theme) .ds-column-9 .ds-hero .cards .ds-card:hover .title, [lwt-newtab-brighttext]:not(.force-light-theme)
         .ds-column-10 .ds-hero .cards .ds-card:hover .title, [lwt-newtab-brighttext]:not(.force-light-theme)
         .ds-column-11 .ds-hero .cards .ds-card:hover .title, [lwt-newtab-brighttext]:not(.force-light-theme)
@@ -2555,16 +2557,18 @@ main {
   flex-direction: column;
   position: relative; }
   .ds-card:hover header {
     color: #0060DF; }
     [lwt-newtab-brighttext]:not(.force-light-theme) .ds-card:hover header {
       color: #45A1FF; }
   .ds-card:active header {
     color: #003EAA; }
+    [lwt-newtab-brighttext]:not(.force-light-theme) .ds-card:active header {
+      color: #0A84FF; }
   .ds-card .img-wrapper {
     width: 100%; }
   .ds-card .img {
     background-color: var(--newtab-card-placeholder-color);
     background-position: center;
     background-repeat: no-repeat;
     background-size: cover;
     border-radius: 4px;
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -2063,17 +2063,18 @@ main {
       height: 0;
       padding-top: 100%; }
   .ds-column-5 .ds-hero .cards,
   .ds-column-6 .ds-hero .cards,
   .ds-column-7 .ds-hero .cards,
   .ds-column-8 .ds-hero .cards {
     display: grid;
     grid-template-columns: repeat(2, 1fr);
-    grid-column-gap: 24px; }
+    grid-column-gap: 24px;
+    grid-auto-rows: min-content; }
   .ds-column-9 .ds-hero,
   .ds-column-10 .ds-hero,
   .ds-column-11 .ds-hero,
   .ds-column-12 .ds-hero {
     display: grid;
     grid-template-columns: repeat(2, 1fr);
     grid-column-gap: 24px; }
     .ds-column-9 .ds-hero.ds-hero-border,
@@ -2141,17 +2142,18 @@ main {
         .ds-column-12 .ds-hero .wrapper .meta .source {
           margin-bottom: 0; }
     .ds-column-9 .ds-hero .cards,
     .ds-column-10 .ds-hero .cards,
     .ds-column-11 .ds-hero .cards,
     .ds-column-12 .ds-hero .cards {
       display: grid;
       grid-template-columns: repeat(2, 1fr);
-      grid-column-gap: 24px; }
+      grid-column-gap: 24px;
+      grid-auto-rows: min-content; }
       [lwt-newtab-brighttext]:not(.force-light-theme) .ds-column-9 .ds-hero .cards .ds-card:hover, [lwt-newtab-brighttext]:not(.force-light-theme)
       .ds-column-10 .ds-hero .cards .ds-card:hover, [lwt-newtab-brighttext]:not(.force-light-theme)
       .ds-column-11 .ds-hero .cards .ds-card:hover, [lwt-newtab-brighttext]:not(.force-light-theme)
       .ds-column-12 .ds-hero .cards .ds-card:hover {
         background: none; }
         [lwt-newtab-brighttext]:not(.force-light-theme) .ds-column-9 .ds-hero .cards .ds-card:hover .title, [lwt-newtab-brighttext]:not(.force-light-theme)
         .ds-column-10 .ds-hero .cards .ds-card:hover .title, [lwt-newtab-brighttext]:not(.force-light-theme)
         .ds-column-11 .ds-hero .cards .ds-card:hover .title, [lwt-newtab-brighttext]:not(.force-light-theme)
@@ -2558,16 +2560,18 @@ main {
   flex-direction: column;
   position: relative; }
   .ds-card:hover header {
     color: #0060DF; }
     [lwt-newtab-brighttext]:not(.force-light-theme) .ds-card:hover header {
       color: #45A1FF; }
   .ds-card:active header {
     color: #003EAA; }
+    [lwt-newtab-brighttext]:not(.force-light-theme) .ds-card:active header {
+      color: #0A84FF; }
   .ds-card .img-wrapper {
     width: 100%; }
   .ds-card .img {
     background-color: var(--newtab-card-placeholder-color);
     background-position: center;
     background-repeat: no-repeat;
     background-size: cover;
     border-radius: 4px;
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -2060,17 +2060,18 @@ main {
       height: 0;
       padding-top: 100%; }
   .ds-column-5 .ds-hero .cards,
   .ds-column-6 .ds-hero .cards,
   .ds-column-7 .ds-hero .cards,
   .ds-column-8 .ds-hero .cards {
     display: grid;
     grid-template-columns: repeat(2, 1fr);
-    grid-column-gap: 24px; }
+    grid-column-gap: 24px;
+    grid-auto-rows: min-content; }
   .ds-column-9 .ds-hero,
   .ds-column-10 .ds-hero,
   .ds-column-11 .ds-hero,
   .ds-column-12 .ds-hero {
     display: grid;
     grid-template-columns: repeat(2, 1fr);
     grid-column-gap: 24px; }
     .ds-column-9 .ds-hero.ds-hero-border,
@@ -2138,17 +2139,18 @@ main {
         .ds-column-12 .ds-hero .wrapper .meta .source {
           margin-bottom: 0; }
     .ds-column-9 .ds-hero .cards,
     .ds-column-10 .ds-hero .cards,
     .ds-column-11 .ds-hero .cards,
     .ds-column-12 .ds-hero .cards {
       display: grid;
       grid-template-columns: repeat(2, 1fr);
-      grid-column-gap: 24px; }
+      grid-column-gap: 24px;
+      grid-auto-rows: min-content; }
       [lwt-newtab-brighttext]:not(.force-light-theme) .ds-column-9 .ds-hero .cards .ds-card:hover, [lwt-newtab-brighttext]:not(.force-light-theme)
       .ds-column-10 .ds-hero .cards .ds-card:hover, [lwt-newtab-brighttext]:not(.force-light-theme)
       .ds-column-11 .ds-hero .cards .ds-card:hover, [lwt-newtab-brighttext]:not(.force-light-theme)
       .ds-column-12 .ds-hero .cards .ds-card:hover {
         background: none; }
         [lwt-newtab-brighttext]:not(.force-light-theme) .ds-column-9 .ds-hero .cards .ds-card:hover .title, [lwt-newtab-brighttext]:not(.force-light-theme)
         .ds-column-10 .ds-hero .cards .ds-card:hover .title, [lwt-newtab-brighttext]:not(.force-light-theme)
         .ds-column-11 .ds-hero .cards .ds-card:hover .title, [lwt-newtab-brighttext]:not(.force-light-theme)
@@ -2555,16 +2557,18 @@ main {
   flex-direction: column;
   position: relative; }
   .ds-card:hover header {
     color: #0060DF; }
     [lwt-newtab-brighttext]:not(.force-light-theme) .ds-card:hover header {
       color: #45A1FF; }
   .ds-card:active header {
     color: #003EAA; }
+    [lwt-newtab-brighttext]:not(.force-light-theme) .ds-card:active header {
+      color: #0A84FF; }
   .ds-card .img-wrapper {
     width: 100%; }
   .ds-card .img {
     background-color: var(--newtab-card-placeholder-color);
     background-position: center;
     background-repeat: no-repeat;
     background-size: cover;
     border-radius: 4px;
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -7291,19 +7291,19 @@ class DSLinkMenu_DSLinkMenu extends exte
       dsLinkMenuHostDiv.parentElement.classList.add("last-item");
     }
     dsLinkMenuHostDiv.parentElement.classList.add("active");
   }
 
   render() {
     const { index, dispatch } = this.props;
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
-    const TOP_STORIES_SOURCE = "TOP_STORIES";
-    const TOP_STORIES_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow"];
+    const TOP_STORIES_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"];
     const title = this.props.title || this.props.source;
+    const type = this.props.type || "DISCOVERY_STREAM";
 
     return external_React_default.a.createElement(
       "div",
       null,
       external_React_default.a.createElement(
         "button",
         { ref: this.contextMenuButtonRef,
           className: "context-menu-button icon",
@@ -7313,25 +7313,27 @@ class DSLinkMenu_DSLinkMenu extends exte
           "span",
           { className: "sr-only" },
           external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: "context_menu_button_sr", values: { title } })
         )
       ),
       isContextMenuOpen && external_React_default.a.createElement(LinkMenu["LinkMenu"], {
         dispatch: dispatch,
         index: index,
-        source: TOP_STORIES_SOURCE,
+        source: type.toUpperCase(),
         onUpdate: this.onMenuUpdate,
         onShow: this.onMenuShow,
         options: TOP_STORIES_CONTEXT_MENU_OPTIONS,
+        shouldSendImpressionStats: true,
         site: {
           referrer: "https://getpocket.com/recommendations",
           title: this.props.title,
           type: this.props.type,
-          url: this.props.url
+          url: this.props.url,
+          guid: this.props.id
         } })
     );
   }
 }
 
 const DSLinkMenu = Object(external_ReactIntl_["injectIntl"])(DSLinkMenu_DSLinkMenu);
 // EXTERNAL MODULE: ./content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
 var ImpressionStats = __webpack_require__(33);
@@ -7478,17 +7480,18 @@ class DSCard_DSCard extends external_Rea
         ),
         external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
           campaignId: this.props.campaignId,
           rows: [{ id: this.props.id, pos: this.props.pos }],
           dispatch: this.props.dispatch,
           source: this.props.type })
       ),
       external_React_default.a.createElement(DSLinkMenu, {
-        index: this.props.index,
+        id: this.props.id,
+        index: this.props.pos,
         dispatch: this.props.dispatch,
         intl: this.props.intl,
         url: this.props.url,
         title: this.props.title,
         source: this.props.source,
         type: this.props.type })
     );
   }
@@ -7665,17 +7668,18 @@ class List_ListItem extends external_Rea
         external_React_default.a.createElement("div", { className: "ds-list-image", style: { backgroundImage: `url(${this.props.image_src})` } }),
         external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
           campaignId: this.props.campaignId,
           rows: [{ id: this.props.id, pos: this.props.pos }],
           dispatch: this.props.dispatch,
           source: this.props.type })
       ),
       external_React_default.a.createElement(DSLinkMenu, {
-        index: this.props.index,
+        id: this.props.id,
+        index: this.props.pos,
         dispatch: this.props.dispatch,
         intl: this.props.intl,
         url: this.props.url,
         title: this.props.title,
         source: this.props.source,
         type: this.props.type })
     );
   }
@@ -7759,17 +7763,17 @@ class Hero_Hero extends external_React_d
       }));
     }
   }
 
   render() {
     const { data } = this.props;
 
     // Handle a render before feed has been fetched by displaying nothing
-    if (!data || !data.recommendations) {
+    if (!data || !data.recommendations || !data.recommendations.length) {
       return external_React_default.a.createElement("div", null);
     }
 
     let [heroRec, ...otherRecs] = data.recommendations.slice(0, this.props.items);
     this.heroRec = heroRec;
 
     let cards = otherRecs.map((rec, index) => external_React_default.a.createElement(DSCard_DSCard, {
       campaignId: rec.campaign_id,
@@ -7847,17 +7851,18 @@ class Hero_Hero extends external_React_d
             ),
             external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
               campaignId: heroRec.campaignId,
               rows: [{ id: heroRec.id, pos: heroRec.pos }],
               dispatch: this.props.dispatch,
               source: this.props.type })
           ),
           external_React_default.a.createElement(DSLinkMenu, {
-            index: this.props.index,
+            id: heroRec.id,
+            index: heroRec.pos,
             dispatch: this.props.dispatch,
             intl: this.props.intl,
             url: heroRec.url,
             title: heroRec.title,
             source: heroRec.domain,
             type: this.props.type })
         ),
         external_React_default.a.createElement(
@@ -12352,16 +12357,40 @@ function DiscoveryStream(prevState = INI
         })
       });
     case Actions["actionTypes"].DISCOVERY_STREAM_SPOCS_ENDPOINT:
       return Object.assign({}, prevState, {
         spocs: Object.assign({}, INITIAL_STATE.DiscoveryStream.spocs, {
           spocs_endpoint: action.data || INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint
         })
       });
+    case Actions["actionTypes"].PLACES_LINK_BLOCKED:
+      // Return if action data is empty, or spocs or feeds data is not loaded
+      if (!action.data || !prevState.spocs.loaded || !prevState.feeds.loaded) {
+        return prevState;
+      }
+      // Filter spocs and recommendations data inside feeds by removing action.data.url
+      // received on PLACES_LINK_BLOCKED triggered by dismiss link menu option
+      return Object.assign({}, prevState, {
+        spocs: Object.assign({}, prevState.spocs, {
+          data: prevState.spocs.data.spocs ? {
+            spocs: prevState.spocs.data.spocs.filter(s => s.url !== action.data.url)
+          } : {}
+        }),
+        feeds: Object.assign({}, prevState.feeds, {
+          data: Object.keys(prevState.feeds.data).reduce((accumulator, feed_url) => {
+            accumulator[feed_url] = {
+              data: Object.assign({}, prevState.feeds.data[feed_url].data, {
+                recommendations: prevState.feeds.data[feed_url].data.recommendations.filter(r => r.url !== action.data.url)
+              })
+            };
+            return accumulator;
+          }, {})
+        })
+      });
     case Actions["actionTypes"].DISCOVERY_STREAM_SPOCS_UPDATE:
       if (action.data) {
         return Object.assign({}, prevState, {
           spocs: Object.assign({}, prevState.spocs, {
             lastUpdated: action.data.lastUpdated,
             data: action.data.spocs,
             loaded: true
           })
--- a/browser/components/newtab/lib/ActivityStream.jsm
+++ b/browser/components/newtab/lib/ActivityStream.jsm
@@ -238,16 +238,20 @@ const PREFS_CONFIG = new Map([
         api_key_pref: "extensions.pocket.oAuthConsumerKey",
         enabled: isEnabled,
         show_spocs: geo === "US",
         // This is currently an exmple layout used for dev purposes.
         layout_endpoint: "https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic",
       });
     },
   }],
+  ["discoverystream.endpoints", {
+    title: "Endpoint prefixes (comma-separated) that are allowed to be requested",
+    value: "https://getpocket.cdn.mozilla.net/",
+  }],
   ["discoverystream.optOut.0", {
     title: "Opt out of new layout v0",
     value: false,
   }],
   ["discoverystream.spoc.impressions", {
     title: "Track spoc impressions",
     skipBroadcast: true,
     value: "{}",
--- a/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
+++ b/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
@@ -1,29 +1,31 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {NewTabUtils} = ChromeUtils.import("resource://gre/modules/NewTabUtils.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 ChromeUtils.defineModuleGetter(this, "perfService", "resource://activity-stream/common/PerfService.jsm");
 
 const {actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
 const {PersistentCache} = ChromeUtils.import("resource://activity-stream/lib/PersistentCache.jsm");
 
 const CACHE_KEY = "discovery_stream";
 const LAYOUT_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const STARTUP_CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week
 const COMPONENT_FEEDS_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const SPOCS_FEEDS_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour
 const MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server
 const PREF_CONFIG = "discoverystream.config";
+const PREF_ENDPOINTS = "discoverystream.endpoints";
 const PREF_OPT_OUT = "discoverystream.optOut.0";
 const PREF_SHOW_SPONSORED = "showSponsored";
 const PREF_SPOC_IMPRESSIONS = "discoverystream.spoc.impressions";
 const PREF_REC_IMPRESSIONS = "discoverystream.rec.impressions";
 
 this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
   constructor() {
     // Internal state for checking if we've intialized all our data
@@ -83,27 +85,30 @@ this.DiscoveryStreamFeed = class Discove
   }
 
   async fetchFromEndpoint(endpoint) {
     if (!endpoint) {
       Cu.reportError("Tried to fetch endpoint but none was configured.");
       return null;
     }
     try {
+      // Make sure the requested endpoint is allowed
+      const allowed = this.store.getState().Prefs.values[PREF_ENDPOINTS].split(",");
+      if (!allowed.some(prefix => endpoint.startsWith(prefix))) {
+        throw new Error(`Not one of allowed prefixes (${allowed})`);
+      }
+
       const response = await fetch(endpoint, {credentials: "omit"});
       if (!response.ok) {
-        // istanbul ignore next
-        throw new Error(`${endpoint} returned unexpected status: ${response.status}`);
+        throw new Error(`Unexpected status (${response.status})`);
       }
       return response.json();
     } catch (error) {
-      // istanbul ignore next
       Cu.reportError(`Failed to fetch ${endpoint}: ${error.message}`);
     }
-    // istanbul ignore next
     return null;
   }
 
   /**
    * Returns true if data in the cache for a particular key has expired or is missing.
    * @param {object} cachedData data returned from cache.get()
    * @param {string} key a cache key
    * @param {string?} url for "feed" only, the URL of the feed.
@@ -195,30 +200,35 @@ this.DiscoveryStreamFeed = class Discove
   buildFeedPromise({newFeedsPromises, newFeeds}, isStartup) {
     return component => {
       const {url} = component.feed;
 
       if (!newFeeds[url]) {
         // We initially stub this out so we don't fetch dupes,
         // we then fill in with the proper object inside the promise.
         newFeeds[url] = {};
-
         const feedPromise = this.getComponentFeed(url, isStartup);
-
-        feedPromise.then(data => {
-          newFeeds[url] = data;
+        feedPromise.then(feed => {
+          newFeeds[url] = this.filterRecommendations(feed);
         }).catch(/* istanbul ignore next */ error => {
           Cu.reportError(`Error trying to load component feed ${url}: ${error}`);
         });
 
         newFeedsPromises.push(feedPromise);
       }
     };
   }
 
+  filterRecommendations(feed) {
+    if (feed && feed.data && feed.data.recommendations && feed.data.recommendations.length) {
+      return {data: this.filterBlocked(feed.data, "recommendations")};
+    }
+    return feed;
+  }
+
   /**
    * reduceFeedComponents - Filters out components with no feeds, and combines
    *                        all feeds on this component with the feeds from other components.
    * @param {Boolean} isStartup We have different cache handling for startup.
    * @returns {Function} We return a function so we can contain the scope for isStartup.
    *                     Reduces feeds into promises and feed data.
    */
   reduceFeedComponents(isStartup) {
@@ -304,22 +314,34 @@ this.DiscoveryStreamFeed = class Discove
       lastUpdated: Date.now(),
       data: {},
     };
 
     sendUpdate({
       type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
       data: {
         lastUpdated: spocs.lastUpdated,
-        spocs: this.transform(this.filterSpocs(spocs.data)),
+        spocs: this.transform(this.frequencyCapSpocs(spocs.data)),
       },
     });
   }
 
-  transform(data) {
+  filterBlocked(data, type) {
+    if (data && data[type] && data[type].length) {
+      const filteredItems = data[type].filter(item => !NewTabUtils.blockedLinks.isBlocked({"url": item.url}));
+      return {
+        ...data,
+        [type]: filteredItems,
+      };
+    }
+    return data;
+  }
+
+  transform(spocs) {
+    const data = this.filterBlocked(spocs, "spocs");
     if (data && data.spocs && data.spocs.length) {
       const spocsPerDomain = this.store.getState().DiscoveryStream.spocs.spocs_per_domain || 1;
       const campaignMap = {};
       return {
         ...data,
         spocs: data.spocs
           .map(s => ({...s, score: s.item_score}))
           .filter(s => s.score >= s.min_score)
@@ -335,17 +357,17 @@ this.DiscoveryStreamFeed = class Discove
             return false;
           }),
       };
     }
     return data;
   }
 
   // Filter spocs based on frequency caps
-  filterSpocs(data) {
+  frequencyCapSpocs(data) {
     if (data && data.spocs && data.spocs.length) {
       const {spocs} = data;
       const impressions = this.readImpressionsPref(PREF_SPOC_IMPRESSIONS);
       return {
         ...data,
         spocs: spocs.filter(s => this.isBelowFrequencyCap(impressions, s)),
       };
     }
@@ -706,17 +728,17 @@ this.DiscoveryStreamFeed = class Discove
 
           const cachedData = await this.cache.get() || {};
           const {spocs} = cachedData;
 
           this.store.dispatch(ac.AlsoToPreloaded({
             type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
             data: {
               lastUpdated: spocs.lastUpdated,
-              spocs: this.transform(this.filterSpocs(spocs.data)),
+              spocs: this.transform(this.frequencyCapSpocs(spocs.data)),
             },
           }));
         }
         break;
       case at.UNINIT:
         // When this feed is shutting down:
         this.uninitPrefs();
         break;
--- a/browser/components/newtab/locales-src/id/strings.properties
+++ b/browser/components/newtab/locales-src/id/strings.properties
@@ -88,16 +88,18 @@ section_disclaimer_topstories_buttontext
 # for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
 # in English, while "Home" should be localized matching the about:preferences
 # sidebar mozilla-central string for the panel that has preferences related to
 # what is shown for the homepage, new windows, and new tabs.
 prefs_home_header=Konten Beranda Firefox
 prefs_home_description=Pilih konten yang ingin Anda tampilkan dalam Beranda Firefox.
 
 prefs_content_discovery_header=Beranda Firefox
+prefs_content_discovery_description=Penemuan Konten dalam Firefox Home memungkinkan Anda untuk menemukan artikel bermutu tinggi dan relevan dari seluruh web.
+prefs_content_discovery_button=Matikan Penemuan Konten
 
 # LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
 # plural forms used in a drop down of multiple row options (1 row, 2 rows).
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 prefs_section_rows_option={num} baris
 prefs_search_header=Pencarian Web
 prefs_topsites_description=Situs yang sering Anda kunjungi
 prefs_topstories_description2=Konten bermutu dari seluruh web, khusus untuk Anda
--- a/browser/components/newtab/locales-src/lij/strings.properties
+++ b/browser/components/newtab/locales-src/lij/strings.properties
@@ -1,9 +1,9 @@
-newtab_page_title=Neuvo Feuggio
+newtab_page_title=Neuvo feuggio
 
 header_top_sites=I megio sciti
 header_highlights=In evidensa
 # LOCALIZATION NOTE(header_recommended_by): This is followed by the name
 # of the corresponding content provider.
 header_recommended_by=Consegiou da {provider}
 
 # LOCALIZATION NOTE(context_menu_button_sr): This is for screen readers when
@@ -47,18 +47,18 @@ menu_action_archive_pocket=Archivia in P
 
 # LOCALIZATION NOTE (menu_action_show_file_*): These are platform specific strings
 # found in the context menu of an item that has been downloaded. The intention behind
 # "this action" is that it will show where the downloaded file exists on the file system
 # for each operating system.
 menu_action_show_file_mac_os=Fanni vedde in Finder
 menu_action_show_file_windows=Arvi cartella
 menu_action_show_file_linux=Arvi cartella
-menu_action_show_file_default=Fanni vedde file
-menu_action_open_file=Arvi file
+menu_action_show_file_default=Mostra o schedaio
+menu_action_open_file=Arvi schedaio
 
 # LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
 # "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
 # link that belongs to this downloaded item"
 menu_action_copy_download_link=Còpia indirisso òrigine
 menu_action_go_to_download_page=Vanni a-a pagina de descaregamento
 menu_action_remove_download=Scancella da-a stöia
 
@@ -88,16 +88,17 @@ section_disclaimer_topstories_buttontext=Va ben, ò capio
 # for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
 # in English, while "Home" should be localized matching the about:preferences
 # sidebar mozilla-central string for the panel that has preferences related to
 # what is shown for the homepage, new windows, and new tabs.
 prefs_home_header=Pagina iniçiâ de Firefox
 prefs_home_description=Çerni i contegnui che ti veu vedde inta pagina iniçiâ de Firefox.
 
 prefs_content_discovery_header=Pagina iniçiâ de Firefox
+prefs_content_discovery_button=Dizabilita a descoverta de neuvi contegnui
 
 # LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
 # plural forms used in a drop down of multiple row options (1 row, 2 rows).
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 prefs_section_rows_option={num} riga;{num} righe
 prefs_search_header=Çerca into Web
 prefs_topsites_description=I sciti che ti vixiti de ciù
 prefs_topstories_description2=I megio contegnui pigiæ in gio pe-a ræ, personalizæ pe ti
--- a/browser/components/newtab/locales-src/pt-BR/strings.properties
+++ b/browser/components/newtab/locales-src/pt-BR/strings.properties
@@ -127,24 +127,24 @@ edit_topsites_edit_button=Editar este si
 
 # LOCALIZATION NOTE (topsites_form_*): This is shown in the New/Edit Topsite modal.
 topsites_form_add_header=Novo site popular
 topsites_form_edit_header=Editar site popular
 topsites_form_title_label=Título
 topsites_form_title_placeholder=Digite um título
 topsites_form_url_label=URL
 topsites_form_image_url_label=URL de imagem personalizada
-topsites_form_url_placeholder=Digite ou cole um URL
+topsites_form_url_placeholder=Digite ou cole uma URL
 topsites_form_use_image_link=Usar uma imagem personalizada…
 # LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
 topsites_form_preview_button=Visualizar
 topsites_form_add_button=Adicionar
 topsites_form_save_button=Salvar
 topsites_form_cancel_button=Cancelar
-topsites_form_url_validation=É necessário um URL válido
+topsites_form_url_validation=É necessário uma URL válida
 topsites_form_image_validation=Não foi possível carregar a imagem. Tente uma URL diferente.
 
 # LOCALIZATION NOTE (pocket_read_more): This is shown at the bottom of the
 # trending stories section and precedes a list of links to popular topics.
 pocket_read_more=Tópicos populares:
 # LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
 # end of the list of popular topic links.
 pocket_read_even_more=Ver mais histórias
--- a/browser/components/newtab/prerendered/locales/id/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/id/activity-stream-strings.js
@@ -36,18 +36,18 @@ window.gActivityStreamStrings = {
   "search_header": "Pencarian {search_engine_name}",
   "search_web_placeholder": "Cari di Web",
   "section_disclaimer_topstories": "Kisah paling menarik di web, dipilih berdasarkan yang Anda baca. Dari Pocket, kini bagian dari Mozilla.",
   "section_disclaimer_topstories_linktext": "Pelajari cara kerjanya.",
   "section_disclaimer_topstories_buttontext": "Oke, paham",
   "prefs_home_header": "Konten Beranda Firefox",
   "prefs_home_description": "Pilih konten yang ingin Anda tampilkan dalam Beranda Firefox.",
   "prefs_content_discovery_header": "Beranda Firefox",
-  "prefs_content_discovery_description": "Content Discovery in Firefox Home allows you to discover high-quality, relevant articles from across the web.",
-  "prefs_content_discovery_button": "Turn Off Content Discovery",
+  "prefs_content_discovery_description": "Penemuan Konten dalam Firefox Home memungkinkan Anda untuk menemukan artikel bermutu tinggi dan relevan dari seluruh web.",
+  "prefs_content_discovery_button": "Matikan Penemuan Konten",
   "prefs_section_rows_option": "{num} baris",
   "prefs_search_header": "Pencarian Web",
   "prefs_topsites_description": "Situs yang sering Anda kunjungi",
   "prefs_topstories_description2": "Konten bermutu dari seluruh web, khusus untuk Anda",
   "prefs_topstories_options_sponsored_label": "Konten Sponsor",
   "prefs_topstories_sponsored_learn_more": "Pelajari lebih lanjut",
   "prefs_highlights_description": "Sejumlah situs yang Anda simpan atau kunjungi",
   "prefs_highlights_options_visited_label": "Laman yang Dikunjungi",
--- a/browser/components/newtab/prerendered/locales/lij/activity-stream-noscripts.html
+++ b/browser/components/newtab/prerendered/locales/lij/activity-stream-noscripts.html
@@ -1,14 +1,14 @@
 <!doctype html>
 <html lang="lij" dir="ltr">
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
-    <title>Neuvo Feuggio</title>
+    <title>Neuvo feuggio</title>
     <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"><!-- Regular React Rendering --></div>
     <div id="snippets-container">
       <div id="snippets"></div>
--- a/browser/components/newtab/prerendered/locales/lij/activity-stream-prerendered-noscripts.html
+++ b/browser/components/newtab/prerendered/locales/lij/activity-stream-prerendered-noscripts.html
@@ -1,14 +1,14 @@
 <!doctype html>
 <html lang="lij" dir="ltr">
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
-    <title>Neuvo Feuggio</title>
+    <title>Neuvo feuggio</title>
     <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"><div data-reactroot=""><div class="outer-wrapper fixed-to-top"><main><div class="non-collapsible-section"><div class="search-wrapper"><div class="search-inner-wrapper"><label for="newtab-search-text" class="search-label"><span class="sr-only"><span>Çerca inta Ræ</span></span></label><input type="search" id="newtab-search-text" maxLength="256" placeholder="Çerca inta Ræ" title="Çerca inta Ræ"/><button id="searchSubmit" class="search-button" title="Çerca"><span class="sr-only"><span>Çerca</span></span></button></div></div></div><div class="body-wrapper"><div class="sections-list"><section class="collapsible-section top-sites animation-enabled" data-section-id="topsites"><div class="section-top-bar"><h3 class="section-title"><span class="click-target-container"><span class="click-target"><span class="icon icon-small-spacer icon-topsites"></span><span>I megio sciti</span></span><span class="click-target"></span><span class="learn-more-link-wrapper"></span></span></h3><div><button class="context-menu-button icon" title="Arvi menû"><span class="sr-only"><span>Arvi into menû contesto pe-a seçion</span></span></button></div></div><div class="section-body"><ul class="top-sites-list"><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li></ul><div class="edit-topsites-wrapper"></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="topstories"><div class="section-top-bar"><h3 class="section-title"><span class="click-target-container"><span class="click-target"><span class="icon icon-small-spacer icon-pocket"></span><span>Consegiou da Pocket</span></span><span class="click-target"></span><span class="learn-more-link-wrapper"></span></span></h3><div><button class="context-menu-button icon" title="Arvi menû"><span class="sr-only"><span>Arvi into menû contesto pe-a seçion</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul><div class="top-stories-bottom-container"><div class="wrapper-more-recommendations"></div></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="highlights"><div class="section-top-bar"><h3 class="section-title"><span class="click-target-container"><span class="click-target"><span class="icon icon-small-spacer icon-highlights"></span><span>In evidensa</span></span><span class="click-target"></span><span class="learn-more-link-wrapper"></span></span></h3><div><button class="context-menu-button icon" title="Arvi menû"><span class="sr-only"><span>Arvi into menû contesto pe-a seçion</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul></div></section></div><div class="prefs-button"><button class="icon icon-settings" title="Personalizza a teu pagina Neuvo feuggio"></button></div></div></main></div></div></div>
     <div id="snippets-container">
       <div id="snippets"></div>
--- a/browser/components/newtab/prerendered/locales/lij/activity-stream-prerendered.html
+++ b/browser/components/newtab/prerendered/locales/lij/activity-stream-prerendered.html
@@ -1,14 +1,14 @@
 <!doctype html>
 <html lang="lij" dir="ltr">
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
-    <title>Neuvo Feuggio</title>
+    <title>Neuvo feuggio</title>
     <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"><div data-reactroot=""><div class="outer-wrapper fixed-to-top"><main><div class="non-collapsible-section"><div class="search-wrapper"><div class="search-inner-wrapper"><label for="newtab-search-text" class="search-label"><span class="sr-only"><span>Çerca inta Ræ</span></span></label><input type="search" id="newtab-search-text" maxLength="256" placeholder="Çerca inta Ræ" title="Çerca inta Ræ"/><button id="searchSubmit" class="search-button" title="Çerca"><span class="sr-only"><span>Çerca</span></span></button></div></div></div><div class="body-wrapper"><div class="sections-list"><section class="collapsible-section top-sites animation-enabled" data-section-id="topsites"><div class="section-top-bar"><h3 class="section-title"><span class="click-target-container"><span class="click-target"><span class="icon icon-small-spacer icon-topsites"></span><span>I megio sciti</span></span><span class="click-target"></span><span class="learn-more-link-wrapper"></span></span></h3><div><button class="context-menu-button icon" title="Arvi menû"><span class="sr-only"><span>Arvi into menû contesto pe-a seçion</span></span></button></div></div><div class="section-body"><ul class="top-sites-list"><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a tabindex="0" draggable="true"><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Cangia sto scito"></button></div></li></ul><div class="edit-topsites-wrapper"></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="topstories"><div class="section-top-bar"><h3 class="section-title"><span class="click-target-container"><span class="click-target"><span class="icon icon-small-spacer icon-pocket"></span><span>Consegiou da Pocket</span></span><span class="click-target"></span><span class="learn-more-link-wrapper"></span></span></h3><div><button class="context-menu-button icon" title="Arvi menû"><span class="sr-only"><span>Arvi into menû contesto pe-a seçion</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul><div class="top-stories-bottom-container"><div class="wrapper-more-recommendations"></div></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="highlights"><div class="section-top-bar"><h3 class="section-title"><span class="click-target-container"><span class="click-target"><span class="icon icon-small-spacer icon-highlights"></span><span>In evidensa</span></span><span class="click-target"></span><span class="learn-more-link-wrapper"></span></span></h3><div><button class="context-menu-button icon" title="Arvi menû"><span class="sr-only"><span>Arvi into menû contesto pe-a seçion</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul></div></section></div><div class="prefs-button"><button class="icon icon-settings" title="Personalizza a teu pagina Neuvo feuggio"></button></div></div></main></div></div></div>
     <div id="snippets-container">
       <div id="snippets"></div>
--- a/browser/components/newtab/prerendered/locales/lij/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/lij/activity-stream-strings.js
@@ -1,11 +1,11 @@
 // Note - this is a generated lij file.
 window.gActivityStreamStrings = {
-  "newtab_page_title": "Neuvo Feuggio",
+  "newtab_page_title": "Neuvo feuggio",
   "header_top_sites": "I megio sciti",
   "header_highlights": "In evidensa",
   "header_recommended_by": "Consegiou da {provider}",
   "context_menu_button_sr": "Arvi into menû contesto pe {title}",
   "section_context_menu_button_sr": "Arvi into menû contesto pe-a seçion",
   "type_label_visited": "Vixitou",
   "type_label_bookmarked": "Azonto a-i segnalibbri",
   "type_label_recommended": "De tentensa",
@@ -22,32 +22,32 @@ window.gActivityStreamStrings = {
   "confirm_history_delete_p1": "Te seguo de scancelâ tutte e ripetiçioin de sta pagina da stöia?",
   "confirm_history_delete_notice_p2": "Sta açion a no se peu anulâ.",
   "menu_action_save_to_pocket": "Sarva in Pocket",
   "menu_action_delete_pocket": "Scancella da Pocket",
   "menu_action_archive_pocket": "Archivia in Pocket",
   "menu_action_show_file_mac_os": "Fanni vedde in Finder",
   "menu_action_show_file_windows": "Arvi cartella",
   "menu_action_show_file_linux": "Arvi cartella",
-  "menu_action_show_file_default": "Fanni vedde file",
-  "menu_action_open_file": "Arvi file",
+  "menu_action_show_file_default": "Mostra o schedaio",
+  "menu_action_open_file": "Arvi schedaio",
   "menu_action_copy_download_link": "Còpia indirisso òrigine",
   "menu_action_go_to_download_page": "Vanni a-a pagina de descaregamento",
   "menu_action_remove_download": "Scancella da-a stöia",
   "search_button": "Çerca",
   "search_header": "Riçerca {search_engine_name}",
   "search_web_placeholder": "Çerca inta Ræ",
   "section_disclaimer_topstories": "E stöie ciù interesanti do Web, seleçionæ in baze a quello che ti lezi. Pigiæ da Pocket, che oua o l'é parte de Mozilla.",
   "section_disclaimer_topstories_linktext": "Descòvri comme fonçionn-a.",
   "section_disclaimer_topstories_buttontext": "Va ben, ò capio",
   "prefs_home_header": "Pagina iniçiâ de Firefox",
   "prefs_home_description": "Çerni i contegnui che ti veu vedde inta pagina iniçiâ de Firefox.",
   "prefs_content_discovery_header": "Pagina iniçiâ de Firefox",
   "prefs_content_discovery_description": "Content Discovery in Firefox Home allows you to discover high-quality, relevant articles from across the web.",
-  "prefs_content_discovery_button": "Turn Off Content Discovery",
+  "prefs_content_discovery_button": "Dizabilita a descoverta de neuvi contegnui",
   "prefs_section_rows_option": "{num} riga;{num} righe",
   "prefs_search_header": "Çerca into Web",
   "prefs_topsites_description": "I sciti che ti vixiti de ciù",
   "prefs_topstories_description2": "I megio contegnui pigiæ in gio pe-a ræ, personalizæ pe ti",
   "prefs_topstories_options_sponsored_label": "Stöie sponsorizæ",
   "prefs_topstories_sponsored_learn_more": "Atre informaçioin",
   "prefs_highlights_description": "'Na seleçion di sciti che t'ê sarvou ò vixitou",
   "prefs_highlights_options_visited_label": "Pagine vixitæ",
--- a/browser/components/newtab/prerendered/locales/lij/activity-stream.html
+++ b/browser/components/newtab/prerendered/locales/lij/activity-stream.html
@@ -1,14 +1,14 @@
 <!doctype html>
 <html lang="lij" dir="ltr">
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
-    <title>Neuvo Feuggio</title>
+    <title>Neuvo feuggio</title>
     <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"><!-- Regular React Rendering --></div>
     <div id="snippets-container">
       <div id="snippets"></div>
--- a/browser/components/newtab/prerendered/locales/pt-BR/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/pt-BR/activity-stream-strings.js
@@ -62,23 +62,23 @@ window.gActivityStreamStrings = {
   "edit_topsites_button_text": "Editar",
   "edit_topsites_edit_button": "Editar este site",
   "topsites_form_add_header": "Novo site popular",
   "topsites_form_edit_header": "Editar site popular",
   "topsites_form_title_label": "Título",
   "topsites_form_title_placeholder": "Digite um título",
   "topsites_form_url_label": "URL",
   "topsites_form_image_url_label": "URL de imagem personalizada",
-  "topsites_form_url_placeholder": "Digite ou cole um URL",
+  "topsites_form_url_placeholder": "Digite ou cole uma URL",
   "topsites_form_use_image_link": "Usar uma imagem personalizada…",
   "topsites_form_preview_button": "Visualizar",
   "topsites_form_add_button": "Adicionar",
   "topsites_form_save_button": "Salvar",
   "topsites_form_cancel_button": "Cancelar",
-  "topsites_form_url_validation": "É necessário um URL válido",
+  "topsites_form_url_validation": "É necessário uma URL válida",
   "topsites_form_image_validation": "Não foi possível carregar a imagem. Tente uma URL diferente.",
   "pocket_read_more": "Tópicos populares:",
   "pocket_read_even_more": "Ver mais histórias",
   "pocket_more_reccommendations": "Mais recomendações",
   "pocket_how_it_works": "Como funciona",
   "pocket_cta_button": "Adicionar o Pocket",
   "pocket_cta_text": "Salve as histórias que você gosta no Pocket e abasteça sua mente com leituras fascinantes.",
   "highlights_empty_state": "Comece a navegar e mostraremos aqui alguns ótimos artigos, vídeos e outras páginas que você favoritou ou visitou recentemente.",
--- a/browser/components/newtab/test/browser/browser.ini
+++ b/browser/components/newtab/test/browser/browser.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 support-files =
   blue_page.html
   red_page.html
   head.js
 prefs =
   browser.newtabpage.activity-stream.debug=false
+  browser.newtabpage.activity-stream.discoverystream.endpoints=data:
 
 [browser_activity_stream_strings.js]
 [browser_as_load_location.js]
 [browser_as_render.js]
 [browser_asrouter_targeting.js]
 [browser_asrouter_trigger_listeners.js]
 [browser_discovery_styles.js]
 [browser_enabled_newtabpage.js]
--- a/browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js
+++ b/browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js
@@ -26,16 +26,17 @@ describe("CFRPageActions", () => {
     "cfr-notification-footer-text",
     "cfr-notification-footer-filled-stars",
     "cfr-notification-footer-empty-stars",
     "cfr-notification-footer-users",
     "cfr-notification-footer-spacer",
     "cfr-notification-footer-learn-more-link",
     "cfr-notification-footer-pintab-animation-container",
     "cfr-notification-footer-animation-button",
+    "cfr-notification-footer-animation-label",
   ];
   const elementClassNames = [
     "popup-notification-body-container",
   ];
 
   beforeEach(() => {
     sandbox = sinon.createSandbox();
     clock = sandbox.useFakeTimers();
--- a/browser/components/newtab/test/unit/common/Reducers.test.js
+++ b/browser/components/newtab/test/unit/common/Reducers.test.js
@@ -697,16 +697,114 @@ describe("Reducers", () => {
         loaded: true,
       });
     });
     it("should handle no data from DISCOVERY_STREAM_SPOCS_UPDATE", () => {
       const data = null;
       const state = DiscoveryStream(undefined, {type: at.DISCOVERY_STREAM_SPOCS_UPDATE, data});
       assert.deepEqual(state.spocs, INITIAL_STATE.DiscoveryStream.spocs);
     });
+    it("should not update state for empty action.data on PLACES_LINK_BLOCKED", () => {
+      const newState = DiscoveryStream(undefined, {type: at.PLACES_LINK_BLOCKED});
+      assert.equal(newState, INITIAL_STATE.DiscoveryStream);
+    });
+    it("should not update state if feeds are not loaded", () => {
+      const deleteAction = {type: at.PLACES_LINK_BLOCKED, data: {url: "foo.com"}};
+      const newState = DiscoveryStream(undefined, deleteAction);
+      assert.equal(newState, INITIAL_STATE.DiscoveryStream);
+    });
+    it("should not update state if spocs and feeds data is undefined", () => {
+      const deleteAction = {type: at.PLACES_LINK_BLOCKED, data: {url: "foo.com"}};
+      const oldState = {
+        spocs: {
+          data: {},
+          loaded: true,
+        },
+        feeds: {
+          data: {},
+          loaded: true,
+        },
+      };
+      const newState = DiscoveryStream(oldState, deleteAction);
+      assert.deepEqual(newState, oldState);
+    });
+    it("should remove the site on PLACES_LINK_BLOCKED from spocs if feeds data is empty", () => {
+      const deleteAction = {type: at.PLACES_LINK_BLOCKED, data: {url: "https://foo.com"}};
+      const oldState = {
+        spocs: {
+          data: {
+            spocs: [
+              {url: "https://foo.com"},
+              {url: "test-spoc.com"},
+            ],
+          },
+          loaded: true,
+        },
+        feeds: {
+          data: {},
+          loaded: true,
+        },
+      };
+      const newState = DiscoveryStream(oldState, deleteAction);
+      assert.deepEqual(newState.spocs.data.spocs, [{url: "test-spoc.com"}]);
+    });
+    it("should remove the site on PLACES_LINK_BLOCKED from feeds if spocs data is empty", () => {
+      const deleteAction = {type: at.PLACES_LINK_BLOCKED, data: {url: "https://foo.com"}};
+      const oldState = {
+        spocs: {
+          data: {},
+          loaded: true,
+        },
+        feeds: {
+          data: {
+            "https://foo.com/feed1": {
+              data: {
+                recommendations: [
+                  {url: "https://foo.com"},
+                  {url: "test.com"},
+                ],
+              },
+            },
+          },
+          loaded: true,
+        },
+      };
+      const newState = DiscoveryStream(oldState, deleteAction);
+      assert.deepEqual(newState.feeds.data["https://foo.com/feed1"].data.recommendations, [{url: "test.com"}]);
+    });
+    it("should remove the site on PLACES_LINK_BLOCKED from both feeds and spocs", () => {
+      const oldState = {
+        feeds: {
+          data: {
+            "https://foo.com/feed1": {
+              data: {
+                recommendations: [
+                  {url: "https://foo.com"},
+                  {url: "test.com"},
+                ],
+              },
+            },
+          },
+          loaded: true,
+        },
+        spocs: {
+          data: {
+            spocs: [
+              {url: "https://foo.com"},
+              {url: "test-spoc.com"},
+            ],
+          },
+          loaded: true,
+        },
+      };
+      const deleteAction = {type: at.PLACES_LINK_BLOCKED, data: {url: "https://foo.com"}};
+      const newState = DiscoveryStream(oldState, deleteAction);
+      assert.deepEqual(newState.spocs.data.spocs, [{url: "test-spoc.com"}]);
+      assert.deepEqual(newState.feeds.data["https://foo.com/feed1"].data.recommendations, [{url: "test.com"}]);
+    });
   });
   describe("Search", () => {
     it("should return INITIAL_STATE by default", () => {
       assert.equal(Search(undefined, {type: "some_action"}), INITIAL_STATE.Search);
     });
     it("should set hide to true on HIDE_SEARCH", () => {
       const nextState = Search(undefined, {type: "HIDE_SEARCH"});
       assert.propertyVal(nextState, "hide", true);
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx
@@ -19,21 +19,21 @@ describe("<DSLinkMenu>", () => {
   });
 
   it("should render LinkMenu when context menu button is clicked", () => {
     let button = wrapper.find(".context-menu-button");
     button.simulate("click", {preventDefault: () => {}});
     assert.equal(wrapper.find(LinkMenu).length, 1);
   });
 
-  it("should pass dispatch, onUpdate, onShow, site, options, source and index to LinkMenu", () => {
+  it("should pass dispatch, onUpdate, onShow, site, options, shouldSendImpressionStats, source and index to LinkMenu", () => {
     wrapper.find(".context-menu-button").simulate("click", {preventDefault: () => {}});
     const linkMenuProps = wrapper.find(LinkMenu).props();
-    ["dispatch", "onUpdate", "onShow", "site", "index", "options", "source"].forEach(prop => assert.property(linkMenuProps, prop));
+    ["dispatch", "onUpdate", "onShow", "site", "index", "options", "source", "shouldSendImpressionStats"].forEach(prop => assert.property(linkMenuProps, prop));
   });
 
   it("should pass through the correct menu options to LinkMenu", () => {
     wrapper.find(".context-menu-button").simulate("click", {preventDefault: () => {}});
     const linkMenuProps = wrapper.find(LinkMenu).props();
     assert.deepEqual(linkMenuProps.options,
-      ["OpenInNewWindow", "OpenInPrivateWindow"]);
+      ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"]);
   });
 });
--- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
@@ -1,24 +1,30 @@
 import {actionCreators as ac, actionTypes as at, actionUtils as au} from "common/Actions.jsm";
 import {combineReducers, createStore} from "redux";
 import {DiscoveryStreamFeed} from "lib/DiscoveryStreamFeed.jsm";
+import {GlobalOverrider} from "test/unit/utils";
 import {reducers} from "common/Reducers.jsm";
 
 const CONFIG_PREF_NAME = "discoverystream.config";
+const DUMMY_ENDPOINT = "https://getpocket.cdn.mozilla.net/dummy";
+const ENDPOINTS_PREF_NAME = "discoverystream.endpoints";
 const SPOC_IMPRESSION_TRACKING_PREF = "discoverystream.spoc.impressions";
 const REC_IMPRESSION_TRACKING_PREF = "discoverystream.rec.impressions";
 const THIRTY_MINUTES = 30 * 60 * 1000;
 const ONE_WEEK = 7 * 24 * 60 * 60 * 1000; // 1 week
 
 describe("DiscoveryStreamFeed", () => {
   let feed;
   let sandbox;
   let fetchStub;
   let clock;
+  let fakeNewTabUtils;
+  let globals;
+
   const setPref = (name, value) => {
     const action = {
       type: at.PREF_CHANGED,
       data: {
         name,
         value: typeof value === "object" ? JSON.stringify(value) : value,
       },
     };
@@ -35,27 +41,82 @@ describe("DiscoveryStreamFeed", () => {
     // Time
     clock = sinon.useFakeTimers();
 
     // Feed
     feed = new DiscoveryStreamFeed();
     feed.store = createStore(combineReducers(reducers), {
       Prefs: {
         values: {
-          [CONFIG_PREF_NAME]: JSON.stringify({enabled: false, show_spocs: false, layout_endpoint: "foo"}),
+          [CONFIG_PREF_NAME]: JSON.stringify({enabled: false, show_spocs: false, layout_endpoint: DUMMY_ENDPOINT}),
+          [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
         },
       },
     });
 
     sandbox.stub(feed, "_maybeUpdateCachedData").resolves();
+
+    globals = new GlobalOverrider();
+    fakeNewTabUtils = {
+      blockedLinks: {
+        links: [],
+        isBlocked: () => false,
+      },
+    };
+    globals.set("NewTabUtils", fakeNewTabUtils);
   });
 
   afterEach(() => {
     clock.restore();
     sandbox.restore();
+    globals.restore();
+  });
+
+  describe("#fetchFromEndpoint", () => {
+    beforeEach(() => {
+      fetchStub.resolves({
+        json: () => Promise.resolve("hi"),
+        ok: true,
+      });
+    });
+    it("should get a response", async () => {
+      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);
+
+      assert.equal(response, "hi");
+    });
+    it("should not send cookies", async () => {
+      await feed.fetchFromEndpoint(DUMMY_ENDPOINT);
+
+      assert.propertyVal(fetchStub.firstCall.args[1], "credentials", "omit");
+    });
+    it("should allow unexpected response", async () => {
+      fetchStub.resolves({ok: false});
+
+      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);
+
+      assert.equal(response, null);
+    });
+    it("should disallow unexpected endpoints", async () => {
+      feed.store.getState = () => ({
+        Prefs: {values: {[ENDPOINTS_PREF_NAME]: "https://other.site"}},
+      });
+
+      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);
+
+      assert.equal(response, null);
+    });
+    it("should allow multiple endpoints", async () => {
+      feed.store.getState = () => ({
+        Prefs: {values: {[ENDPOINTS_PREF_NAME]: `https://other.site,${DUMMY_ENDPOINT}`}},
+      });
+
+      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);
+
+      assert.equal(response, "hi");
+    });
   });
 
   describe("#loadLayout", () => {
     it("should fetch data and populate the cache if it is empty", async () => {
       const resp = {layout: ["foo", "bar"]};
       const fakeCache = {};
       sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
       sandbox.stub(feed.cache, "set").returns(Promise.resolve());
@@ -529,17 +590,90 @@ describe("DiscoveryStreamFeed", () => {
         {campaign_id: 1, item_score: 0.8, score: 0.8, min_score: 0.1},
         {campaign_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1},
         {campaign_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1},
         {campaign_id: 2, item_score: 0.6, score: 0.6, min_score: 0.1},
       ]);
     });
   });
 
-  describe("#filterSpocs", () => {
+  describe("#filterBlocked", () => {
+    it("should return initial data if spocs are empty", () => {
+      const result = feed.filterBlocked({spocs: []});
+
+      assert.equal(result.spocs.length, 0);
+    });
+    it("should return initial spocs data if links are not blocked", () => {
+      const result = feed.filterBlocked({
+        spocs: [
+          {url: "https://foo.com"},
+          {url: "test.com"},
+        ],
+      }, "spocs");
+      assert.equal(result.spocs.length, 2);
+    });
+    it("should return filtered out spocs based on blockedlist", () => {
+      fakeNewTabUtils.blockedLinks.links = [{url: "https://foo.com"}];
+      fakeNewTabUtils.blockedLinks.isBlocked = site => (fakeNewTabUtils.blockedLinks.links[0].url === site.url);
+
+      const result = feed.filterBlocked({
+        spocs: [
+          {url: "https://foo.com"},
+          {url: "test.com"},
+        ],
+      }, "spocs");
+
+      assert.lengthOf(result.spocs, 1);
+      assert.equal(result.spocs[0].url, "test.com");
+      assert.notInclude(result.spocs, fakeNewTabUtils.blockedLinks.links[0]);
+    });
+    it("should return initial recommendations data if links are not blocked", () => {
+      const result = feed.filterBlocked({
+        recommendations: [
+          {url: "https://foo.com"},
+          {url: "test.com"},
+        ],
+      }, "recommendations");
+      assert.equal(result.recommendations.length, 2);
+    });
+    it("should return filtered out recommendations based on blockedlist", () => {
+      fakeNewTabUtils.blockedLinks.links = [{url: "https://foo.com"}];
+      fakeNewTabUtils.blockedLinks.isBlocked = site => (fakeNewTabUtils.blockedLinks.links[0].url === site.url);
+
+      const result = feed.filterBlocked({
+        recommendations: [
+          {url: "https://foo.com"},
+          {url: "test.com"},
+        ],
+      }, "recommendations");
+
+      assert.lengthOf(result.recommendations, 1);
+      assert.equal(result.recommendations[0].url, "test.com");
+      assert.notInclude(result.recommendations, fakeNewTabUtils.blockedLinks.links[0]);
+    });
+    it("filterRecommendations based on blockedlist by passing feed data", () => {
+      fakeNewTabUtils.blockedLinks.links = [{url: "https://foo.com"}];
+      fakeNewTabUtils.blockedLinks.isBlocked = site => (fakeNewTabUtils.blockedLinks.links[0].url === site.url);
+
+      const result = feed.filterRecommendations({
+        data: {
+          recommendations: [
+            {url: "https://foo.com"},
+            {url: "test.com"},
+          ],
+        },
+      });
+
+      assert.lengthOf(result.data.recommendations, 1);
+      assert.equal(result.data.recommendations[0].url, "test.com");
+      assert.notInclude(result.data.recommendations, fakeNewTabUtils.blockedLinks.links[0]);
+    });
+  });
+
+  describe("#frequencyCapSpocs", () => {
     it("should return filtered out spocs based on frequency caps", () => {
       const fakeSpocs = {
         spocs: [
           {
             campaign_id: "seen",
             caps: {
               lifetime: 3,
               campaign: {
@@ -560,17 +694,17 @@ describe("DiscoveryStreamFeed", () => {
           },
         ],
       };
       const fakeImpressions = {
         "seen": [Date.now() - 1],
       };
       sandbox.stub(feed, "readImpressionsPref").returns(fakeImpressions);
 
-      const result = feed.filterSpocs(fakeSpocs);
+      const result = feed.frequencyCapSpocs(fakeSpocs);
 
       assert.equal(result.spocs.length, 1);
       assert.equal(result.spocs[0].campaign_id, "not-seen");
     });
   });
 
   describe("#isBelowFrequencyCap", () => {
     it("should return true if there are no campaign impressions", () => {