Bug 1399970 - Add Photon polish, PureComponents and bug fixes to Activity Stream. r=dmose
authorEd Lee <edilee@mozilla.com>
Thu, 14 Sep 2017 15:08:05 -0700
changeset 381060 3b514f20525296a9b6a623ba680fb027bc9e6cae
parent 381059 d7ccda988869be632fb6b3345d3efba111db7f7d
child 381061 7fc8dcc9330001298c7441c5197942d3f4ac79ea
push id95039
push userarchaeopteryx@coole-files.de
push dateFri, 15 Sep 2017 09:12:35 +0000
treeherdermozilla-inbound@c4a244ec50df [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdmose
bugs1399970, 1399686, 1399226
milestone57.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 1399970 - Add Photon polish, PureComponents and bug fixes to Activity Stream. r=dmose Also fix Bug 1399686 - `make package` broken on macOS after landing of bug 1399226 MozReview-Commit-ID: HVZ02HlV1b6
CLOBBER
browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
browser/extensions/activity-stream/data/content/activity-stream.bundle.js
browser/extensions/activity-stream/data/content/activity-stream.css
browser/extensions/activity-stream/data/content/assets/glyph-search-16.svg
browser/extensions/activity-stream/data/content/assets/glyph-settings-16.svg
browser/extensions/activity-stream/data/locales.json
browser/extensions/activity-stream/install.rdf.in
browser/extensions/activity-stream/lib/PlacesFeed.jsm
browser/extensions/activity-stream/lib/TopSitesFeed.jsm
browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1395486 - Moving files to content-accessible requires a clobber so as not to break Android packaging.
+Bug 1399970 / Bug 1399226 - Removed some now-unused icons. Clobbering to avoid `make package` failures, e.g., bug 1399686.
--- a/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
@@ -4,17 +4,17 @@
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy-Report-Only" content="script-src 'unsafe-inline'; img-src http: https: data: blob:; style-src 'unsafe-inline'; child-src 'none'; object-src 'none'; report-uri https://tiles.services.mozilla.com/v4/links/activity-stream/csp">
     <title></title>
     <link rel="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
   <body class="activity-stream">
-    <div id="root"><div class="outer-wrapper fixed-to-top" data-reactroot="" data-reactid="1" data-react-checksum="1939271514"><main data-reactid="2"><div class="search-wrapper" data-reactid="3"><label for="newtab-search-text" class="search-label" data-reactid="4"><span class="sr-only" data-reactid="5"><span data-reactid="6">Search the Web</span></span></label><input type="search" id="newtab-search-text" maxlength="256" placeholder="Search the Web" title="Search the Web" data-reactid="7"/><button id="searchSubmit" class="search-button" title=" " data-reactid="8"><span class="sr-only" data-reactid="9"><span data-reactid="10"> </span></span></button></div><section class="top-sites" data-reactid="11"><h3 class="section-title" data-reactid="12"><span class="icon icon-small-spacer icon-topsites" data-reactid="13"></span><span data-reactid="14"> </span></h3><ul class="top-sites-list" data-reactid="15"><li class="top-site-outer placeholder" data-reactid="16"><a data-reactid="17"><div class="tile" aria-hidden="true" data-reactid="18"><span class="letter-fallback" data-reactid="19"></span><div class="screenshot" style="background-image:none;" data-reactid="20"></div></div><div class="title " data-reactid="21"><span dir="auto" data-reactid="22"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="23"><a data-reactid="24"><div class="tile" aria-hidden="true" data-reactid="25"><span class="letter-fallback" data-reactid="26"></span><div class="screenshot" style="background-image:none;" data-reactid="27"></div></div><div class="title " data-reactid="28"><span dir="auto" data-reactid="29"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="30"><a data-reactid="31"><div class="tile" aria-hidden="true" data-reactid="32"><span class="letter-fallback" data-reactid="33"></span><div class="screenshot" style="background-image:none;" data-reactid="34"></div></div><div class="title " data-reactid="35"><span dir="auto" data-reactid="36"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="37"><a data-reactid="38"><div class="tile" aria-hidden="true" data-reactid="39"><span class="letter-fallback" data-reactid="40"></span><div class="screenshot" style="background-image:none;" data-reactid="41"></div></div><div class="title " data-reactid="42"><span dir="auto" data-reactid="43"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="44"><a data-reactid="45"><div class="tile" aria-hidden="true" data-reactid="46"><span class="letter-fallback" data-reactid="47"></span><div class="screenshot" style="background-image:none;" data-reactid="48"></div></div><div class="title " data-reactid="49"><span dir="auto" data-reactid="50"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="51"><a data-reactid="52"><div class="tile" aria-hidden="true" data-reactid="53"><span class="letter-fallback" data-reactid="54"></span><div class="screenshot" style="background-image:none;" data-reactid="55"></div></div><div class="title " data-reactid="56"><span dir="auto" data-reactid="57"></span></div></a></li></ul><div class="edit-topsites-wrapper" data-reactid="58"><div class="edit-topsites-button" data-reactid="59"><button class="edit" title=" " data-reactid="60"><span data-reactid="61"> </span></button></div></div></section><div class="sections-list" data-reactid="62"><section data-reactid="63"><div class="section-top-bar" data-reactid="64"><h3 class="section-title" data-reactid="65"><span class="icon icon-small-spacer icon-pocket" data-reactid="66"></span><span data-reactid="67"> </span></h3></div><ul class="section-list" style="padding:0;" data-reactid="68"><li class="card-outer placeholder" data-reactid="69"><a data-reactid="70"><div class="card" data-reactid="71"><div class="card-details no-image" data-reactid="72"><div class="card-text no-image no-host-name no-context" data-reactid="73"><h4 class="card-title" dir="auto" data-reactid="74"></h4><p class="card-description" dir="auto" data-reactid="75"></p></div><div class="card-context" data-reactid="76"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="77"><a data-reactid="78"><div class="card" data-reactid="79"><div class="card-details no-image" data-reactid="80"><div class="card-text no-image no-host-name no-context" data-reactid="81"><h4 class="card-title" dir="auto" data-reactid="82"></h4><p class="card-description" dir="auto" data-reactid="83"></p></div><div class="card-context" data-reactid="84"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="85"><a data-reactid="86"><div class="card" data-reactid="87"><div class="card-details no-image" data-reactid="88"><div class="card-text no-image no-host-name no-context" data-reactid="89"><h4 class="card-title" dir="auto" data-reactid="90"></h4><p class="card-description" dir="auto" data-reactid="91"></p></div><div class="card-context" data-reactid="92"></div></div></div></a></li></ul><div class="topic" data-reactid="93"><span data-reactid="94"><span data-reactid="95"> </span></span><ul data-reactid="96"><li data-reactid="97"><a class="topic-link" data-reactid="98"></a></li></ul></div></section><section data-reactid="99"><div class="section-top-bar" data-reactid="100"><h3 class="section-title" data-reactid="101"><span class="icon icon-small-spacer icon-highlights" data-reactid="102"></span><span data-reactid="103"> </span></h3></div><ul class="section-list" style="padding:0;" data-reactid="104"><li class="card-outer placeholder" data-reactid="105"><a data-reactid="106"><div class="card" data-reactid="107"><div class="card-details no-image" data-reactid="108"><div class="card-text no-image no-host-name no-context" data-reactid="109"><h4 class="card-title" dir="auto" data-reactid="110"></h4><p class="card-description" dir="auto" data-reactid="111"></p></div><div class="card-context" data-reactid="112"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="113"><a data-reactid="114"><div class="card" data-reactid="115"><div class="card-details no-image" data-reactid="116"><div class="card-text no-image no-host-name no-context" data-reactid="117"><h4 class="card-title" dir="auto" data-reactid="118"></h4><p class="card-description" dir="auto" data-reactid="119"></p></div><div class="card-context" data-reactid="120"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="121"><a data-reactid="122"><div class="card" data-reactid="123"><div class="card-details no-image" data-reactid="124"><div class="card-text no-image no-host-name no-context" data-reactid="125"><h4 class="card-title" dir="auto" data-reactid="126"></h4><p class="card-description" dir="auto" data-reactid="127"></p></div><div class="card-context" data-reactid="128"></div></div></div></a></li></ul></section></div><!-- react-empty: 129 --></main></div></div>
+    <div id="root"><div class="outer-wrapper fixed-to-top" data-reactroot="" data-reactid="1" data-react-checksum="544412221"><main data-reactid="2"><div class="search-wrapper" data-reactid="3"><label for="newtab-search-text" class="search-label" data-reactid="4"><span class="sr-only" data-reactid="5"><span data-reactid="6">Search the Web</span></span></label><input type="search" id="newtab-search-text" maxlength="256" placeholder="Search the Web" title="Search the Web" data-reactid="7"/><button id="searchSubmit" class="search-button" title=" " data-reactid="8"><span class="sr-only" data-reactid="9"><span data-reactid="10"> </span></span></button></div><section class="top-sites" data-reactid="11"><h3 class="section-title" data-reactid="12"><span class="icon icon-small-spacer icon-topsites" data-reactid="13"></span><span data-reactid="14"> </span></h3><ul class="top-sites-list" data-reactid="15"><li class="top-site-outer placeholder" data-reactid="16"><a data-reactid="17"><div class="tile" aria-hidden="true" data-reactid="18"><span class="letter-fallback" data-reactid="19"></span><div class="screenshot" style="background-image:none;" data-reactid="20"></div></div><div class="title " data-reactid="21"><span dir="auto" data-reactid="22"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="23"><a data-reactid="24"><div class="tile" aria-hidden="true" data-reactid="25"><span class="letter-fallback" data-reactid="26"></span><div class="screenshot" style="background-image:none;" data-reactid="27"></div></div><div class="title " data-reactid="28"><span dir="auto" data-reactid="29"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="30"><a data-reactid="31"><div class="tile" aria-hidden="true" data-reactid="32"><span class="letter-fallback" data-reactid="33"></span><div class="screenshot" style="background-image:none;" data-reactid="34"></div></div><div class="title " data-reactid="35"><span dir="auto" data-reactid="36"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="37"><a data-reactid="38"><div class="tile" aria-hidden="true" data-reactid="39"><span class="letter-fallback" data-reactid="40"></span><div class="screenshot" style="background-image:none;" data-reactid="41"></div></div><div class="title " data-reactid="42"><span dir="auto" data-reactid="43"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="44"><a data-reactid="45"><div class="tile" aria-hidden="true" data-reactid="46"><span class="letter-fallback" data-reactid="47"></span><div class="screenshot" style="background-image:none;" data-reactid="48"></div></div><div class="title " data-reactid="49"><span dir="auto" data-reactid="50"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="51"><a data-reactid="52"><div class="tile" aria-hidden="true" data-reactid="53"><span class="letter-fallback" data-reactid="54"></span><div class="screenshot" style="background-image:none;" data-reactid="55"></div></div><div class="title " data-reactid="56"><span dir="auto" data-reactid="57"></span></div></a></li></ul><div class="edit-topsites-wrapper" data-reactid="58"><div class="edit-topsites-button" data-reactid="59"><button class="edit" title=" " data-reactid="60"><span data-reactid="61"> </span></button></div></div></section><div class="sections-list" data-reactid="62"><section data-reactid="63"><div class="section-top-bar" data-reactid="64"><h3 class="section-title" data-reactid="65"><span class="icon icon-small-spacer icon-pocket" data-reactid="66"></span><span data-reactid="67"> </span></h3></div><ul class="section-list" style="padding:0;" data-reactid="68"><li class="card-outer placeholder" data-reactid="69"><a data-reactid="70"><div class="card" data-reactid="71"><div class="card-details no-image" data-reactid="72"><div class="card-text no-context no-description no-host-name no-image" data-reactid="73"><h4 class="card-title" dir="auto" data-reactid="74"></h4><p class="card-description" dir="auto" data-reactid="75"></p></div><div class="card-context" data-reactid="76"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="77"><a data-reactid="78"><div class="card" data-reactid="79"><div class="card-details no-image" data-reactid="80"><div class="card-text no-context no-description no-host-name no-image" data-reactid="81"><h4 class="card-title" dir="auto" data-reactid="82"></h4><p class="card-description" dir="auto" data-reactid="83"></p></div><div class="card-context" data-reactid="84"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="85"><a data-reactid="86"><div class="card" data-reactid="87"><div class="card-details no-image" data-reactid="88"><div class="card-text no-context no-description no-host-name no-image" data-reactid="89"><h4 class="card-title" dir="auto" data-reactid="90"></h4><p class="card-description" dir="auto" data-reactid="91"></p></div><div class="card-context" data-reactid="92"></div></div></div></a></li></ul><div class="topic" data-reactid="93"><span data-reactid="94"><span data-reactid="95"> </span></span><ul data-reactid="96"><li data-reactid="97"><a class="topic-link" data-reactid="98"></a></li></ul></div></section><section data-reactid="99"><div class="section-top-bar" data-reactid="100"><h3 class="section-title" data-reactid="101"><span class="icon icon-small-spacer icon-highlights" data-reactid="102"></span><span data-reactid="103"> </span></h3></div><ul class="section-list" style="padding:0;" data-reactid="104"><li class="card-outer placeholder" data-reactid="105"><a data-reactid="106"><div class="card" data-reactid="107"><div class="card-details no-image" data-reactid="108"><div class="card-text no-context no-description no-host-name no-image" data-reactid="109"><h4 class="card-title" dir="auto" data-reactid="110"></h4><p class="card-description" dir="auto" data-reactid="111"></p></div><div class="card-context" data-reactid="112"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="113"><a data-reactid="114"><div class="card" data-reactid="115"><div class="card-details no-image" data-reactid="116"><div class="card-text no-context no-description no-host-name no-image" data-reactid="117"><h4 class="card-title" dir="auto" data-reactid="118"></h4><p class="card-description" dir="auto" data-reactid="119"></p></div><div class="card-context" data-reactid="120"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="121"><a data-reactid="122"><div class="card" data-reactid="123"><div class="card-details no-image" data-reactid="124"><div class="card-text no-context no-description no-host-name no-image" data-reactid="125"><h4 class="card-title" dir="auto" data-reactid="126"></h4><p class="card-description" dir="auto" data-reactid="127"></p></div><div class="card-context" data-reactid="128"></div></div></div></a></li></ul></section></div><!-- react-empty: 129 --></main></div></div>
     <div id="snippets-container">
       <div id="snippets"></div>
     </div>
 <script src="resource://activity-stream/data/content/activity-stream-initial-state.js"></script>
     <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="resource://activity-stream/vendor/react.js"></script>
     <script src="resource://activity-stream/vendor/react-dom.js"></script>
     <script src="resource://activity-stream/vendor/react-intl.js"></script>
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -332,17 +332,17 @@ module.exports = g;
 /***/ (function(module, exports) {
 
 module.exports = {
   TOP_SITES_SOURCE: "TOP_SITES",
   TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
   // minimum size necessary to show a rich icon instead of a screenshot
   MIN_RICH_FAVICON_SIZE: 96,
   // minimum size necessary to show any icon in the top left corner with a screenshot
-  MIN_CORNER_FAVICON_SIZE: 32
+  MIN_CORNER_FAVICON_SIZE: 16
 };
 
 /***/ }),
 /* 6 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* This Source Code Form is subject to the terms of the Mozilla Public
@@ -804,32 +804,38 @@ const { TOP_SITES_SOURCE, TOP_SITES_CONT
 const TopSiteLink = props => {
   const { link } = props;
   const topSiteOuterClassName = `top-site-outer${props.className ? ` ${props.className}` : ""}`;
   const { tippyTopIcon, faviconSize } = link;
   let imageClassName;
   let imageStyle;
   let showSmallFavicon = false;
   let smallFaviconStyle;
+  let smallFaviconFallback;
   if (tippyTopIcon || faviconSize >= MIN_RICH_FAVICON_SIZE) {
     // styles and class names for top sites with rich icons
     imageClassName = "top-site-icon rich-icon";
     imageStyle = {
       backgroundColor: link.backgroundColor,
       backgroundImage: `url(${tippyTopIcon || link.favicon})`
     };
   } else {
     // styles and class names for top sites with screenshot + small icon in top left corner
     imageClassName = `screenshot${link.screenshot ? " active" : ""}`;
     imageStyle = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
 
-    // only show a favicon in top left if it's greater than 32x32
+    // only show a favicon in top left if it's greater than 16x16
     if (faviconSize >= MIN_CORNER_FAVICON_SIZE) {
       showSmallFavicon = true;
       smallFaviconStyle = { backgroundImage: `url(${link.favicon})` };
+    } else if (link.screenshot) {
+      // Don't show a small favicon if there is no screenshot, because that
+      // would result in two fallback icons
+      showSmallFavicon = true;
+      smallFaviconFallback = true;
     }
   }
   return React.createElement(
     "li",
     { className: topSiteOuterClassName, key: link.guid || link.url },
     React.createElement(
       "a",
       { href: link.url, onClick: props.onClick },
@@ -837,17 +843,21 @@ const TopSiteLink = props => {
         "div",
         { className: "tile", "aria-hidden": true },
         React.createElement(
           "span",
           { className: "letter-fallback" },
           props.title[0]
         ),
         React.createElement("div", { className: imageClassName, style: imageStyle }),
-        showSmallFavicon && React.createElement("div", { className: "top-site-icon default-icon", style: smallFaviconStyle })
+        showSmallFavicon && React.createElement(
+          "div",
+          { className: "top-site-icon default-icon", style: smallFaviconStyle },
+          smallFaviconFallback && props.title[0]
+        )
       ),
       React.createElement(
         "div",
         { className: `title ${link.isPinned ? "pinned" : ""}` },
         link.isPinned && React.createElement("div", { className: "icon icon-pin-small" }),
         React.createElement(
           "span",
           { dir: "auto" },
@@ -858,17 +868,17 @@ const TopSiteLink = props => {
     props.children
   );
 };
 TopSiteLink.defaultProps = {
   title: "",
   link: {}
 };
 
-class TopSite extends React.Component {
+class TopSite extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = { showContextMenu: false, activeTile: null };
     this.onLinkClick = this.onLinkClick.bind(this);
     this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
     this.onMenuUpdate = this.onMenuUpdate.bind(this);
     this.onDismissButtonClick = this.onDismissButtonClick.bind(this);
     this.onPinButtonClick = this.onPinButtonClick.bind(this);
@@ -1001,17 +1011,17 @@ module.exports.TopSitePlaceholder = TopS
 
 const React = __webpack_require__(1);
 const { injectIntl } = __webpack_require__(2);
 const ContextMenu = __webpack_require__(17);
 const { actionCreators: ac } = __webpack_require__(0);
 const linkMenuOptions = __webpack_require__(18);
 const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"];
 
-class LinkMenu extends React.Component {
+class LinkMenu extends React.PureComponent {
   getOptions() {
     const props = this.props;
     const { site, index, source } = props;
 
     // Handle special case of default site
     const propOptions = !site.isDefault ? props.options : DEFAULT_SITE_MENU_OPTIONS;
 
     const options = propOptions.map(o => linkMenuOptions[o](site, index, source)).map(option => {
@@ -1113,17 +1123,17 @@ const { PrerenderData } = __webpack_requ
 // this just uses english locale data. We can make this more sophisticated if
 // more features are needed.
 function addLocaleDataForReactIntl({ locale, textDirection }) {
   addLocaleData([{ locale, parentLocale: "en" }]);
   document.documentElement.lang = locale;
   document.documentElement.dir = textDirection;
 }
 
-class Base extends React.Component {
+class Base extends React.PureComponent {
   componentWillMount() {
     this.sendNewTabRehydrated(this.props.App);
   }
 
   componentDidMount() {
     // Request state AFTER the first render to ensure we don't cause the
     // prerendered DOM to be unmounted. Otherwise, NEW_TAB_STATE_REQUEST is
     // dispatched right after the store is ready.
@@ -1270,19 +1280,19 @@ const { perfService: perfSvc } = __webpa
  * we want to get to the closest reliable paint for measuring, and
  * setTimeout is often throttled or queued by browsers in ways that could
  * make it lag too long.
  *
  * XXX Should be made more generic by using this.props.children, or potentially
  * even split out into a higher-order component to wrap whatever.
  *
  * @class TopSitesPerfTimer
- * @extends {React.Component}
+ * @extends {React.PureComponent}
  */
-class TopSitesPerfTimer extends React.Component {
+class TopSitesPerfTimer extends React.PureComponent {
   constructor(props) {
     super(props);
     // Just for test dependency injection:
     this.perfSvc = this.props.perfSvc || perfSvc;
 
     this._sendPaintedEvent = this._sendPaintedEvent.bind(this);
     this._timestampHandled = false;
   }
@@ -1378,17 +1388,17 @@ const { FormattedMessage, injectIntl } =
 const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
 
 const TopSiteForm = __webpack_require__(16);
 const { TopSite, TopSitePlaceholder } = __webpack_require__(8);
 
 const { TOP_SITES_DEFAULT_LENGTH, TOP_SITES_SHOWMORE_LENGTH } = __webpack_require__(6);
 const { TOP_SITES_SOURCE } = __webpack_require__(5);
 
-class TopSitesEdit extends React.Component {
+class TopSitesEdit extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
       showEditModal: false,
       showAddForm: false,
       showEditForm: false,
       editIndex: -1 // Index of top site being edited
     };
@@ -1551,17 +1561,17 @@ module.exports._unconnected = TopSitesEd
 /***/ (function(module, exports, __webpack_require__) {
 
 const React = __webpack_require__(1);
 const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
 const { FormattedMessage } = __webpack_require__(2);
 
 const { TOP_SITES_SOURCE } = __webpack_require__(5);
 
-class TopSiteForm extends React.Component {
+class TopSiteForm extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
       label: props.label || "",
       url: props.url || "",
       validationError: false
     };
     this.onLabelChange = this.onLabelChange.bind(this);
@@ -1734,17 +1744,17 @@ TopSiteForm.defaultProps = {
 module.exports = TopSiteForm;
 
 /***/ }),
 /* 17 */
 /***/ (function(module, exports, __webpack_require__) {
 
 const React = __webpack_require__(1);
 
-class ContextMenu extends React.Component {
+class ContextMenu extends React.PureComponent {
   constructor(props) {
     super(props);
     this.hideContext = this.hideContext.bind(this);
   }
   hideContext() {
     this.props.onUpdate(false);
   }
   componentWillMount() {
@@ -1771,17 +1781,17 @@ class ContextMenu extends React.Componen
         "ul",
         { role: "menu", className: "context-menu-list" },
         this.props.options.map((option, i) => option.type === "separator" ? React.createElement("li", { key: i, className: "separator" }) : React.createElement(ContextMenuItem, { key: i, option: option, hideContext: this.hideContext }))
       )
     );
   }
 }
 
-class ContextMenuItem extends React.Component {
+class ContextMenuItem extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
   }
   onClick() {
     this.props.hideContext();
     this.props.option.onClick();
@@ -1887,17 +1897,17 @@ module.exports = {
     userEvent: "BLOCK"
   }),
   DeleteUrl: site => ({
     id: "menu_action_delete",
     icon: "delete",
     action: {
       type: at.DIALOG_OPEN,
       data: {
-        onConfirm: [ac.SendToMain({ type: at.DELETE_HISTORY_URL, data: site.url }), ac.UserEvent({ event: "DELETE" })],
+        onConfirm: [ac.SendToMain({ type: at.DELETE_HISTORY_URL, data: { url: site.url, forceBlock: site.bookmarkGuid } }), ac.UserEvent({ event: "DELETE" })],
         body_string_id: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"],
         confirm_button_string_id: "menu_action_delete"
       }
     },
     userEvent: "DIALOG_OPEN"
   }),
   PinTopSite: (site, index) => ({
     id: "menu_action_pin",
@@ -1946,17 +1956,17 @@ module.exports.CheckPinTopSite = (site, 
 
 
 const React = __webpack_require__(1);
 const { connect } = __webpack_require__(3);
 const { FormattedMessage, injectIntl } = __webpack_require__(2);
 const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
 const { IS_NEWTAB } = __webpack_require__(20);
 
-class Search extends React.Component {
+class Search extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);
     this.onInputMount = this.onInputMount.bind(this);
   }
 
   handleEvent(event) {
     // Also track search events with our own telemetry
@@ -2178,17 +2188,17 @@ const { actionTypes: at, actionCreators:
 /**
  * Manual migration component used to start the profile import wizard.
  * Message is presented temporarily and will go away if:
  * 1.  User clicks "No Thanks"
  * 2.  User completed the data import
  * 3.  After 3 active days
  * 4.  User clicks "Cancel" on the import wizard (currently not implemented).
  */
-class ManualMigration extends React.Component {
+class ManualMigration extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onLaunchTour = this.onLaunchTour.bind(this);
     this.onCancelTour = this.onCancelTour.bind(this);
   }
   onLaunchTour() {
     this.props.dispatch(ac.SendToMain({ type: at.MIGRATION_START }));
     this.props.dispatch(ac.UserEvent({ event: at.MIGRATION_START }));
@@ -2257,17 +2267,17 @@ const PreferencesInput = props => React.
   ),
   props.descString && React.createElement(
     "p",
     { className: "prefs-input-description" },
     getFormattedMessage(props.descString)
   )
 );
 
-class PreferencesPane extends React.Component {
+class PreferencesPane extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = { visible: false };
     this.handleClickOutside = this.handleClickOutside.bind(this);
     this.handlePrefChange = this.handlePrefChange.bind(this);
     this.handleSectionChange = this.handleSectionChange.bind(this);
     this.togglePane = this.togglePane.bind(this);
     this.onWrapperMount = this.onWrapperMount.bind(this);
@@ -2394,17 +2404,17 @@ const Card = __webpack_require__(25);
 const { PlaceholderCard } = Card;
 const Topics = __webpack_require__(27);
 const { actionCreators: ac } = __webpack_require__(0);
 
 const VISIBLE = "visible";
 const VISIBILITY_CHANGE_EVENT = "visibilitychange";
 const CARDS_PER_ROW = 3;
 
-class Section extends React.Component {
+class Section extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onInfoEnter = this.onInfoEnter.bind(this);
     this.onInfoLeave = this.onInfoLeave.bind(this);
     this.state = { infoActive: false };
   }
 
   /**
@@ -2604,17 +2614,17 @@ Section.defaultProps = {
   document: global.document,
   rows: [],
   emptyState: {},
   title: ""
 };
 
 const SectionIntl = injectIntl(Section);
 
-class Sections extends React.Component {
+class Sections extends React.PureComponent {
   render() {
     const sections = this.props.Sections;
     return React.createElement(
       "div",
       { className: "sections-list" },
       sections.filter(section => section.enabled).map(section => React.createElement(SectionIntl, _extends({ key: section.id }, section, { dispatch: this.props.dispatch })))
     );
   }
@@ -2640,17 +2650,17 @@ const { actionCreators: ac, actionTypes:
  * Card component.
  * Cards are found within a Section component and contain information about a link such
  * as preview image, page title, page description, and some context about if the page
  * was visited, bookmarked, trending etc...
  * Each Section can make an unordered list of Cards which will create one instane of
  * this class. Each card will then get a context menu which reflects the actions that
  * can be done on this Card.
  */
-class Card extends React.Component {
+class Card extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = { showContextMenu: false, activeCard: null };
     this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
     this.onMenuUpdate = this.onMenuUpdate.bind(this);
     this.onLinkClick = this.onLinkClick.bind(this);
   }
   onMenuButtonClick(event) {
@@ -2710,17 +2720,17 @@ class Card extends React.Component {
             { className: `card-details${hasImage ? "" : " no-image"}` },
             link.hostname && React.createElement(
               "div",
               { className: "card-host-name" },
               link.hostname
             ),
             React.createElement(
               "div",
-              { className: `card-text${hasImage ? "" : " no-image"}${link.hostname ? "" : " no-host-name"}${icon ? "" : " no-context"}` },
+              { className: ["card-text", icon ? "" : "no-context", link.description ? "" : "no-description", link.hostname ? "" : "no-host-name", hasImage ? "" : "no-image"].join(" ") },
               React.createElement(
                 "h4",
                 { className: "card-title", dir: "auto" },
                 link.title
               ),
               React.createElement(
                 "p",
                 { className: "card-description", dir: "auto" },
@@ -2799,32 +2809,32 @@ module.exports = {
 
 /***/ }),
 /* 27 */
 /***/ (function(module, exports, __webpack_require__) {
 
 const React = __webpack_require__(1);
 const { FormattedMessage } = __webpack_require__(2);
 
-class Topic extends React.Component {
+class Topic extends React.PureComponent {
   render() {
     const { url, name } = this.props;
     return React.createElement(
       "li",
       null,
       React.createElement(
         "a",
         { key: name, className: "topic-link", href: url },
         name
       )
     );
   }
 }
 
-class Topics extends React.Component {
+class Topics extends React.PureComponent {
   render() {
     const { topics, read_more_endpoint } = this.props;
     return React.createElement(
       "div",
       { className: "topic" },
       React.createElement(
         "span",
         null,
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -47,17 +47,17 @@ input {
     background-image: url("assets/glyph-info-16.svg"); }
   .icon.icon-import {
     background-image: url("assets/glyph-import-16.svg"); }
   .icon.icon-new-window {
     background-image: url("assets/glyph-newWindow-16.svg"); }
   .icon.icon-new-window-private {
     background-image: url("chrome://browser/skin/privateBrowsing.svg"); }
   .icon.icon-settings {
-    background-image: url("assets/glyph-settings-16.svg"); }
+    background-image: url("chrome://browser/skin/settings.svg"); }
   .icon.icon-pin {
     background-image: url("assets/glyph-pin-16.svg"); }
   .icon.icon-unpin {
     background-image: url("assets/glyph-unpin-16.svg"); }
   .icon.icon-edit {
     background-image: url("assets/glyph-edit-16.svg"); }
   .icon.icon-pocket {
     background-image: url("assets/glyph-pocket-16.svg"); }
@@ -172,17 +172,17 @@ a {
   height: 100%;
   flex-grow: 1; }
   .outer-wrapper.fixed-to-top {
     height: auto; }
 
 main {
   margin: auto;
   width: 224px;
-  padding-bottom: 120px; }
+  padding-bottom: 48px; }
   @media (min-width: 416px) {
     main {
       width: 352px; } }
   @media (min-width: 544px) {
     main {
       width: 480px; } }
   @media (min-width: 800px) {
     main {
@@ -198,19 +198,19 @@ main {
   .section-title span {
     color: #737373;
     fill: #737373;
     vertical-align: middle; }
 
 .top-sites-list {
   list-style: none;
   margin: 0;
+  margin-bottom: -18px;
   padding: 0;
-  margin-inline-end: -32px;
-  margin-bottom: -18px; }
+  margin-inline-end: -32px; }
   @media (max-width: 416px) {
     .top-sites-list :nth-child(2n+1) .context-menu {
       margin-inline-start: auto;
       margin-inline-end: auto;
       offset-inline-start: -32px;
       offset-inline-end: auto; }
     .top-sites-list :nth-child(2n) .context-menu {
       margin-inline-start: auto;
@@ -244,17 +244,17 @@ main {
   @media (min-width: 800px) and (max-width: 1024px) {
     .top-sites-list :nth-child(6n+5) .context-menu {
       margin-inline-start: auto;
       margin-inline-end: 5px;
       offset-inline-start: auto;
       offset-inline-end: 0; } }
   .top-sites-list li {
     display: inline-block;
-    margin: 0 0 18px;
+    margin: 0 0 8px;
     margin-inline-end: 32px; }
   .top-sites-list .top-site-outer {
     position: relative; }
     .top-sites-list .top-site-outer > a {
       display: block;
       color: inherit;
       outline: none; }
       .top-sites-list .top-site-outer > a.active .tile, .top-sites-list .top-site-outer > a:focus .tile {
@@ -335,17 +335,21 @@ main {
       width: 100%;
       background-size: 96px; }
     .top-sites-list .top-site-outer .default-icon {
       z-index: 1;
       top: -6px;
       offset-inline-start: -6px;
       height: 42px;
       width: 42px;
-      background-size: 32px; }
+      background-size: 32px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 20px; }
     .top-sites-list .top-site-outer .title {
       font: message-box;
       height: 30px;
       line-height: 30px;
       text-align: center;
       width: 96px;
       position: relative; }
       .top-sites-list .top-site-outer .title .icon {
@@ -383,16 +387,20 @@ main {
         border: 0;
         border-right: 1px solid #B1B1B3;
         background-color: #FFF;
         cursor: pointer;
         height: 100%;
         width: 25px; }
         .top-sites-list .top-site-outer .edit-menu button:hover {
           background-color: #EDEDF0; }
+        .top-sites-list .top-site-outer .edit-menu button:first-child:dir(ltr), .top-sites-list .top-site-outer .edit-menu button:last-child:dir(rtl) {
+          width: 30px; }
+        .top-sites-list .top-site-outer .edit-menu button:last-child:dir(ltr), .top-sites-list .top-site-outer .edit-menu button:first-child:dir(rtl) {
+          width: 28px; }
         .top-sites-list .top-site-outer .edit-menu button:last-child:dir(ltr) {
           border-right: 0; }
         .top-sites-list .top-site-outer .edit-menu button:first-child:dir(rtl) {
           border-right: 0; }
     .top-sites-list .top-site-outer:hover .edit-menu, .top-sites-list .top-site-outer:focus .edit-menu, .top-sites-list .top-site-outer.active .edit-menu {
       transform: scale(1);
       opacity: 1; }
 
@@ -410,17 +418,18 @@ main {
     .edit-topsites-wrapper .edit-topsites-button button:focus {
       background: #EDEDF0;
       border-bottom: dotted 1px #737373; }
 
 .edit-topsites-wrapper .modal {
   offset-inline-start: -31px;
   position: absolute;
   top: -29px;
-  width: calc(100% + 62px); }
+  width: calc(100% + 62px);
+  box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1); }
 
 .edit-topsites-wrapper .edit-topsites-inner-wrapper {
   margin: 0;
   padding: 15px 30px; }
 
 .edit-topsites-wrapper .show-more,
 .edit-topsites-wrapper .show-less {
   background-position: left 10px center;
@@ -507,17 +516,18 @@ main {
     background-image: url("assets/glyph-info-option-12.svg");
     background-size: 12px 12px;
     background-repeat: no-repeat;
     background-position: center;
     fill: rgba(12, 12, 13, 0.6);
     -moz-context-properties: fill;
     height: 16px;
     width: 16px;
-    display: inline-block; }
+    display: inline-block;
+    margin-bottom: -2px; }
   .sections-list .section-top-bar .info-option-icon[aria-expanded="true"] {
     fill: rgba(12, 12, 13, 0.8); }
   .sections-list .section-top-bar .section-info-option .info-option {
     visibility: hidden;
     opacity: 0;
     transition: visibility 0.2s, opacity 0.2s ease-out;
     transition-delay: 0.5s; }
   .sections-list .section-top-bar .info-option-icon[aria-expanded="true"] + .info-option {
@@ -527,21 +537,22 @@ main {
   .sections-list .section-top-bar .info-option {
     z-index: 9999;
     position: absolute;
     background: #FFF;
     border: 1px solid #D7D7DB;
     border-radius: 3px;
     font-size: 13px;
     line-height: 120%;
-    margin-inline-end: -1px;
+    margin-inline-end: -13px;
     offset-inline-end: 0;
     top: 20px;
     width: 320px;
     padding: 24px;
+    box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
     -moz-user-select: none; }
   .sections-list .section-top-bar .info-option-header {
     font-size: 15px;
     font-weight: 600; }
   .sections-list .section-top-bar .info-option-body {
     margin: 0;
     margin-top: 12px; }
   .sections-list .section-top-bar .info-option-link {
@@ -650,47 +661,51 @@ main {
   display: flex;
   position: relative;
   margin: 0 0 40px;
   width: 100%;
   height: 36px; }
   .search-wrapper input {
     border: 0;
     box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
+    border: 1px solid rgba(0, 0, 0, 0.15);
+    box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
+    border-radius: 3px;
     border-radius: 4px;
     color: inherit;
     padding: 0;
     padding-inline-end: 36px;
     padding-inline-start: 35px;
-    width: 100%; }
+    width: 100%;
+    font-size: 15px; }
     .search-wrapper input:focus {
       border-color: #0060DF;
       box-shadow: 0 0 0 2px #0060DF;
       z-index: 1; }
     .search-wrapper input:focus + .search-button {
       z-index: 1;
       background-color: #0060DF;
       background-image: url("chrome://browser/skin/forward.svg");
       fill: #FFF;
       -moz-context-properties: fill; }
   .search-wrapper .search-label {
-    background: url("assets/glyph-search-16.svg") no-repeat center center/20px;
-    fill: rgba(12, 12, 13, 0.6);
+    background: url("chrome://browser/skin/find.svg") no-repeat 12px center/16px;
+    fill: rgba(12, 12, 13, 0.4);
     -moz-context-properties: fill;
     position: absolute;
     offset-inline-start: 0;
     height: 100%;
     width: 35px;
     z-index: 2; }
   .search-wrapper .search-button {
     background: url("chrome://browser/skin/forward.svg") no-repeat center center;
     border-radius: 0 3px 3px 0;
     border: 0;
     width: 36px;
-    fill: rgba(12, 12, 13, 0.6);
+    fill: rgba(12, 12, 13, 0.4);
     -moz-context-properties: fill;
     background-size: 16px 16px;
     height: 100%;
     offset-inline-end: 0;
     position: absolute; }
     .search-wrapper .search-button:hover {
       z-index: 1;
       background-color: #0060DF;
@@ -732,22 +747,24 @@ main {
         line-height: 16px;
         display: flex;
         align-items: center; }
         .context-menu > ul > li > a:hover, .context-menu > ul > li > a:focus {
           background: #0060DF;
           color: #FFF; }
           .context-menu > ul > li > a:hover a, .context-menu > ul > li > a:focus a {
             color: #0C0C0D; }
+          .context-menu > ul > li > a:hover .icon, .context-menu > ul > li > a:focus .icon {
+            fill: #FFF; }
           .context-menu > ul > li > a:hover:hover, .context-menu > ul > li > a:hover:focus, .context-menu > ul > li > a:focus:hover, .context-menu > ul > li > a:focus:focus {
             color: #FFF; }
 
 .prefs-pane {
   color: #4A4A4F;
-  font-size: 15px;
+  font-size: 14px;
   line-height: 21px; }
   .prefs-pane .sidebar {
     background: #FFF;
     border-left: 1px solid #D7D7DB;
     box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
     height: 100%;
     offset-inline-end: 0;
     overflow-y: auto;
@@ -758,54 +775,54 @@ main {
     transition-property: transform;
     width: 400px;
     z-index: 12000; }
     .prefs-pane .sidebar.hidden {
       transform: translateX(100%); }
       .prefs-pane .sidebar.hidden:dir(rtl) {
         transform: translateX(-100%); }
     .prefs-pane .sidebar h1 {
-      font-size: 24px;
+      font-size: 21px;
       margin: 0;
       padding-top: 20px; }
   .prefs-pane hr {
     border: 0;
     border-bottom: 1px solid #D7D7DB;
-    margin: 28px 0; }
+    margin: 20px 0; }
   .prefs-pane .prefs-modal-inner-wrapper {
     padding-bottom: 100px; }
     .prefs-pane .prefs-modal-inner-wrapper section {
       margin: 20px 0; }
       .prefs-pane .prefs-modal-inner-wrapper section p {
         margin: 5px 0 5px 30px; }
       .prefs-pane .prefs-modal-inner-wrapper section label {
         display: inline-block;
         position: relative;
         width: 100%; }
         .prefs-pane .prefs-modal-inner-wrapper section label input {
           offset-inline-start: -30px;
           position: absolute;
           top: 0; }
       .prefs-pane .prefs-modal-inner-wrapper section > label {
-        font-size: 17px;
+        font-size: 16px;
         font-weight: bold;
         line-height: 19px; }
     .prefs-pane .prefs-modal-inner-wrapper .options {
       background: #F9F9FA;
       border: 1px solid #D7D7DB;
       border-radius: 2px;
       margin: -10px 0 20px;
       margin-inline-start: 30px;
       padding: 10px; }
       .prefs-pane .prefs-modal-inner-wrapper .options label {
         background-position-x: 35px;
         background-position-y: 2.5px;
         background-repeat: no-repeat;
         display: inline-block;
-        font-size: 13px;
+        font-size: 14px;
         font-weight: normal;
         height: auto;
         line-height: 21px;
         width: 100%; }
         .prefs-pane .prefs-modal-inner-wrapper .options label:dir(rtl) {
           background-position-x: 217px; }
       .prefs-pane .prefs-modal-inner-wrapper .options [type='checkbox']:not(:checked) + label,
       .prefs-pane .prefs-modal-inner-wrapper .options [type='checkbox']:checked + label {
@@ -1014,16 +1031,19 @@ main {
     .card-outer .card-text.no-host-name, .card-outer .card-text.no-context {
       max-height: 97px; }
     .card-outer .card-text.no-image.no-host-name, .card-outer .card-text.no-image.no-context {
       max-height: 211px; }
     .card-outer .card-text.no-host-name.no-context {
       max-height: 116px; }
     .card-outer .card-text.no-image.no-host-name.no-context {
       max-height: 230px; }
+    .card-outer .card-text:not(.no-description) .card-title {
+      max-height: 57px;
+      overflow: hidden; }
   .card-outer .card-host-name {
     color: #737373;
     font-size: 10px;
     padding-bottom: 4px;
     text-transform: uppercase; }
   .card-outer .card-title {
     margin: 0 0 2px;
     font-size: 14px;
@@ -1031,17 +1051,17 @@ main {
     line-height: 19px; }
   .card-outer .card-description {
     font-size: 12px;
     margin: 0;
     word-wrap: break-word;
     overflow: hidden;
     line-height: 19px; }
   .card-outer .card-context {
-    padding: 16px 16px 8px 14px;
+    padding: 12px 16px 12px 14px;
     position: absolute;
     bottom: 0;
     left: 0;
     right: 0;
     color: #737373;
     font-size: 11px;
     display: flex; }
   .card-outer .card-context-icon {
deleted file mode 100644
--- a/browser/extensions/activity-stream/data/content/assets/glyph-search-16.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M30.989 28.571l-2.2 2.2-9.533-9.534a11.436 11.436 0 1 1 2.2-2.2zM12.37 3.745a8.407 8.407 0 1 0 8.406 8.406 8.406 8.406 0 0 0-8.406-8.406z" fill="context-fill"/></svg>
\ No newline at end of file
deleted file mode 100644
--- a/browser/extensions/activity-stream/data/content/assets/glyph-settings-16.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill="none" stroke="context-fill" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M8 1v3m0 8v3m-3.5-3.5l-1.45 1.45m9.9-9.9L11 5M1 8h3m8 0h3"/><circle cx="8" cy="8" r="4"/><path d="M3.05 3.05L5 5m6 6l1.95 1.95"/></g></svg>
\ No newline at end of file
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -587,16 +587,17 @@
     "settings_pane_button_label": "Personelait ho pajenn Ivinell Nevez",
     "settings_pane_header": "Gwellvezioù an ivinell nevez"
   },
   "ca": {
     "newtab_page_title": "Pestanya nova",
     "default_label_loading": "S'està carregant…",
     "header_top_sites": "Llocs principals",
     "header_stories": "Articles populars",
+    "header_highlights": "Destacats",
     "header_visit_again": "Torneu a visitar",
     "header_bookmarks": "Adreces d'interès recents",
     "header_recommended_by": "Recomanat per {provider}",
     "header_bookmarks_placeholder": "Encara no teniu cap adreça d'interès.",
     "header_stories_from": "de",
     "type_label_visited": "Visitats",
     "type_label_bookmarked": "A les adreces d'interès",
     "type_label_synced": "Sincronitzat des d'un altre dispositiu",
@@ -618,37 +619,41 @@
     "confirm_history_delete_notice_p2": "Aquesta acció no es pot desfer.",
     "menu_action_save_to_pocket": "Desa al Pocket",
     "search_for_something_with": "Cerca {search_term} amb:",
     "search_button": "Cerca",
     "search_header": "Cerca de {search_engine_name}",
     "search_web_placeholder": "Cerca al web",
     "search_settings": "Canvia els paràmetres de cerca",
     "section_info_option": "Informació",
+    "section_info_send_feedback": "Doneu la vostra opinió",
+    "section_info_privacy_notice": "Avís de privadesa",
     "welcome_title": "Us donem la benvinguda a la pestanya nova",
     "welcome_body": "El Firefox utilitzarà aquest espai per mostrar-vos les adreces d'interès, els articles i els vídeos més rellevants, així com les pàgines que heu visitat recentment, per tal que hi pugueu accedir fàcilment.",
     "welcome_label": "S'estan identificant els vostres llocs destacats",
     "time_label_less_than_minute": "<1 m",
     "time_label_minute": "{number} m",
     "time_label_hour": "{number} h",
     "time_label_day": "{number} d",
     "settings_pane_button_label": "Personalitzeu la pàgina de pestanya nova",
     "settings_pane_header": "Preferències de pestanya nova",
-    "settings_pane_body": "Trieu què voleu veure quan obriu una pestanya nova.",
+    "settings_pane_body2": "Trieu què voleu veure en aquesta pàgina.",
     "settings_pane_search_header": "Cerca",
     "settings_pane_search_body": "Cerca al web des de la pestanya nova.",
     "settings_pane_topsites_header": "Llocs principals",
     "settings_pane_topsites_body": "Accediu als llocs web que visiteu més sovint.",
     "settings_pane_topsites_options_showmore": "Mostra dues files",
     "settings_pane_bookmarks_header": "Adreces d'interès recents",
     "settings_pane_bookmarks_body": "Les adreces d'interès que aneu creant, en un lloc còmode.",
     "settings_pane_visit_again_header": "Torneu a visitar",
     "settings_pane_visit_again_body": "El Firefox us mostrarà parts del vostre historial de navegació que potser us agradaria recordar o tornar a visitar.",
-    "settings_pane_pocketstories_header": "Articles populars",
-    "settings_pane_pocketstories_body": "El Pocket, membre de la família Mozilla, us permet accedir a contingut d'alta qualitat que d'altra manera potser no trobaríeu.",
+    "settings_pane_highlights_header": "Destacats",
+    "settings_pane_highlights_options_bookmarks": "Adreces d'interès",
+    "settings_pane_highlights_options_visited": "Llocs visitats",
+    "settings_pane_snippets_header": "Retalls",
     "settings_pane_done_button": "Fet",
     "edit_topsites_button_text": "Edita",
     "edit_topsites_button_label": "Personalitzeu la secció Llocs principals",
     "edit_topsites_showmore_button": "Mostra'n més",
     "edit_topsites_showless_button": "Mostra'n menys",
     "edit_topsites_done_button": "Fet",
     "edit_topsites_pin_button": "Fixa aquest lloc",
     "edit_topsites_unpin_button": "No fixis aquest lloc",
@@ -661,20 +666,18 @@
     "topsites_form_url_placeholder": "Escriviu o enganxeu un URL",
     "topsites_form_add_button": "Afegeix",
     "topsites_form_save_button": "Desa",
     "topsites_form_cancel_button": "Cancel·la",
     "topsites_form_url_validation": "Es necessita un URL vàlid",
     "pocket_read_more": "Temes populars:",
     "pocket_read_even_more": "Mostra més articles",
     "pocket_feedback_header": "El millor del web, seleccionat per més de 25 milions de persones.",
-    "pocket_feedback_body": "El Pocket, membre de la família Mozilla, us permet accedir a contingut d'alta qualitat que d'altra manera potser no trobaríeu.",
-    "pocket_send_feedback": "Doneu la vostra opinió",
     "topstories_empty_state": "Ja esteu al dia. Torneu més tard per veure més articles populars de {provider}. No podeu esperar? Trieu un tema popular per descobrir els articles més interessants de tot el web.",
-    "manual_migration_explanation": "Proveu el Firefox amb els vostres llocs preferits i les adreces d'interès d'un altre navegador.",
+    "manual_migration_explanation2": "Proveu el Firefox amb les adreces d'interès, l'historial i les contrasenyes d'un altre navegador.",
     "manual_migration_cancel_button": "No, gràcies",
     "manual_migration_import_button": "Importa-ho ara"
   },
   "cak": {
     "newtab_page_title": "K'ak'a' ruwi'",
     "default_label_loading": "Tajin nusamajij…",
     "header_top_sites": "Utziläj taq Ruxaq K'amaya'l",
     "header_stories": "Utziläj taq B'anob'äl",
@@ -3134,16 +3137,26 @@
     "welcome_title": "Բարի գալուստ նոր ներդիր",
     "welcome_body": "Firefox-ը կօգտագործի այս բացատը՝ ցուցադրելու ձեզ համար առավել կարևոր էջանիշերը, հոդվածները և ձեր այցելած վերջին էջերը, որպեսզի հեշտությամբ վերադառնաք դրանց:",
     "welcome_label": "Նույնացնում է ձեր գունանշումը",
     "time_label_less_than_minute": "<1 ր",
     "time_label_minute": "{number} ր",
     "time_label_hour": "{number} ժ",
     "time_label_day": "{number} օր"
   },
+  "ia": {
+    "newtab_page_title": "Nove scheda",
+    "default_label_loading": "Cargamento…",
+    "header_top_sites": "Sitos principal",
+    "header_stories": "Articulos popular",
+    "header_highlights": "In evidentia",
+    "header_visit_again": "Visita de novo",
+    "header_bookmarks": "Marcatores recente",
+    "header_recommended_by": "Recommendate per {provider}"
+  },
   "id": {
     "newtab_page_title": "Tab Baru",
     "default_label_loading": "Memuat…",
     "header_top_sites": "Situs Teratas",
     "header_stories": "Cerita Utama",
     "header_highlights": "Sorotan",
     "header_visit_again": "Kunjungi Lagi",
     "header_bookmarks": "Markah Terbaru",
--- a/browser/extensions/activity-stream/install.rdf.in
+++ b/browser/extensions/activity-stream/install.rdf.in
@@ -3,17 +3,17 @@
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>activity-stream@mozilla.org</em:id>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
-    <em:version>2017.09.14.0590-7fa80d82</em:version>
+    <em:version>2017.09.14.1322-706b3303</em:version>
     <em:name>Activity Stream</em:name>
     <em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -239,19 +239,24 @@ class PlacesFeed {
         NewTabUtils.activityStreamLinks.blockURL({url: action.data});
         break;
       case at.BOOKMARK_URL:
         NewTabUtils.activityStreamLinks.addBookmark(action.data, action._target.browser);
         break;
       case at.DELETE_BOOKMARK_BY_ID:
         NewTabUtils.activityStreamLinks.deleteBookmark(action.data);
         break;
-      case at.DELETE_HISTORY_URL:
-        NewTabUtils.activityStreamLinks.deleteHistoryEntry(action.data);
+      case at.DELETE_HISTORY_URL: {
+        const {url, forceBlock} = action.data;
+        NewTabUtils.activityStreamLinks.deleteHistoryEntry(url);
+        if (forceBlock) {
+          NewTabUtils.activityStreamLinks.blockURL({url});
+        }
         break;
+      }
       case at.OPEN_NEW_WINDOW:
         this.openNewWindow(action);
         break;
       case at.OPEN_PRIVATE_WINDOW:
         this.openNewWindow(action, true);
         break;
       case at.SAVE_TO_POCKET:
         Pocket.savePage(action._target.browser, action.data.site.url, action.data.site.title);
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -101,51 +101,57 @@ this.TopSitesFeed = class TopSitesFeed {
       if (link && link.screenshot) {
         currentScreenshots[link.url] = link.screenshot;
       }
     }
 
     // Now, get a tippy top icon, a rich icon, or screenshot for every item
     for (let link of links) {
       if (!link) { continue; }
-
-      // Check for tippy top icon or a rich icon.
-      link = this._tippyTopProvider.processSite(link);
-      if (link.tippyTopIcon || link.faviconSize >= MIN_FAVICON_SIZE) { continue; }
-
-      // If no tippy top, then we get a screenshot.
-      if (currentScreenshots[link.url]) {
-        link.screenshot = currentScreenshots[link.url];
-      } else {
-        this.getScreenshot(link.url);
-      }
+      this._fetchIcon(link, currentScreenshots);
     }
     const newAction = {type: at.TOP_SITES_UPDATED, data: links};
 
     if (target) {
       // Send an update to content so the preloaded tab can get the updated content
       this.store.dispatch(ac.SendToContent(newAction, target));
     } else {
       // Broadcast an update to all open content pages
       this.store.dispatch(ac.BroadcastToContent(newAction));
     }
     this.lastUpdated = Date.now();
   }
+  _fetchIcon(link, screenshotCache = {}) {
+    // Check for tippy top icon or a rich icon.
+    this._tippyTopProvider.processSite(link);
+    if (!link.tippyTopIcon && (!link.favicon || link.faviconSize < MIN_FAVICON_SIZE)) {
+      // If no tippy top, then we get a screenshot.
+      if (screenshotCache[link.url]) {
+        link.screenshot = screenshotCache[link.url];
+      } else {
+        this.getScreenshot(link.url);
+      }
+    }
+  }
   _getPinnedWithData(links) {
     // Augment the pinned links with any other extra data we have for them already in the store.
     // Alternatively you can pass in some links that you know have data you want the pinned links
     // to also have. This is useful for start up to make sure pinned links have favicons
     // (See github ticket #3428 fore more details)
-    let originalLinks = links ? links : this.store.getState().TopSites.rows;
+    const originalLinks = links || this.store.getState().TopSites.rows;
     const pinned = NewTabUtils.pinnedLinks.links;
     return pinned.map(pinnedLink => {
       if (pinnedLink) {
         const hostname = shortURL(pinnedLink);
         const originalLink = originalLinks.find(link => link && link.url === pinnedLink.url);
-        return Object.assign(pinnedLink, originalLink || {hostname});
+        // If it's a new link then it won't have an icon, so fetch one
+        if (!originalLink) {
+          this._fetchIcon(pinnedLink);
+        }
+        return Object.assign(originalLink || {hostname}, pinnedLink);
       }
       return pinnedLink;
     });
   }
   _broadcastPinnedSitesUpdated() {
     this.store.dispatch(ac.BroadcastToContent({
       type: at.PINNED_SITES_UPDATED,
       data: this._getPinnedWithData()
--- a/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
@@ -160,22 +160,37 @@ this.TopStoriesFeed = class TopStoriesFe
   // If personalization is turned on we have to rotate stories on the client.
   // An item can only be on top for two iterations (1hr) before it gets moved
   // to the end. This will later be improved based on interactions/impressions.
   rotate(items) {
     if (!this.personalized || items.length <= 3) {
       return items;
     }
 
+    if (!this.topItems) {
+      this.topItems = new Map();
+    }
+
+    // This avoids an infinite recursion if for some reason the feed stops
+    // changing. Otherwise, there's a chance we'd be rotating forever to
+    // find an item we haven't displayed on top yet.
+    if (this.topItems.size >= items.length) {
+      this.topItems.clear();
+    }
+
     const guid = items[0].guid;
-    if (!this.topItem || !(guid in this.topItem)) {
-      this.topItem = {[guid]: 0};
-    } else if (++this.topItem[guid] === 2) {
-      items.push(items.shift());
-      this.topItem = {[items[0].guid]: 0};
+    if (!this.topItems.has(guid)) {
+      this.topItems.set(guid, 0);
+    } else {
+      const val = this.topItems.get(guid) + 1;
+      this.topItems.set(guid, val);
+      if (val >= 2) {
+        items.push(items.shift());
+        this.rotate(items);
+      }
     }
     return items;
   }
 
   getApiKeyFromPref(apiKeyPref) {
     if (!apiKeyPref) {
       return apiKeyPref;
     }
--- a/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
@@ -93,18 +93,24 @@ describe("PlacesFeed", () => {
       feed.onAction({type: at.BOOKMARK_URL, data, _target});
       assert.calledWith(global.NewTabUtils.activityStreamLinks.addBookmark, data, _target.browser);
     });
     it("should delete a bookmark on DELETE_BOOKMARK_BY_ID", () => {
       feed.onAction({type: at.DELETE_BOOKMARK_BY_ID, data: "g123kd"});
       assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteBookmark, "g123kd");
     });
     it("should delete a history entry on DELETE_HISTORY_URL", () => {
-      feed.onAction({type: at.DELETE_HISTORY_URL, data: "guava.com"});
+      feed.onAction({type: at.DELETE_HISTORY_URL, data: {url: "guava.com", forceBlock: null}});
       assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com");
+      assert.notCalled(global.NewTabUtils.activityStreamLinks.blockURL);
+    });
+    it("should delete a history entry on DELETE_HISTORY_URL and force a site to be blocked if specified", () => {
+      feed.onAction({type: at.DELETE_HISTORY_URL, data: {url: "guava.com", forceBlock: "g123kd"}});
+      assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com");
+      assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {url: "guava.com"});
     });
     it("should call openNewWindow with the correct url on OPEN_NEW_WINDOW", () => {
       sinon.stub(feed, "openNewWindow");
       const openWindowAction = {type: at.OPEN_NEW_WINDOW, data: {url: "foo.com"}};
       feed.onAction(openWindowAction);
       assert.calledWith(feed.openNewWindow, openWindowAction);
     });
     it("should call openNewWindow with the correct url and privacy args on OPEN_PRIVATE_WINDOW", () => {
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -282,63 +282,78 @@ describe("Top Sites Feed", () => {
       sandbox.stub(feed, "getScreenshot");
       await feed.refresh(action);
       const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
 
       assert.calledOnce(feed.store.dispatch);
       assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
       assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, reference);
     });
-    it("should reuse screenshots for existing links, and call feed.getScreenshot for others", async () => {
-      sandbox.stub(feed, "getScreenshot");
+    it("should call _fetchIcon for each link and pass in existing screenshots", async () => {
       feed.store.state.TopSites.rows = [{url: FAKE_LINKS[0].url, screenshot: "foo.jpg"}];
+      const expectedScreenshotCache = {};
+      expectedScreenshotCache[FAKE_LINKS[0].url] = "foo.jpg";
+      sinon.spy(feed, "_fetchIcon");
       await feed.refresh(action);
-
       const results = feed.store.dispatch.firstCall.args[0].data;
-
+      assert.callCount(feed._fetchIcon, results.length);
       results.forEach(link => {
-        if (link.url === FAKE_LINKS[0].url) {
-          assert.equal(link.screenshot, "foo.jpg");
-        } else {
-          assert.calledWith(feed.getScreenshot, link.url);
-        }
+        assert.calledWith(feed._fetchIcon, link, expectedScreenshotCache);
       });
     });
     it("should handle empty slots in the resulting top sites array", async () => {
       links = [FAKE_LINKS[0]];
       fakeNewTabUtils.pinnedLinks.links = [null, null, FAKE_LINKS[1], null, null, null, null, null, FAKE_LINKS[2]];
       sandbox.stub(feed, "getScreenshot");
       await feed.refresh(action);
       assert.calledOnce(feed.store.dispatch);
     });
-    it("should skip getting screenshot if there is a tippy top icon", async () => {
+  });
+  describe("#_fetchIcon", () => {
+    it("should reuse screenshots for existing links, and call feed.getScreenshot for others", () => {
+      sandbox.stub(feed, "getScreenshot");
+      const screenshotCache = {};
+      screenshotCache[FAKE_LINKS[0].url] = "foo.jpg";
+      screenshotCache[FAKE_LINKS[1].url] = "bar.png";
+
+      feed._fetchIcon(FAKE_LINKS[0], screenshotCache);
+      assert.notCalled(feed.getScreenshot);
+      assert.propertyVal(FAKE_LINKS[0], "screenshot", "foo.jpg");
+
+      feed._fetchIcon(FAKE_LINKS[1], screenshotCache);
+      assert.notCalled(feed.getScreenshot);
+      assert.propertyVal(FAKE_LINKS[1], "screenshot", "bar.png");
+
+      feed._fetchIcon(FAKE_LINKS[2], screenshotCache);
+      assert.calledOnce(feed.getScreenshot);
+      assert.calledWith(feed.getScreenshot, FAKE_LINKS[2].url);
+    });
+    it("should skip getting a screenshot if there is a tippy top icon", () => {
       sandbox.stub(feed, "getScreenshot");
       feed._tippyTopProvider.processSite = site => {
         site.tippyTopIcon = "icon.png";
         site.backgroundColor = "#fff";
         return site;
       };
-      await feed.refresh(action);
-      assert.calledOnce(feed.store.dispatch);
+      const link = {url: "example.com"};
+      feed._fetchIcon(link);
+      assert.propertyVal(link, "tippyTopIcon", "icon.png");
+      assert.notProperty(link, "screenshot");
       assert.notCalled(feed.getScreenshot);
     });
-    it("should skip getting screenshot if there is an icon of size greater than 96x96 and no tippy top", async () => {
+    it("should skip getting a screenshot if there is an icon of size greater than 96x96 and no tippy top", () => {
       sandbox.stub(feed, "getScreenshot");
-      feed.getLinksWithDefaults = () => [{
+      const link = {
         url: "foo.com",
         favicon: "data:foo",
         faviconSize: 196
-      }];
-      feed._tippyTopProvider.processSite = site => {
-        site.tippyTopIcon = null;
-        site.backgroundColor = null;
-        return site;
       };
-      await feed.refresh(action);
-      assert.calledOnce(feed.store.dispatch);
+      feed._fetchIcon(link);
+      assert.notProperty(link, "tippyTopIcon");
+      assert.notProperty(link, "screenshot");
       assert.notCalled(feed.getScreenshot);
     });
   });
   describe("getScreenshot", () => {
     it("should call Screenshots.getScreenshotForURL with the right url", async () => {
       const url = "foo.com";
       await feed.getScreenshot(url);
       assert.calledWith(fakeScreenshot.getScreenshotForURL, url);
@@ -412,20 +427,42 @@ describe("Top Sites Feed", () => {
     });
     it("should compare against links if available, instead of getting from store", () => {
       const frecentSite = {url: "foo.com", faviconSize: 32, favicon: "favicon.png"};
       const pinnedSite1 = {url: "bar.com"};
       const pinnedSite2 = {url: "foo.com"};
       fakeNewTabUtils.pinnedLinks.links = [pinnedSite1, pinnedSite2];
       feed.store = {getState() { return {TopSites: {rows: sinon.spy()}}; }};
       let result = feed._getPinnedWithData([frecentSite]);
-      assert.deepEqual(result[0], pinnedSite1);
-      assert.deepEqual(result[1], Object.assign({}, frecentSite, pinnedSite2));
+      assert.include(result[0], pinnedSite1);
+      assert.include(result[1], Object.assign({}, frecentSite, pinnedSite2));
       assert.notCalled(feed.store.getState().TopSites.rows);
     });
+    it("should fetch an icon on TOP_SITES_PIN and TOP_SITES_ADD for new urls", () => {
+      feed.store.getState = () => ({TopSites: {rows: FAKE_LINKS}});
+      fakeNewTabUtils.pinnedLinks = {
+        links: new Array(6).fill(null),
+        pin(site, index) {
+          this.links[index] = site;
+        }
+      };
+      sinon.spy(feed, "_fetchIcon");
+
+      const pinExistingAction = {type: at.TOP_SITES_PIN, data: {site: FAKE_LINKS[4], index: 4}};
+      const addAction = {type: at.TOP_SITES_ADD, data: {site: {url: "foo.com"}}};
+      const pinNewAction = {type: at.TOP_SITES_PIN, data: {site: {url: "bar.net"}, index: 0}};
+      feed.onAction(pinExistingAction);
+      feed.onAction(addAction);
+      feed.onAction(pinNewAction);
+
+      assert.calledTwice(feed._fetchIcon);
+      assert.neverCalledWithMatch(feed._fetchIcon, {url: FAKE_LINKS[4].url});
+      assert.calledWithMatch(feed._fetchIcon, {url: "foo.com"});
+      assert.calledWithMatch(feed._fetchIcon, {url: "bar.net"});
+    });
     it("should call unpin with correct parameters on TOP_SITES_UNPIN", () => {
       fakeNewTabUtils.pinnedLinks.links = [null, null, {url: "foo.com"}, null, null, null, null, null, FAKE_LINKS[0]];
       const unpinAction = {
         type: at.TOP_SITES_UNPIN,
         data: {site: {url: "foo.com"}}
       };
       feed.onAction(unpinAction);
       assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);
--- a/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
@@ -332,37 +332,46 @@ describe("Top Stories Feed", () => {
       assert.deepEqual(items, [{"score": 0.2}, {"score": 0.1}]);
     });
     it("should rotate items if personalization is preffed on", () => {
       let items = [{"guid": "g1"}, {"guid": "g2"}, {"guid": "g3"}, {"guid": "g4"}];
 
       instance.personalized = true;
 
       let rotated = instance.rotate(items);
-      assert.deepEqual({"g1": 0}, instance.topItem);
+      assert.deepEqual(new Map([["g1", 0]]), instance.topItems);
       assert.deepEqual(items, rotated);
 
       rotated = instance.rotate(items);
-      assert.deepEqual({"g1": 1}, instance.topItem);
+      assert.deepEqual(new Map([["g1", 1]]), instance.topItems);
       assert.deepEqual(items, rotated);
 
       rotated = instance.rotate(items);
-      assert.deepEqual({"g2": 0}, instance.topItem);
+      assert.deepEqual(new Map([["g1", 2], ["g2", 0]]), instance.topItems);
       assert.deepEqual([{"guid": "g2"}, {"guid": "g3"}, {"guid": "g4"}, {"guid": "g1"}], rotated);
 
-      rotated = instance.rotate(items);
-      assert.deepEqual({"g2": 1}, instance.topItem);
+      // Simulate g1 on top again which should again be rotated to the end
+      rotated = instance.rotate([{"guid": "g1"}, {"guid": "g2"}, {"guid": "g3"}, {"guid": "g4"}]);
+      assert.deepEqual(new Map([["g1", 3], ["g2", 1]]), instance.topItems);
       assert.deepEqual([{"guid": "g2"}, {"guid": "g3"}, {"guid": "g4"}, {"guid": "g1"}], rotated);
     });
-    it("should note rotate items if personalization is preffed off", () => {
+    it("should not rotate items if personalization is preffed off", () => {
       let items = [{"guid": "g1"}, {"guid": "g2"}, {"guid": "g3"}, {"guid": "g4"}];
 
       instance.personalized = false;
 
-      instance.topItem = {"g1": 1};
+      instance.topItems = new Map([["g1", 1]]);
+      const rotated = instance.rotate(items);
+      assert.deepEqual(items, rotated);
+    });
+    it("should stop rotating if all items have been on top", () => {
+      let items = [{"guid": "g1"}, {"guid": "g2"}, {"guid": "g3"}, {"guid": "g4"}];
+      instance.topItems = new Map([["g1", 2], ["g2", 2], ["g3", 2], ["g4", 2]]);
+      instance.personalized = true;
+
       const rotated = instance.rotate(items);
       assert.deepEqual(items, rotated);
     });
     it("should insert spoc at provided interval", async () => {
       let fetchStub = globals.sandbox.stub();
       globals.set("fetch", fetchStub);
       globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});