Bug 1386785 - Part 1. Uplift Bug 1385716 - Add geo-based sites/stories, CSP tag and bug fixes to Activity Stream. r=ursula draft
authorEd Lee <edilee@mozilla.com>
Mon, 31 Jul 2017 22:46:31 -0700
changeset 619911 875cb6116002bfe92427e9ee33f91c96362abe02
parent 619910 60ee1059ee6c76d9dff806aee85bb29bcd20fba0
child 619912 1bc93b623bdccdf3a6a80346f348fb5150b1991b
push id71861
push userbmo:edilee@mozilla.com
push dateWed, 02 Aug 2017 20:06:54 +0000
reviewersursula
bugs1386785, 1385716
milestone56.0
Bug 1386785 - Part 1. Uplift Bug 1385716 - Add geo-based sites/stories, CSP tag and bug fixes to Activity Stream. r=ursula MozReview-Commit-ID: 7eTnGf5iqiA
browser/extensions/activity-stream/bootstrap.js
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/activity-stream.html
browser/extensions/activity-stream/data/locales.json
browser/extensions/activity-stream/lib/ActivityStream.jsm
browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
browser/extensions/activity-stream/lib/LocalizationFeed.jsm
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/functional/mochitest/.eslintrc.js
browser/extensions/activity-stream/test/functional/mochitest/browser.ini
browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
browser/extensions/activity-stream/test/functional/mochitest/browser_as_render.js
browser/extensions/activity-stream/test/functional/mochitest/head.js
browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
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
browser/extensions/activity-stream/test/unit/unit-entry.js
--- a/browser/extensions/activity-stream/bootstrap.js
+++ b/browser/extensions/activity-stream/bootstrap.js
@@ -70,18 +70,20 @@ function init(reason) {
 /**
  * uninit - Uninitializes the activityStream instance, if it exsits.This could be
  *          called by the shutdown() function exposed by bootstrap.js, or it could
  *          be called when ACTIVITY_STREAM_ENABLED_PREF is changed from true to false.
  *
  * @param  {type} reason Reason for uninitialization. Could be uninstall, upgrade, or PREF_OFF
  */
 function uninit(reason) {
+  // Make sure to only uninit once in case both pref change and shutdown happen
   if (activityStream) {
     activityStream.uninit(reason);
+    activityStream = null;
   }
 }
 
 /**
  * onPrefChanged - handler for changes to ACTIVITY_STREAM_ENABLED_PREF
  *
  */
 function onPrefChanged() {
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -1450,17 +1450,26 @@ class Card extends React.Component {
     event.preventDefault();
     this.setState({
       activeCard: this.props.index,
       showContextMenu: true
     });
   }
   onLinkClick(event) {
     event.preventDefault();
-    this.props.dispatch(ac.SendToMain({ type: at.OPEN_LINK, data: this.props.link }));
+    const altKey = event.altKey,
+          button = event.button,
+          ctrlKey = event.ctrlKey,
+          metaKey = event.metaKey,
+          shiftKey = event.shiftKey;
+
+    this.props.dispatch(ac.SendToMain({
+      type: at.OPEN_LINK,
+      data: Object.assign(this.props.link, { event: { altKey, button, ctrlKey, metaKey, shiftKey } })
+    }));
     this.props.dispatch(ac.UserEvent({
       event: "CLICK",
       source: this.props.eventSource,
       action_position: this.props.index
     }));
   }
   onMenuUpdate(showContextMenu) {
     this.setState({ showContextMenu });
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -152,17 +152,18 @@ a {
 .outer-wrapper {
   display: flex;
   flex-grow: 1;
   padding: 62px 32px 32px;
   height: 100%; }
 
 main {
   margin: auto;
-  width: 224px; }
+  width: 224px;
+  padding-bottom: 120px; }
   @media (min-width: 416px) {
     main {
       width: 352px; } }
   @media (min-width: 544px) {
     main {
       width: 480px; } }
   @media (min-width: 800px) {
     main {
@@ -178,23 +179,59 @@ main {
   margin: 0 0 18px; }
   .section-title span {
     vertical-align: middle; }
 
 .top-sites-list {
   list-style: none;
   margin: 0;
   padding: 0;
-  margin-inline-end: -32px; }
-  @media (min-width: 544px) {
-    .top-sites-list {
-      width: 512px; } }
-  @media (min-width: 800px) {
-    .top-sites-list {
-      width: 768px; } }
+  margin-inline-end: -32px;
+  margin-bottom: -18px; }
+  @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;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 416px) and (max-width: 544px) {
+    .top-sites-list :nth-child(3n+2) .context-menu, .top-sites-list :nth-child(3n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 544px) and (max-width: 800px) {
+    .top-sites-list :nth-child(4n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 544px) and (max-width: 768px) {
+    .top-sites-list :nth-child(4n+3) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 800px) and (max-width: 1248px) {
+    .top-sites-list :nth-child(6n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @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-inline-end: 32px; }
   .top-sites-list .top-site-outer {
     position: relative; }
     .top-sites-list .top-site-outer > a {
       display: block;
@@ -332,16 +369,34 @@ main {
     color: #0A84FF; }
 
 .sections-list .section-list {
   clear: both;
   margin: 0;
   display: grid;
   grid-template-columns: repeat(auto-fit, 224px);
   grid-gap: 32px; }
+  @media (max-width: 544px) {
+    .sections-list .section-list .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 544px) and (max-width: 800px) {
+    .sections-list .section-list :nth-child(2n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
+  @media (min-width: 800px) and (max-width: 1248px) {
+    .sections-list .section-list :nth-child(3n) .context-menu {
+      margin-inline-start: auto;
+      margin-inline-end: 5px;
+      offset-inline-start: auto;
+      offset-inline-end: 0; } }
 
 .sections-list .section-empty-state {
   width: 100%;
   height: 266px;
   display: flex;
   border: solid 1px rgba(0, 0, 0, 0.1);
   border-radius: 3px;
   margin-bottom: 16px; }
@@ -363,36 +418,48 @@ main {
       font-size: 13px;
       font-weight: 300;
       color: #A0A0A0;
       text-align: center; }
 
 .topic {
   font-size: 13px;
   color: #BFC0C7;
-  min-width: 780px;
-  line-height: 16px;
-  margin-top: 16px; }
+  margin-top: 16px;
+  line-height: 1.6; }
+  @media (min-width: 800px) {
+    .topic {
+      line-height: 16px;
+      min-width: 780px; } }
   .topic ul {
-    display: inline;
-    padding-left: 12px; }
+    margin: 0;
+    padding: 0; }
+    @media (min-width: 800px) {
+      .topic ul {
+        display: inline;
+        padding-left: 12px; } }
   .topic ul li {
-    display: inline; }
-  .topic ul li::after {
-    content: '•';
-    padding-left: 8px;
-    padding-right: 8px; }
-  .topic ul li:last-child::after {
-    content: none; }
+    display: inline-block; }
+    @media (min-width: 800px) {
+      .topic ul li {
+        display: inline; } }
+    .topic ul li::after {
+      content: '•';
+      padding-left: 8px;
+      padding-right: 8px; }
+    .topic ul li:last-child::after {
+      content: none; }
   .topic .topic-link {
     color: #008EA4; }
   .topic .topic-read-more {
-    float: right;
-    margin-right: 40px;
     color: #008EA4; }
+    @media (min-width: 800px) {
+      .topic .topic-read-more {
+        margin-right: 40px;
+        float: right; } }
   .topic .topic-read-more-logo {
     padding-right: 10px;
     margin-left: 5px;
     background-image: url("assets/topic-show-more-12.svg");
     background-repeat: no-repeat;
     vertical-align: middle; }
 
 .search-wrapper {
@@ -689,27 +756,32 @@ main {
   .card-outer .card {
     height: 100%;
     border-radius: 3px; }
   .card-outer > a {
     display: block;
     color: inherit;
     height: 100%;
     outline: none;
-    position: absolute; }
+    position: absolute;
+    max-width: 224px; }
     .card-outer > a.active .card, .card-outer > a:focus .card {
       box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
       transition: box-shadow 150ms; }
+    .card-outer > a.active .card-title, .card-outer > a:focus .card-title {
+      color: #00AFF7; }
   .card-outer:hover, .card-outer:focus, .card-outer.active {
     outline: none;
     box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
     transition: box-shadow 150ms; }
     .card-outer:hover .context-menu-button, .card-outer:focus .context-menu-button, .card-outer.active .context-menu-button {
       transform: scale(1);
       opacity: 1; }
+    .card-outer:hover .card-title, .card-outer:focus .card-title, .card-outer.active .card-title {
+      color: #00AFF7; }
   .card-outer .card-preview-image {
     position: relative;
     background-size: cover;
     background-position: center;
     background-repeat: no-repeat;
     height: 122px;
     border-bottom-color: rgba(0, 0, 0, 0.1);
     border-bottom-style: solid;
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -1,12 +1,13 @@
 <!doctype html>
 <html lang="en-us" dir="ltr">
   <head>
     <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="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>
     <div id="snippets-container">
       <div id="snippets"></div>
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -480,24 +480,26 @@
   "bs": {},
   "ca": {
     "newtab_page_title": "Pestanya nova",
     "default_label_loading": "S'està carregant…",
     "header_top_sites": "Llocs principals",
     "header_stories": "Articles populars",
     "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",
     "type_label_recommended": "Tendència",
     "type_label_open": "Obert",
     "type_label_topic": "Tema",
+    "type_label_now": "Ara",
     "menu_action_bookmark": "Afegeix a les adreces d'interès",
     "menu_action_remove_bookmark": "Elimina l'adreça d'interès",
     "menu_action_copy_address": "Copia l'adreça",
     "menu_action_email_link": "Envia l'enllaç per correu…",
     "menu_action_open_new_window": "Obre en una finestra nova",
     "menu_action_open_private_window": "Obre en una finestra privada nova",
     "menu_action_dismiss": "Descarta",
     "menu_action_delete": "Elimina de l'historial",
@@ -506,16 +508,17 @@
     "confirm_history_delete_p1": "Segur que voleu suprimir de l'historial totes les instàncies d'aquesta pàgina?",
     "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ó",
     "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",
@@ -550,17 +553,21 @@
     "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ó"
+    "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_cancel_button": "No, gràcies",
+    "manual_migration_import_button": "Importa-ho ara"
   },
   "cak": {},
   "cs": {
     "newtab_page_title": "Nový panel",
     "default_label_loading": "Načítání…",
     "header_top_sites": "Top stránky",
     "header_stories": "Nejlepší příběhy",
     "header_visit_again": "Znovu navštívit",
@@ -1402,35 +1409,38 @@
     "topsites_form_save_button": "Guardar",
     "topsites_form_cancel_button": "Cancelar",
     "topsites_form_url_validation": "Se requiere URL válida",
     "pocket_read_more": "Tópicos populares:",
     "pocket_read_even_more": "Ver más historias",
     "pocket_feedback_header": "Lo mejor de la web, seleccionado por más de 25 millones de personas.",
     "pocket_feedback_body": "Pocket, parte de la familia Mozilla, ayudará a conectarte con contenido de alta calidad que no podrías haber encontrado de otra forma.",
     "pocket_send_feedback": "Enviar opinión",
+    "topstories_empty_state": "Ya te pusiste al día. Volvé más tarde para más historias de {provider}. ¿No podés esperar? Seleccioná un tema popular para encontrar más historias de todo el mundo.",
     "manual_migration_explanation": "Probá Firefox con tus sitios favoritos y marcadores de otro navegador.",
     "manual_migration_cancel_button": "No gracias",
     "manual_migration_import_button": "Importar ahora"
   },
   "es-CL": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Sitios frecuentes",
     "header_stories": "Historias populares",
     "header_visit_again": "Volver a visitar",
     "header_bookmarks": "Marcadores recientes",
+    "header_recommended_by": "Recomendado por {provider}",
     "header_bookmarks_placeholder": "Todavía no tienes marcadores.",
     "header_stories_from": "de",
     "type_label_visited": "Visitado",
     "type_label_bookmarked": "Marcado",
     "type_label_synced": "Sacado de otro dispositivo",
     "type_label_recommended": "Popular",
     "type_label_open": "Abrir",
     "type_label_topic": "Tema",
+    "type_label_now": "Ahora",
     "menu_action_bookmark": "Marcador",
     "menu_action_remove_bookmark": "Remover marcador",
     "menu_action_copy_address": "Copiar dirección",
     "menu_action_email_link": "Enviar enlace por correo",
     "menu_action_open_new_window": "Abrir en una nueva ventana",
     "menu_action_open_private_window": "Abrir en una nueva ventana privada",
     "menu_action_dismiss": "Descartar",
     "menu_action_delete": "Eliminar del historial",
@@ -1439,16 +1449,17 @@
     "confirm_history_delete_p1": "¿Estás seguro de que quieres eliminar cada instancia de esta página de tu historial?",
     "confirm_history_delete_notice_p2": "Esta acción no puede ser deshecha.",
     "menu_action_save_to_pocket": "Guardar en Pocket",
     "search_for_something_with": "Buscar {search_term} con:",
     "search_button": "Buscar",
     "search_header": "Búsqueda de {search_engine_name}",
     "search_web_placeholder": "Buscar en la Web",
     "search_settings": "Cambiar ajustes de búsqueda",
+    "section_info_option": "Info",
     "welcome_title": "Bienvenido a la nueva pestaña",
     "welcome_body": "Firefox usará este espacio para mostrarte los marcadores, artículos, videos y páginas visitadas recientemente más relevantes, para que puedas regresar a ellos de una.",
     "welcome_label": "Identificando tus destacados",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
     "time_label_day": "{number}d",
     "settings_pane_button_label": "Personaliza tu página de Nueva pestaña",
@@ -1483,17 +1494,21 @@
     "topsites_form_add_button": "Añadir",
     "topsites_form_save_button": "Guardar",
     "topsites_form_cancel_button": "Cancelar",
     "topsites_form_url_validation": "URL válida requerida",
     "pocket_read_more": "Temas populares:",
     "pocket_read_even_more": "Ver más historias",
     "pocket_feedback_header": "Lo mejor de la web, revisado por más de 25 millones de personas.",
     "pocket_feedback_body": "Pocket, una parte de la familia de Mozilla, te ayudará a conectarte con contenido de alta calidad que de otra forma no hubieras encontrado.",
-    "pocket_send_feedback": "Enviar comentario"
+    "pocket_send_feedback": "Enviar comentario",
+    "topstories_empty_state": "Te has puesto al día. Revisa más tarde para ver más historias de {provider}. ¿No puedes esperar? Selecciona un tema popular para encontrar más historias de todo el mundo.",
+    "manual_migration_explanation": "Prueba Firefox con tus sitios favoritos y marcadores de otro navegador.",
+    "manual_migration_cancel_button": "No, gracias",
+    "manual_migration_import_button": "Importar ahora"
   },
   "es-ES": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Sitios favoritos",
     "header_stories": "Historias populares",
     "header_visit_again": "Visitar de nuevo",
     "header_bookmarks": "Marcadores recientes",
@@ -1967,17 +1982,17 @@
     "pocket_read_more": "Sujets populaires :",
     "pocket_read_even_more": "Afficher plus d’articles",
     "pocket_feedback_header": "Le meilleur du Web, sélectionné par plus de 25 millions de personnes.",
     "pocket_feedback_body": "Pocket, un membre de la famille Mozilla, vous aide à découvrir du contenu de grande qualité que vous auriez pu manquer dans le cas contraire.",
     "pocket_send_feedback": "Donner mon avis",
     "topstories_empty_state": "Il n’y en a pas d’autres. Revenez plus tard pour plus d’articles de {provider}. Vous ne voulez pas attendre ? Choisissez un sujet parmi les plus populaires pour découvrir d’autres articles intéressants sur le Web.",
     "manual_migration_explanation": "Essayez Firefox avec vos sites et marque-pages préférés, importés depuis un autre navigateur.",
     "manual_migration_cancel_button": "Non merci",
-    "manual_migration_import_button": "Importer maintenant"
+    "manual_migration_import_button": "Importer"
   },
   "fy-NL": {
     "newtab_page_title": "Nij ljepblêd",
     "default_label_loading": "Lade…",
     "header_top_sites": "Topwebsites",
     "header_stories": "Topferhalen",
     "header_visit_again": "Nochris besykje",
     "header_bookmarks": "Resinte blêdwizers",
@@ -2917,17 +2932,17 @@
     "search_for_something_with": "{search_term} -ის ძიება:",
     "search_button": "ძიება",
     "search_header": "{search_engine_name} -ში ძიება",
     "search_web_placeholder": "ინტერნეტში ძიება",
     "search_settings": "ძიების პარამეტრების შეცვლა",
     "section_info_option": "ინფორმაცია",
     "welcome_title": "მოგესალმებით ახალ ჩანართზე",
     "welcome_body": "Firefox ამ სივრცეს გამოიყენებს თქვენთვის ყველაზე საჭირო სანიშნეების, სტატიების, ვიდეოებისა და ბოლოს მონახულებული გვერდებისთვის, რომ ადვილად შეძლოთ მათზე დაბრუნება.",
-    "welcome_label": "რჩეული ვებ-გვერდების დადგენა",
+    "welcome_label": "რჩეული ვებგვერდების დადგენა",
     "time_label_less_than_minute": "<1წთ",
     "time_label_minute": "{number}წთ",
     "time_label_hour": "{number}სთ",
     "time_label_day": "{number}დღე",
     "settings_pane_button_label": "მოირგეთ ახალი ჩანართის გვერდი",
     "settings_pane_header": "ახალი ჩანართის პარამეტრები",
     "settings_pane_body": "აირჩიეთ რისი ხილვა გსურთ ახალი ჩანართის გახსნისას.",
     "settings_pane_search_header": "ძიება",
@@ -3347,24 +3362,26 @@
   },
   "lt": {
     "newtab_page_title": "Nauja kortelė",
     "default_label_loading": "Įkeliama…",
     "header_top_sites": "Lankomiausios svetainės",
     "header_stories": "Populiariausi straipsniai",
     "header_visit_again": "Aplankykite vėl",
     "header_bookmarks": "Paskiausi adresyno įrašai",
+    "header_recommended_by": "Rekomendavo „{provider}“",
     "header_bookmarks_placeholder": "Jūs dar neturite adresyno įrašų.",
     "header_stories_from": "iš",
     "type_label_visited": "Aplankyti",
     "type_label_bookmarked": "Adresyne",
     "type_label_synced": "Sinchronizuoti iš kito įrenginio",
     "type_label_recommended": "Populiaru",
     "type_label_open": "Atviri",
     "type_label_topic": "Tema",
+    "type_label_now": "Dabar",
     "menu_action_bookmark": "Įrašyti į adresyną",
     "menu_action_remove_bookmark": "Pašalinti iš adresyno",
     "menu_action_copy_address": "Kopijuoti adresą",
     "menu_action_email_link": "Siųsti saitą el. paštu…",
     "menu_action_open_new_window": "Atverti naujame lange",
     "menu_action_open_private_window": "Atverti naujame privačiajame lange",
     "menu_action_dismiss": "Paslėpti",
     "menu_action_delete": "Pašalinti iš istorijos",
@@ -3373,16 +3390,17 @@
     "confirm_history_delete_p1": "Ar tikrai norite pašalinti visus šio tinklalapio įrašus iš savo naršymo žurnalo?",
     "confirm_history_delete_notice_p2": "Atlikus šį veiksmą, jo atšaukti neįmanoma.",
     "menu_action_save_to_pocket": "Įrašyti į „Pocket“",
     "search_for_something_with": "Ieškoti „{search_term}“ per:",
     "search_button": "Ieškoti",
     "search_header": "{search_engine_name} paieška",
     "search_web_placeholder": "Ieškokite saityne",
     "search_settings": "Keisti paieškos nuostatas",
+    "section_info_option": "Informacija",
     "welcome_title": "Sveiki, čia nauja kortelė",
     "welcome_body": "„Firefox“ naudos šią vietą jums aktualiausių adresyno įrašų, straipsnių, vaizdo įrašų bei neseniai lankytų tinklalapių rodymui, kad galėtumėte lengvai į juos sugrįžti.",
     "welcome_label": "Nustatomi jūsų akcentai",
     "time_label_less_than_minute": "<1 min.",
     "time_label_minute": "{number} min.",
     "time_label_hour": "{number} val.",
     "time_label_day": "{number} d.",
     "settings_pane_button_label": "Tinkinkite savo naujos kortelės puslapį",
@@ -3417,17 +3435,21 @@
     "topsites_form_add_button": "Pridėti",
     "topsites_form_save_button": "Įrašyti",
     "topsites_form_cancel_button": "Atsisakyti",
     "topsites_form_url_validation": "Reikalingas tinkamas URL",
     "pocket_read_more": "Populiarios temos:",
     "pocket_read_even_more": "Rodyti daugiau straipsnių",
     "pocket_feedback_header": "Geriausi dalykai internete, kuruojami daugiau nei 25 milijonų žmonių.",
     "pocket_feedback_body": "„Pocket“, „Mozillos“ šeimos dalis, padės jums atrasti kokybišką turinį, kurio kitaip gal nebūtumėte radę.",
-    "pocket_send_feedback": "Siųsti atsiliepimą"
+    "pocket_send_feedback": "Siųsti atsiliepimą",
+    "topstories_empty_state": "Viską perskaitėte. Užsukite vėliau, norėdami rasti daugiau gerų straipsnių iš „{provider}“. Nekantraujate? Pasirinkite populiarią temą, norėdami rasti daugiau puikių straipsnių saityne.",
+    "manual_migration_explanation": "Išbandykite „Firefox“ su mėgstamiausiomis svetainėmis bei adresyno įrašais iš kitos naršyklės.",
+    "manual_migration_cancel_button": "Ačiū, ne",
+    "manual_migration_import_button": "Importuoti dabar"
   },
   "ltg": {},
   "lv": {
     "newtab_page_title": "Jauna cilne"
   },
   "mai": {},
   "mk": {},
   "ml": {
@@ -4270,56 +4292,67 @@
     "manual_migration_explanation": "Experimente o Firefox com os seus sites favoritos e marcadores de outro navegador.",
     "manual_migration_cancel_button": "Não obrigado",
     "manual_migration_import_button": "Importar agora"
   },
   "rm": {
     "newtab_page_title": "Nov tab",
     "default_label_loading": "Chargiar…",
     "header_top_sites": "Paginas preferidas",
-    "header_highlights": "Accents",
     "header_stories": "Artitgels populars",
+    "header_visit_again": "Turnar a visitar",
+    "header_bookmarks": "Segnapaginas novs",
+    "header_recommended_by": "Recumandà da {provider}",
+    "header_bookmarks_placeholder": "Ti n'has anc nagins segnapaginas.",
     "header_stories_from": "da",
     "type_label_visited": "Visità",
     "type_label_bookmarked": "Cun segnapagina",
     "type_label_synced": "Sincronisà dad auters apparats",
     "type_label_recommended": "Popular",
     "type_label_open": "Avert",
     "type_label_topic": "Tema",
+    "type_label_now": "Ussa",
     "menu_action_bookmark": "Marcar sco segnapagina",
     "menu_action_remove_bookmark": "Allontanar il segnapagina",
     "menu_action_copy_address": "Copiar l'adressa",
     "menu_action_email_link": "Trametter la colliaziun per e-mail…",
     "menu_action_open_new_window": "Avrir en ina nova fanestra",
     "menu_action_open_private_window": "Avrir en ina nova fanestra privata",
     "menu_action_dismiss": "Serrar",
     "menu_action_delete": "Stizzar da la cronologia",
+    "menu_action_pin": "Fixar",
+    "menu_action_unpin": "Betg pli fixar",
+    "confirm_history_delete_p1": "Vuls ti propi stizzar mintga instanza da questa pagina ord la cronologia?",
+    "confirm_history_delete_notice_p2": "Questa acziun na po betg vegnir revocada.",
     "menu_action_save_to_pocket": "Memorisar en Pocket",
     "search_for_something_with": "Tschertgar {search_term} cun:",
     "search_button": "Tschertgar",
     "search_header": "Tschertga da {search_engine_name}",
     "search_web_placeholder": "Tschertgar en il Web",
     "search_settings": "Midar las preferenzas per tschertgar",
+    "section_info_option": "Info",
     "welcome_title": "Bainvegni sin in nov tab",
     "welcome_body": "Firefox utilisescha quest plaz per ta mussar ils segnapaginas, ils artitgels, ils videos e las paginas las pli relevantas che ti has visità dacurt, uschè che ti pos turnar a moda simpla tar quellas.",
     "welcome_label": "Identifitgar tes accents",
     "time_label_less_than_minute": "< 1 min",
     "time_label_minute": "{number} min",
     "time_label_hour": "{number} uras",
     "time_label_day": "{number} dis",
     "settings_pane_button_label": "Persunalisar tia pagina per novs tabs",
     "settings_pane_header": "Preferenzas per novs tabs",
     "settings_pane_body": "Tscherna tge che ti vesas sche ti avras in nov tab.",
     "settings_pane_search_header": "Tschertgar",
     "settings_pane_search_body": "Tschertgar en l'internet da tes nov tab.",
     "settings_pane_topsites_header": "Paginas preferidas",
     "settings_pane_topsites_body": "Acceder las websites che ti visitas il pli savens.",
     "settings_pane_topsites_options_showmore": "Mussar duas colonnas",
-    "settings_pane_highlights_header": "Accents",
-    "settings_pane_highlights_body": "Dar in sguard enavos sin websites visitadas dacurt e sin segnapaginas creads dacurt.",
+    "settings_pane_bookmarks_header": "Novs segnapaginas",
+    "settings_pane_bookmarks_body": "Tes novs segnapaginas en in lieu pratic.",
+    "settings_pane_visit_again_header": "Turnar a visitar",
+    "settings_pane_visit_again_body": "Firefox ta mussa parts da tia cronologia da navigaziun che pudessan esser interessantas per turnar.",
     "settings_pane_pocketstories_header": "Artitgels populars",
     "settings_pane_pocketstories_body": "Pocket che fa part da Mozilla ta gida da scuvrir cuntegn dad auta qualitad che ti avessas uschiglio forsa manchentà.",
     "settings_pane_done_button": "Finì",
     "edit_topsites_button_text": "Modifitgar",
     "edit_topsites_button_label": "Persunalisar la secziun da paginas preferidas",
     "edit_topsites_showmore_button": "Mussar dapli",
     "edit_topsites_showless_button": "Mussar pli pauc",
     "edit_topsites_done_button": "Finì",
@@ -4335,17 +4368,21 @@
     "topsites_form_add_button": "Agiuntar",
     "topsites_form_save_button": "Memorisar",
     "topsites_form_cancel_button": "Interrumper",
     "topsites_form_url_validation": "In URL valid è necessari",
     "pocket_read_more": "Temas populars:",
     "pocket_read_even_more": "Mussar dapli artitgels",
     "pocket_feedback_header": "Il meglier ord il web, selecziunà da dapli che 25 milliuns umans.",
     "pocket_feedback_body": "Pocket che fa part da Mozilla ta gida da scuvrir cuntegn dad auta qualitad che ti avessas uschiglio forsa manchentà.",
-    "pocket_send_feedback": "Trametter in resun"
+    "pocket_send_feedback": "Trametter in resun",
+    "topstories_empty_state": "Ussa has ti legì tut las novitads. Turna pli tard per ulteriuras novitads da {provider}. Na pos betg spetgar? Tscherna in tema popular per chattar ulteriuras istorgias ord il web.",
+    "manual_migration_explanation": "Utilisescha Firefox cun tias paginas preferidas e tes segnapaginas d'in auter navigatur.",
+    "manual_migration_cancel_button": "Na, grazia",
+    "manual_migration_import_button": "Importar ussa"
   },
   "ro": {
     "newtab_page_title": "Filă nouă",
     "default_label_loading": "Se încarcă…",
     "header_top_sites": "Site-uri de top",
     "header_highlights": "Evidențieri",
     "header_stories_from": "de la",
     "type_label_visited": "Vizitate",
@@ -4406,24 +4443,26 @@
   },
   "ru": {
     "newtab_page_title": "Новая вкладка",
     "default_label_loading": "Загрузка…",
     "header_top_sites": "Топ сайтов",
     "header_stories": "Топ статей",
     "header_visit_again": "Посетить снова",
     "header_bookmarks": "Недавние закладки",
+    "header_recommended_by": "Рекомендовано {provider}",
     "header_bookmarks_placeholder": "У вас ещё нет каких-либо закладок.",
     "header_stories_from": "от",
     "type_label_visited": "Посещено",
     "type_label_bookmarked": "В закладках",
     "type_label_synced": "Синхронизировано с другого устройства",
     "type_label_recommended": "Популярные",
     "type_label_open": "Открыта",
     "type_label_topic": "Тема",
+    "type_label_now": "Сейчас",
     "menu_action_bookmark": "Добавить в закладки",
     "menu_action_remove_bookmark": "Удалить закладку",
     "menu_action_copy_address": "Скопировать ссылку",
     "menu_action_email_link": "Отправить ссылку…",
     "menu_action_open_new_window": "Открыть в новом окне",
     "menu_action_open_private_window": "Открыть в новом приватном окне",
     "menu_action_dismiss": "Скрыть",
     "menu_action_delete": "Удалить из истории",
@@ -4432,16 +4471,17 @@
     "confirm_history_delete_p1": "Вы действительно хотите удалить все записи об этой странице из вашей истории?",
     "confirm_history_delete_notice_p2": "Это действие не может быть отменено.",
     "menu_action_save_to_pocket": "Сохранить в Pocket",
     "search_for_something_with": "Искать {search_term} в:",
     "search_button": "Искать",
     "search_header": "Искать в {search_engine_name}",
     "search_web_placeholder": "Искать в Интернете",
     "search_settings": "Изменить настройки поиска",
+    "section_info_option": "Информация",
     "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} д.",
     "settings_pane_button_label": "Настроить свою страницу новой вкладки",
@@ -4476,17 +4516,21 @@
     "topsites_form_add_button": "Добавить",
     "topsites_form_save_button": "Сохранить",
     "topsites_form_cancel_button": "Отмена",
     "topsites_form_url_validation": "Введите корректный URL",
     "pocket_read_more": "Популярные темы:",
     "pocket_read_even_more": "Больше статей",
     "pocket_feedback_header": "Лучшее из Интернета, отобранное более чем 25 миллионами людей.",
     "pocket_feedback_body": "Pocket, часть семьи Mozilla, поможет подключить вас к высококачественному контенту, который вы можете иначе и не найти.",
-    "pocket_send_feedback": "Отправить отзыв"
+    "pocket_send_feedback": "Отправить отзыв",
+    "topstories_empty_state": "Вы всё прочитали. Зайдите попозже, чтобы увидеть больше лучших статей от {provider}. Не можете ждать? Выберите популярную тему, чтобы найти больше интересных статей со всего Интернета.",
+    "manual_migration_explanation": "Попробуйте Firefox со своими любимыми сайтами и закладками из другого браузера.",
+    "manual_migration_cancel_button": "Нет, спасибо",
+    "manual_migration_import_button": "Импортировать сейчас"
   },
   "si": {},
   "sk": {
     "newtab_page_title": "Nová karta",
     "default_label_loading": "Načítava sa…",
     "header_top_sites": "Top stránky",
     "header_stories": "Top príbehy",
     "header_visit_again": "Navštívte znova",
@@ -4646,16 +4690,17 @@
     "topsites_form_save_button": "Shrani",
     "topsites_form_cancel_button": "Prekliči",
     "topsites_form_url_validation": "Vnesite veljaven URL",
     "pocket_read_more": "Priljubljene teme:",
     "pocket_read_even_more": "Prikaži več vesti",
     "pocket_feedback_header": "Najboljše s spleta, kar je izbralo več kot 25 milijonov ljudi.",
     "pocket_feedback_body": "Pocket, del Mozilline družine, vam bo pomagal pridobiti visokokakovostne vsebine, ki jih sicer ne bi našli.",
     "pocket_send_feedback": "Pošlji povratne informacije",
+    "topstories_empty_state": "Zdaj ste seznanjeni z novicami. Vrnite se pozneje in si oglejte nove prispevke iz {provider}. Komaj čakate? Izberite priljubljeno temo in odkrijte več velikih zgodb na spletu.",
     "manual_migration_explanation": "Preskusite Firefox s svojimi priljubljenimi stranmi in zaznamki iz drugih brskalnikov.",
     "manual_migration_cancel_button": "Ne, hvala",
     "manual_migration_import_button": "Uvozi zdaj"
   },
   "son": {},
   "sq": {
     "newtab_page_title": "Skedë e Re",
     "default_label_loading": "Po ngarkohet…",
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -17,44 +17,66 @@ const {PlacesFeed} = Cu.import("resource
 const {PrefsFeed} = Cu.import("resource://activity-stream/lib/PrefsFeed.jsm", {});
 const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
 const {SnippetsFeed} = Cu.import("resource://activity-stream/lib/SnippetsFeed.jsm", {});
 const {SystemTickFeed} = Cu.import("resource://activity-stream/lib/SystemTickFeed.jsm", {});
 const {TelemetryFeed} = Cu.import("resource://activity-stream/lib/TelemetryFeed.jsm", {});
 const {TopSitesFeed} = Cu.import("resource://activity-stream/lib/TopSitesFeed.jsm", {});
 const {TopStoriesFeed} = Cu.import("resource://activity-stream/lib/TopStoriesFeed.jsm", {});
 
+const DEFAULT_SITES = new Map([
+  // This first item is the global list fallback for any unexpected geos
+  ["", "https://www.youtube.com/,https://www.facebook.com/,https://www.wikipedia.org/,https://www.reddit.com/,https://www.amazon.com/,https://twitter.com/"],
+  ["US", "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"],
+  ["CA", "https://www.youtube.com/,https://www.facebook.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://www.amazon.ca/,https://twitter.com/"],
+  ["DE", "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.de/,https://www.ebay.de/,https://www.wikipedia.org/,https://www.reddit.com/"],
+  ["PL", "https://www.youtube.com/,https://www.facebook.com/,https://allegro.pl/,https://www.wikipedia.org/,https://www.olx.pl/,https://www.wykop.pl/"],
+  ["RU", "https://vk.com/,https://www.youtube.com/,https://ok.ru/,https://www.avito.ru/,https://www.aliexpress.com/,https://www.wikipedia.org/"],
+  ["GB", "https://www.youtube.com/,https://www.facebook.com/,https://www.reddit.com/,https://www.amazon.co.uk/,https://www.bbc.co.uk/,https://www.ebay.co.uk/"],
+  ["FR", "https://www.youtube.com/,https://www.facebook.com/,https://www.wikipedia.org/,https://www.amazon.fr/,https://www.leboncoin.fr/,https://twitter.com/"]
+]);
+const GEO_PREF = "browser.search.region";
 const REASON_ADDON_UNINSTALL = 6;
 
-// For now, we only want to show top stories by default to the following locales
-const showTopStoriesByDefault = ["en-US", "en-CA"].includes(Services.locale.getRequestedLocale());
-// Sections, keyed by section id
-const SECTIONS = new Map([
-  ["topstories", {
-    feed: TopStoriesFeed,
-    prefTitle: "Fetches content recommendations from a configurable content provider",
-    showByDefault: showTopStoriesByDefault
-  }]
-]);
-
-const SECTION_FEEDS_CONFIG = Array.from(SECTIONS.entries()).map(entry => {
-  const id = entry[0];
-  const {feed: Feed, prefTitle, showByDefault: value} = entry[1];
-  return {
-    name: `section.${id}`,
-    factory: () => new Feed(),
-    title: prefTitle || `${id} section feed`,
-    value
-  };
-});
-
+// Configure default Activity Stream prefs with a plain `value` or a `getValue`
+// that computes a value. A `value_local_dev` is used for development defaults.
 const PREFS_CONFIG = new Map([
   ["default.sites", {
     title: "Comma-separated list of default top sites to fill in behind visited sites",
-    value: "https://www.facebook.com/,https://www.youtube.com/,https://www.amazon.com/,https://www.yahoo.com/,https://www.ebay.com/,https://twitter.com/"
+    getValue: ({geo}) => DEFAULT_SITES.get(DEFAULT_SITES.has(geo) ? geo : "")
+  }],
+  ["feeds.section.topstories.options", {
+    title: "Configuration options for top stories feed",
+    // This is a dynamic pref as it depends on the feed being shown or not
+    getValue: args => JSON.stringify({
+      api_key_pref: "extensions.pocket.oAuthConsumerKey",
+      // Use the opposite value as what default value the feed would have used
+      hidden: !PREFS_CONFIG.get("feeds.section.topstories").getValue(args),
+      learn_more_endpoint: "https://getpocket.com/firefox_learnmore?src=ff_newtab",
+      provider_description: "pocket_feedback_body",
+      provider_icon: "pocket",
+      provider_name: "Pocket",
+      read_more_endpoint: "https://getpocket.com/explore/trending?src=ff_new_tab",
+      stories_endpoint: `https://getpocket.com/v3/firefox/global-recs?consumer_key=$apiKey&locale_lang=${args.locale}`,
+      stories_referrer: "https://getpocket.com/recommendations",
+      survey_link: "https://www.surveymonkey.com/r/newtabffx",
+      topics_endpoint: `https://getpocket.com/v3/firefox/trending-topics?consumer_key=$apiKey&locale_lang=${args.locale}`
+    })
+  }],
+  ["migrationExpired", {
+    title: "Boolean flag that decides whether to show the migration message or not.",
+    value: false
+  }],
+  ["migrationLastShownDate", {
+    title: "Timestamp when migration message was last shown. In seconds.",
+    value: 0
+  }],
+  ["migrationRemainingDays", {
+    title: "Number of days to show the manual migration message",
+    value: 4
   }],
   ["showSearch", {
     title: "Show the Search bar on the New Tab page",
     value: true
   }],
   ["showTopSites", {
     title: "Show the Top Sites section on the New Tab page",
     value: true
@@ -67,56 +89,34 @@ const PREFS_CONFIG = new Map([
   ["telemetry.log", {
     title: "Log telemetry events in the console",
     value: false,
     value_local_dev: true
   }],
   ["telemetry.ping.endpoint", {
     title: "Telemetry server endpoint",
     value: "https://tiles.services.mozilla.com/v4/links/activity-stream"
-  }],
-  ["feeds.section.topstories.options", {
-    title: "Configuration options for top stories feed",
-    value: `{
-      "stories_endpoint": "https://getpocket.com/v3/firefox/global-recs?consumer_key=$apiKey&locale_lang=$locale",
-      "stories_referrer": "https://getpocket.com/recommendations",
-      "topics_endpoint": "https://getpocket.com/v3/firefox/trending-topics?consumer_key=$apiKey&locale_lang=$locale",
-      "read_more_endpoint": "https://getpocket.com/explore/trending?src=ff_new_tab",
-      "learn_more_endpoint": "https://getpocket.com/firefox_learnmore?src=ff_newtab",
-      "survey_link": "https://www.surveymonkey.com/r/newtabffx",
-      "api_key_pref": "extensions.pocket.oAuthConsumerKey",
-      "provider_name": "Pocket",
-      "provider_icon": "pocket",
-      "provider_description": "pocket_feedback_body",
-      "hidden": ${!showTopStoriesByDefault}
-    }`
-  }],
-  ["migrationExpired", {
-    title: "Boolean flag that decides whether to show the migration message or not.",
-    value: false
-  }],
-  ["migrationRemainingDays", {
-    title: "Number of days to show the manual migration message",
-    value: 4
-  }],
-  ["migrationLastShownDate", {
-    title: "Timestamp when migration message was last shown. In seconds.",
-    value: 0
   }]
 ]);
 
-const FEEDS_CONFIG = new Map();
-for (const {name, factory, title, value} of SECTION_FEEDS_CONFIG.concat([
+// Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
+const FEEDS_DATA = [
   {
     name: "localization",
     factory: () => new LocalizationFeed(),
     title: "Initialize strings and detect locale for Activity Stream",
     value: true
   },
   {
+    name: "migration",
+    factory: () => new ManualMigration(),
+    title: "Manual migration wizard",
+    value: true
+  },
+  {
     name: "newtabinit",
     factory: () => new NewTabInit(),
     title: "Sends a copy of the state to each new tab that is opened",
     value: true
   },
   {
     name: "places",
     factory: () => new PlacesFeed(),
@@ -125,16 +125,30 @@ for (const {name, factory, title, value}
   },
   {
     name: "prefs",
     factory: () => new PrefsFeed(PREFS_CONFIG),
     title: "Preferences",
     value: true
   },
   {
+    name: "section.topstories",
+    factory: () => new TopStoriesFeed(),
+    title: "Fetches content recommendations from a configurable content provider",
+    // Dynamically determine if Pocket should be shown for a geo / locale
+    getValue: ({geo, locale}) => {
+      const locales = ({
+        "US": ["en-US", "en-GB", "en-ZA"],
+        "CA": ["en-US", "en-GB", "en-ZA"],
+        "DE": ["de", "de-DE", "de-AT", "de-CH"]
+      })[geo];
+      return !!locales && locales.includes(locale);
+    }
+  },
+  {
     name: "snippets",
     factory: () => new SnippetsFeed(),
     title: "Gets snippets data",
     value: false
   },
   {
     name: "systemtick",
     factory: () => new SystemTickFeed(),
@@ -147,27 +161,24 @@ for (const {name, factory, title, value}
     title: "Relays telemetry-related actions to TelemetrySender",
     value: true
   },
   {
     name: "topsites",
     factory: () => new TopSitesFeed(),
     title: "Queries places and gets metadata for Top Sites section",
     value: true
-  },
-  {
-    name: "migration",
-    factory: () => new ManualMigration(),
-    title: "Manual migration wizard",
-    value: true
   }
-])) {
-  const pref = `feeds.${name}`;
-  FEEDS_CONFIG.set(pref, factory);
-  PREFS_CONFIG.set(pref, {title, value});
+];
+
+const FEEDS_CONFIG = new Map();
+for (const config of FEEDS_DATA) {
+  const pref = `feeds.${config.name}`;
+  FEEDS_CONFIG.set(pref, config.factory);
+  PREFS_CONFIG.set(pref, config);
 }
 
 this.ActivityStream = class ActivityStream {
 
   /**
    * constructor - Initializes an instance of ActivityStream
    *
    * @param  {object} options Options for the ActivityStream instance
@@ -178,34 +189,84 @@ this.ActivityStream = class ActivityStre
   constructor(options = {}) {
     this.initialized = false;
     this.options = options;
     this.store = new Store();
     this.feeds = FEEDS_CONFIG;
     this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
   }
   init() {
+    this._updateDynamicPrefs();
     this._defaultPrefs.init();
+
     this.store.init(this.feeds);
     this.store.dispatch({
       type: at.INIT,
       data: {version: this.options.version}
     });
+
     this.initialized = true;
   }
   uninit() {
+    if (this.geo === "") {
+      Services.prefs.removeObserver(GEO_PREF, this);
+    }
+
     this.store.dispatch({type: at.UNINIT});
     this.store.uninit();
 
     this.initialized = false;
   }
   uninstall(reason) {
     if (reason === REASON_ADDON_UNINSTALL) {
       // This resets all prefs in the config to their default values,
       // so we DON'T want to do this on an upgrade/downgrade, only on a
       // real uninstall
       this._defaultPrefs.reset();
     }
   }
+  _updateDynamicPrefs() {
+    // Save the geo pref if we have it
+    if (Services.prefs.prefHasUserValue(GEO_PREF)) {
+      this.geo = Services.prefs.getStringPref(GEO_PREF);
+    } else if (this.geo !== "") {
+      // Watch for geo changes and use a dummy value for now
+      Services.prefs.addObserver(GEO_PREF, this);
+      this.geo = "";
+    }
+
+    this.locale = Services.locale.getRequestedLocale();
+
+    // Update the pref config of those with dynamic values
+    for (const pref of PREFS_CONFIG.keys()) {
+      const prefConfig = PREFS_CONFIG.get(pref);
+      if (!prefConfig.getValue) {
+        continue;
+      }
+
+      const newValue = prefConfig.getValue({
+        geo: this.geo,
+        locale: this.locale
+      });
+
+      // If there's an existing value and it has changed, that means we need to
+      // overwrite the default with the new value.
+      if (prefConfig.value !== undefined && prefConfig.value !== newValue) {
+        this._defaultPrefs.setDefaultPref(pref, newValue);
+      }
+
+      prefConfig.value = newValue;
+    }
+  }
+  observe(subject, topic, data) {
+    switch (topic) {
+      case "nsPref:changed":
+        // We should only expect one geo change, so update and stop observing
+        if (data === GEO_PREF) {
+          this._updateDynamicPrefs();
+          Services.prefs.removeObserver(GEO_PREF, this);
+        }
+        break;
+    }
+  }
 };
 
-this.PREFS_CONFIG = PREFS_CONFIG;
-this.EXPORTED_SYMBOLS = ["ActivityStream", "SECTIONS"];
+this.EXPORTED_SYMBOLS = ["ActivityStream", "PREFS_CONFIG"];
--- a/browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
@@ -49,22 +49,22 @@ this.DefaultPrefs = class DefaultPrefs {
    * @param  {string} branch (optional) The pref branch (defaults to ACTIVITY_STREAM_PREF_BRANCH)
    */
   constructor(config, branch = ACTIVITY_STREAM_PREF_BRANCH) {
     this._config = config;
     this.branch = Services.prefs.getDefaultBranch(branch);
   }
 
   /**
-   * _setDefaultPref - Sets the default value (not user-defined) for a given pref
+   * setDefaultPref - Sets the default value (not user-defined) for a given pref
    *
    * @param  {string} key The name of the pref
    * @param  {type} val The default value of the pref
    */
-  _setDefaultPref(key, val) {
+  setDefaultPref(key, val) {
     switch (typeof val) {
       case "boolean":
         this.branch.setBoolPref(key, val);
         break;
       case "number":
         this.branch.setIntPref(key, val);
         break;
       case "string":
@@ -84,17 +84,17 @@ this.DefaultPrefs = class DefaultPrefs {
     for (const pref of this._config.keys()) {
       const prefConfig = this._config.get(pref);
       let value;
       if (IS_UNOFFICIAL_BUILD && "value_local_dev" in prefConfig) {
         value = prefConfig.value_local_dev;
       } else {
         value = prefConfig.value;
       }
-      this._setDefaultPref(pref, value);
+      this.setDefaultPref(pref, value);
     }
   }
 
   /**
    * reset - Resets all user-defined prefs for prefs in ._config to their defaults
    */
   reset() {
     for (const name of this._config.keys()) {
--- a/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
+++ b/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
@@ -25,17 +25,24 @@ this.LocalizationFeed = class Localizati
 
     this.updateLocale();
   }
   uninit() {
     Services.obs.removeObserver(this, LOCALES_CHANGE_TOPIC);
   }
 
   updateLocale() {
-    let locale = Services.locale.getRequestedLocale() || DEFAULT_LOCALE;
+    // Just take the first element in the result array, as it should be
+    // the best locale
+    let locale = Services.locale.negotiateLanguages(
+      Services.locale.getAppLocalesAsLangTags(), // desired locales
+      Object.keys(this.allStrings), // available locales
+      DEFAULT_LOCALE // fallback
+    )[0];
+
     let strings = this.allStrings[locale];
 
     // Use the default strings for any that are missing
     if (locale !== DEFAULT_LOCALE) {
       strings = Object.assign({}, this.allStrings[DEFAULT_LOCALE], strings || {});
     }
 
     this.store.dispatch(ac.BroadcastToContent({
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -220,20 +220,22 @@ class PlacesFeed {
         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);
         break;
       case at.OPEN_LINK: {
+        const win = action._target.browser.ownerGlobal;
+        const where = win.whereToOpenLink(action.data.event);
         if (action.data.referrer) {
-          action._target.browser.loadURI(action.data.url, Services.io.newURI(action.data.referrer));
+          win.openLinkIn(action.data.url, where, {referrerURI: Services.io.newURI(action.data.referrer)});
         } else {
-          action._target.browser.loadURI(action.data.url);
+          win.openLinkIn(action.data.url, where);
         }
         break;
       }
     }
   }
 }
 
 this.PlacesFeed = PlacesFeed;
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -2,35 +2,37 @@
  * 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 {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
-const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
 const {insertPinned} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Screenshots",
   "resource://activity-stream/lib/Screenshots.jsm");
 
 const TOP_SITES_SHOWMORE_LENGTH = 12;
 const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
+const DEFAULT_SITES_PREF = "default.sites";
 const DEFAULT_TOP_SITES = [];
 
 this.TopSitesFeed = class TopSitesFeed {
   constructor() {
     this.lastUpdated = 0;
   }
-  init() {
+  refreshDefaults(sites) {
+    // Clear out the array of any previous defaults
+    DEFAULT_TOP_SITES.length = 0;
+
     // Add default sites if any based on the pref
-    let sites = new Prefs().get("default.sites");
     if (sites) {
       for (const url of sites.split(",")) {
         DEFAULT_TOP_SITES.push({
           isDefault: true,
           url
         });
       }
     }
@@ -105,19 +107,16 @@ this.TopSitesFeed = class TopSitesFeed {
     this.store.dispatch(ac.BroadcastToContent({
       type: at.PINNED_SITES_UPDATED,
       data: this._getPinnedWithData()
     }));
   }
   onAction(action) {
     let realRows;
     switch (action.type) {
-      case at.INIT:
-        this.init();
-        break;
       case at.NEW_TAB_LOAD:
         // Only check against real rows returned from history, not default ones.
         realRows = this.store.getState().TopSites.rows.filter(row => !row.isDefault);
         if (
           // When a new tab is opened, if we don't have enough top sites yet, refresh the data.
           (realRows.length < TOP_SITES_SHOWMORE_LENGTH) ||
 
           // When a new tab is opened, if the last time we refreshed the data
@@ -125,16 +124,24 @@ this.TopSitesFeed = class TopSitesFeed {
           (Date.now() - this.lastUpdated >= UPDATE_TIME)
         ) {
           this.refresh(action.meta.fromTarget);
         }
         break;
       case at.PLACES_HISTORY_CLEARED:
         this.refresh();
         break;
+      case at.PREF_CHANGED:
+        if (action.data.name === DEFAULT_SITES_PREF) {
+          this.refreshDefaults(action.data.value);
+        }
+        break;
+      case at.PREFS_INITIAL_VALUES:
+        this.refreshDefaults(action.data[DEFAULT_SITES_PREF]);
+        break;
       case at.TOP_SITES_PIN:
         this.pin(action);
         break;
       case at.TOP_SITES_UNPIN:
         this.unpin(action);
         break;
     }
   }
--- a/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
@@ -11,31 +11,31 @@ Cu.importGlobalProperties(["fetch"]);
 
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
 
 const STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours
 const STORIES_NOW_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours
 const SECTION_ID = "TopStories";
+const FEED_PREF = "feeds.section.topstories";
+const SECTION_OPTIONS_PREF = "feeds.section.topstories.options";
 
 this.TopStoriesFeed = class TopStoriesFeed {
-  constructor() {
-    this.storiesLastUpdated = 0;
-    this.topicsLastUpdated = 0;
-  }
 
   init() {
     try {
+      this.storiesLastUpdated = 0;
+      this.topicsLastUpdated = 0;
+
       const prefs = new Prefs();
-      const options = JSON.parse(prefs.get("feeds.section.topstories.options"));
+      const options = JSON.parse(prefs.get(SECTION_OPTIONS_PREF));
       const apiKey = this._getApiKeyFromPref(options.api_key_pref);
-      const locale = Services.locale.getRequestedLocale();
-      this.stories_endpoint = this._produceFinalEndpointUrl(options.stories_endpoint, apiKey, locale);
-      this.topics_endpoint = this._produceFinalEndpointUrl(options.topics_endpoint, apiKey, locale);
+      this.stories_endpoint = this._produceFinalEndpointUrl(options.stories_endpoint, apiKey);
+      this.topics_endpoint = this._produceFinalEndpointUrl(options.topics_endpoint, apiKey);
 
       this.read_more_endpoint = options.read_more_endpoint;
       this.stories_referrer = options.stories_referrer;
 
       // TODO https://github.com/mozilla/activity-stream/issues/2902
       const sectionOptions = {
         id: SECTION_ID,
         eventSource: "TOP_STORIES",
@@ -85,17 +85,18 @@ this.TopStoriesFeed = class TopStoriesFe
             .filter(s => !NewTabUtils.blockedLinks.isBlocked({"url": s.dedupe_url}))
             .map(s => ({
               "guid": s.id,
               "type": (Date.now() - (s.published_timestamp * 1000)) <= STORIES_NOW_THRESHOLD ? "now" : "trending",
               "title": s.title,
               "description": s.excerpt,
               "image": this._normalizeUrl(s.image_src),
               "referrer": this.stories_referrer,
-              "url": s.dedupe_url
+              "url": s.dedupe_url,
+              "eTLD": this._addETLD(s.dedupe_url)
             }));
           return items;
         })
         .catch(error => Cu.reportError(`Failed to fetch content: ${error.message}`));
 
       if (stories) {
         this.dispatchUpdateEvent(this.storiesLastUpdated,
           {"type": at.SECTION_ROWS_UPDATE, "data": {"id": SECTION_ID, "rows": stories}});
@@ -135,38 +136,43 @@ this.TopStoriesFeed = class TopStoriesFe
   _getApiKeyFromPref(apiKeyPref) {
     if (!apiKeyPref) {
       return apiKeyPref;
     }
 
     return new Prefs().get(apiKeyPref) || Services.prefs.getCharPref(apiKeyPref);
   }
 
-  _produceFinalEndpointUrl(url, apiKey, locale) {
+  _produceFinalEndpointUrl(url, apiKey) {
     if (!url) {
       return url;
     }
     if (url.includes("$apiKey") && !apiKey) {
       throw new Error(`An API key was specified but none configured: ${url}`);
     }
-    if (url.includes("$locale") && !locale) {
-      throw new Error(`A locale was specified but none detected: ${url}`);
-    }
-    return url.replace("$apiKey", apiKey).replace("$locale", locale);
+    return url.replace("$apiKey", apiKey);
   }
 
   // Need to remove parenthesis from image URLs as React will otherwise
   // fail to render them properly as part of the card template.
   _normalizeUrl(url) {
     if (url) {
       return url.replace(/\(/g, "%28").replace(/\)/g, "%29");
     }
     return url;
   }
 
+  _addETLD(url) {
+    try {
+      return Services.eTLD.getPublicSuffix(Services.io.newURI(url));
+    } catch (err) {
+      return "";
+    }
+  }
+
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
         break;
       case at.SYSTEM_TICK:
         if (Date.now() - this.storiesLastUpdated >= STORIES_UPDATE_TIME) {
           this.fetchStories();
@@ -174,20 +180,27 @@ this.TopStoriesFeed = class TopStoriesFe
         if (Date.now() - this.topicsLastUpdated >= TOPICS_UPDATE_TIME) {
           this.fetchTopics();
         }
         break;
       case at.UNINIT:
         this.uninit();
         break;
       case at.FEED_INIT:
-        if (action.data === "feeds.section.topstories") {
+        if (action.data === FEED_PREF) {
+          this.init();
+        }
+        break;
+      case at.PREF_CHANGED:
+        if (action.data.name === SECTION_OPTIONS_PREF) {
           this.init();
         }
         break;
     }
   }
 };
 
 this.STORIES_UPDATE_TIME = STORIES_UPDATE_TIME;
 this.TOPICS_UPDATE_TIME = TOPICS_UPDATE_TIME;
 this.SECTION_ID = SECTION_ID;
-this.EXPORTED_SYMBOLS = ["TopStoriesFeed", "STORIES_UPDATE_TIME", "TOPICS_UPDATE_TIME", "SECTION_ID"];
+this.FEED_PREF = FEED_PREF;
+this.SECTION_OPTIONS_PREF = SECTION_OPTIONS_PREF;
+this.EXPORTED_SYMBOLS = ["TopStoriesFeed", "STORIES_UPDATE_TIME", "TOPICS_UPDATE_TIME", "SECTION_ID", "FEED_PREF", "SECTION_OPTIONS_PREF"];
--- a/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
+++ b/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
@@ -1,41 +1,5 @@
 module.exports = {
-  "globals": {
-    "add_task": false,
-    "Assert": false,
-    "BrowserOpenTab": false,
-    "BrowserTestUtils": false,
-    "content": false,
-    "ContentTask": false,
-    "ContentTaskUtils": false,
-    "Components": false,
-    "EventUtils": false,
-    "executeSoon": false,
-    "expectUncaughtException": false,
-    "export_assertions": false,
-    "extractJarToTmp": false,
-    "finish": false,
-    "getJar": false,
-    "getRootDirectory": false,
-    "getTestFilePath": false,
-    "gBrowser": false,
-    "gTestPath": false,
-    "info": false,
-    "is": false,
-    "isnot": false,
-    "ok": false,
-    "OpenBrowserWindow": false,
-    "Preferences": false,
-    "registerCleanupFunction": false,
-    "requestLongerTimeout": false,
-    "Services": false,
-    "SimpleTest": false,
-    "SpecialPowers": false,
-    "TestUtils": false,
-    "todo": false,
-    "todo_is": false,
-    "todo_isnot": false,
-    "waitForClipboard": false,
-    "waitForExplicitFinish": false,
-    "waitForFocus": false
-  }
+  "extends": [
+    "plugin:mozilla/browser-test"
+  ]
 };
--- a/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
@@ -1,7 +1,9 @@
 [DEFAULT]
 support-files =
   blue_page.html
+  head.js
 
 [browser_as_load_location.js]
+[browser_as_render.js]
 [browser_getScreenshots.js]
 skip-if=true # issue 2851
--- a/browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
@@ -1,34 +1,53 @@
 "use strict";
 
-let Cu = Components.utils;
-Cu.import("resource://gre/modules/Services.jsm");
-
 /**
- * Tests that opening a new tab opens a page with the expected activity stream
- * content.
+ * Helper to test that a newtab page loads its html document.
  *
- * XXX /browser/components/newtab/tests/browser/browser_newtab_overrides in
- * mozilla-central is where this test was adapted from.  Once we get decide on
- * and implement how we're going to set the URL in mozilla-central, we may well
- * want to (separately from this test), clone/adapt that entire file for our
- * new setup.
+ * @param selector {String} CSS selector to find an element in newtab content
+ * @param message {String} Description of the test printed with the assertion
  */
-add_task(async function checkActivityStreamLoads() {
-  const asURL = "resource://activity-stream/data/content/activity-stream.html";
-
+async function checkNewtabLoads(selector, message) {
   // simulate a newtab open as a user would
   BrowserOpenTab();
 
   // wait until the browser loads
   let browser = gBrowser.selectedBrowser;
-  await BrowserTestUtils.browserLoaded(browser);
+  await waitForPreloaded(browser);
 
   // check what the content task thinks has been loaded.
-  await ContentTask.spawn(browser, {url: asURL}, args => {
-    Assert.ok(content.document.querySelector("body.activity-stream"),
-      'Got <body class="activity-stream" Element');
-  });
+  let found = await ContentTask.spawn(browser, selector, arg =>
+    content.document.querySelector(arg) !== null);
+  ok(found, message);
 
   // avoid leakage
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+// Test with activity stream on
+async function checkActivityStreamLoads() {
+  await checkNewtabLoads("body.activity-stream", "Got <body class='activity-stream'> Element");
+}
+
+// Run a first time not from a preloaded browser
+add_task(async function checkActivityStreamNotPreloadedLoad() {
+  gBrowser.removePreloadedBrowser();
+  await checkActivityStreamLoads();
 });
+
+// Run a second time from a preloaded browser
+add_task(checkActivityStreamLoads);
+
+// Test with activity stream off
+async function checkNotActivityStreamLoads() {
+  await SpecialPowers.pushPrefEnv({set: [["browser.newtabpage.activity-stream.enabled", false]]});
+  await checkNewtabLoads("body:not(.activity-stream)", "Got <body> Element not for activity-stream");
+}
+
+// Run a first time not from a preloaded browser
+add_task(async function checkNotActivityStreamNotPreloadedLoad() {
+  gBrowser.removePreloadedBrowser();
+  await checkNotActivityStreamLoads();
+});
+
+// Run a second time from a preloaded browser
+add_task(checkNotActivityStreamLoads);
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser_as_render.js
@@ -0,0 +1,29 @@
+"use strict";
+
+test_newtab(function test_render_search() {
+  let search = content.document.getElementById("newtab-search-text");
+  ok(search, "Got the search box");
+  isnot(search.placeholder, "search_web_placeholder", "Search box is localized");
+});
+
+test_newtab(function test_render_topsites() {
+  let topSites = content.document.querySelector(".top-sites-list");
+  ok(topSites, "Got the top sites section");
+});
+
+test_newtab({
+  async before({pushPrefs}) {
+    await pushPrefs(["browser.newtabpage.activity-stream.showTopSites", false]);
+  },
+  test: function test_render_no_topsites() {
+    let topSites = content.document.querySelector(".top-sites-list");
+    ok(!topSites, "No top sites section");
+  }
+});
+
+// This next test runs immediately after test_render_no_topsites to make sure
+// the topsites pref is restored
+test_newtab(function test_render_topsites_again() {
+  let topSites = content.document.querySelector(".top-sites-list");
+  ok(topSites, "Got the top sites section again");
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/head.js
@@ -0,0 +1,95 @@
+"use strict";
+
+function popPrefs() {
+  return SpecialPowers.popPrefEnv();
+}
+function pushPrefs(...prefs) {
+  return SpecialPowers.pushPrefEnv({set: prefs});
+}
+
+// Activity Stream tests expect it to be enabled, and make sure to clear out any
+// preloaded browsers that might have about:newtab that we don't want to test
+const ACTIVITY_STREAM_PREF = "browser.newtabpage.activity-stream.enabled";
+pushPrefs([ACTIVITY_STREAM_PREF, true]);
+gBrowser.removePreloadedBrowser();
+
+/**
+ * Helper to wait for potentially preloaded browsers to "load" where a preloaded
+ * page has already loaded and won't trigger "load", and a "load"ed page might
+ * not necessarily have had all its javascript/render logic executed.
+ */
+async function waitForPreloaded(browser) {
+  let readyState = await ContentTask.spawn(browser, {}, () => content.document.readyState);
+  if (readyState !== "complete") {
+    await BrowserTestUtils.browserLoaded(browser);
+  }
+}
+
+/**
+ * Helper to run Activity Stream about:newtab test tasks in content.
+ *
+ * @param testInfo {Function|Object}
+ *   {Function} This parameter will be used as if the function were called with
+ *              an Object with this parameter as "test" key's value.
+ *   {Object} The following keys are expected:
+ *     before {Function} Optional. Runs before and returns an arg for "test"
+ *     test   {Function} The test to run in the about:newtab content task taking
+ *                       an arg from "before" and returns a result to "after"
+ *     after  {Function} Optional. Runs after and with the result of "test"
+ */
+function test_newtab(testInfo) { // eslint-disable-line no-unused-vars
+  // Extract any test parts or default to just the single content task
+  let {before, test: contentTask, after} = testInfo;
+  if (!before) {
+    before = () => ({});
+  }
+  if (!contentTask) {
+    contentTask = testInfo;
+  }
+  if (!after) {
+    after = () => {};
+  }
+
+  // Helper to push prefs for just this test and pop them when done
+  let needPopPrefs = false;
+  let scopedPushPrefs = async(...args) => {
+    needPopPrefs = true;
+    await pushPrefs(...args);
+  };
+  let scopedPopPrefs = async() => {
+    if (needPopPrefs) {
+      await popPrefs();
+    }
+  };
+
+  // Make the test task with optional before/after and content task to run in a
+  // new tab that opens and closes.
+  let testTask = async() => {
+    // Open about:newtab without using the default load listener
+    let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+
+    // Specially wait for potentially preloaded browsers
+    let browser = tab.linkedBrowser;
+    await waitForPreloaded(browser);
+
+    // Wait for React to render something
+    await BrowserTestUtils.waitForCondition(() => ContentTask.spawn(browser, {},
+      () => content.document.getElementById("root").children.length),
+      "Should render activity stream content");
+
+    // Chain together before -> contentTask -> after data passing
+    try {
+      let contentArg = await before({pushPrefs: scopedPushPrefs, tab});
+      let contentResult = await ContentTask.spawn(browser, contentArg, contentTask);
+      await after(contentResult);
+    } finally {
+      // Clean up for next tests
+      await scopedPopPrefs();
+      await BrowserTestUtils.removeTab(tab);
+    }
+  };
+
+  // Copy the name of the content task to identify the test
+  Object.defineProperty(testTask, "name", {value: contentTask.name});
+  add_task(testTask);
+}
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -1,22 +1,22 @@
 const injector = require("inject!lib/ActivityStream.jsm");
 
 const REASON_ADDON_UNINSTALL = 6;
 
 describe("ActivityStream", () => {
   let sandbox;
   let as;
   let ActivityStream;
-  let SECTIONS;
+  let PREFS_CONFIG;
   function Fake() {}
 
   beforeEach(() => {
     sandbox = sinon.sandbox.create();
-    ({ActivityStream, SECTIONS} = injector({
+    ({ActivityStream, PREFS_CONFIG} = injector({
       "lib/LocalizationFeed.jsm": {LocalizationFeed: Fake},
       "lib/ManualMigration.jsm": {ManualMigration: Fake},
       "lib/NewTabInit.jsm": {NewTabInit: Fake},
       "lib/PlacesFeed.jsm": {PlacesFeed: Fake},
       "lib/PrefsFeed.jsm": {PrefsFeed: Fake},
       "lib/SnippetsFeed.jsm": {SnippetsFeed: Fake},
       "lib/SystemTickFeed.jsm": {SystemTickFeed: Fake},
       "lib/TelemetryFeed.jsm": {TelemetryFeed: Fake},
@@ -106,30 +106,113 @@ describe("ActivityStream", () => {
     it("should create a Telemetry feed", () => {
       const feed = as.feeds.get("feeds.telemetry")();
       assert.instanceOf(feed, Fake);
     });
     it("should create a Prefs feed", () => {
       const feed = as.feeds.get("feeds.prefs")();
       assert.instanceOf(feed, Fake);
     });
-    it("should create a section feed for each section in SECTIONS", () => {
+    it("should create a section feed for each section in PREFS_CONFIG", () => {
       // If new sections are added, their feeds will have to be added to the
       // list of injected feeds above for this test to pass
-      SECTIONS.forEach((value, key) => {
-        const feed = as.feeds.get(`feeds.section.${key}`)();
-        assert.instanceOf(feed, Fake);
-      });
+      let feedCount = 0;
+      for (const pref of PREFS_CONFIG.keys()) {
+        if (pref.search(/^feeds\.section\.[^.]+$/) === 0) {
+          const feed = as.feeds.get(pref)();
+          assert.instanceOf(feed, Fake);
+          feedCount++;
+        }
+      }
+      assert.isAbove(feedCount, 0);
     });
     it("should create a ManualMigration feed", () => {
       const feed = as.feeds.get("feeds.migration")();
       assert.instanceOf(feed, Fake);
     });
     it("should create a Snippets feed", () => {
       const feed = as.feeds.get("feeds.snippets")();
       assert.instanceOf(feed, Fake);
     });
     it("should create a SystemTick feed", () => {
       const feed = as.feeds.get("feeds.systemtick")();
       assert.instanceOf(feed, Fake);
     });
   });
+  describe("_updateDynamicPrefs topstories default value", () => {
+    it("should be false with no geo/locale", () => {
+      as._updateDynamicPrefs();
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should be false with unexpected geo", () => {
+      sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("NOGEO");
+
+      as._updateDynamicPrefs();
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should be false with expected geo and unexpected locale", () => {
+      sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("US");
+      sandbox.stub(global.Services.locale, "getRequestedLocale").returns("no-LOCALE");
+
+      as._updateDynamicPrefs();
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should be true with expected geo and locale", () => {
+      sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("US");
+      sandbox.stub(global.Services.locale, "getRequestedLocale").returns("en-US");
+
+      as._updateDynamicPrefs();
+
+      assert.isTrue(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should be false after expected geo and locale then unexpected", () => {
+      sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+      sandbox.stub(global.Services.prefs, "getStringPref")
+        .onFirstCall()
+        .returns("US")
+        .onSecondCall()
+        .returns("NOGEO");
+      sandbox.stub(global.Services.locale, "getRequestedLocale").returns("en-US");
+
+      as._updateDynamicPrefs();
+      as._updateDynamicPrefs();
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+  });
+  describe("_updateDynamicPrefs topstories delayed default value", () => {
+    let clock;
+    beforeEach(() => {
+      clock = sinon.useFakeTimers();
+
+      // Have addObserver cause prefHasUserValue to now return true then observe
+      sandbox.stub(global.Services.prefs, "addObserver").callsFake((pref, obs) => {
+        sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+        setTimeout(() => obs.observe(null, "nsPref:changed", pref)); // eslint-disable-line max-nested-callbacks
+      });
+    });
+    afterEach(() => clock.restore());
+
+    it("should set false with unexpected geo", () => {
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("NOGEO");
+
+      as._updateDynamicPrefs();
+      clock.tick(1);
+
+      assert.isFalse(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+    it("should set true with expected geo and locale", () => {
+      sandbox.stub(global.Services.prefs, "getStringPref").returns("US");
+      sandbox.stub(global.Services.locale, "getRequestedLocale").returns("en-US");
+
+      as._updateDynamicPrefs();
+      clock.tick(1);
+
+      assert.isTrue(PREFS_CONFIG.get("feeds.section.topstories").value);
+    });
+  });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
@@ -20,18 +20,16 @@ describe("Localization Feed", () => {
   let feed;
   let globals;
   let sandbox;
   beforeEach(() => {
     globals = new GlobalOverrider();
     sandbox = globals.sandbox;
     feed = new LocalizationFeed();
     feed.store = {dispatch: sinon.spy()};
-
-    sandbox.stub(global.Services.locale, "getRequestedLocale");
   });
   afterEach(() => {
     globals.restore();
   });
 
   it("should fetch strings on init", async () => {
     sandbox.stub(feed, "updateLocale");
     sandbox.stub(global, "fetch");
@@ -45,55 +43,59 @@ describe("Localization Feed", () => {
 
   describe("#updateLocale", () => {
     beforeEach(() => {
       feed.allStrings = TEST_STRINGS;
     });
 
     it("should dispatch with locale and strings for default", () => {
       const locale = DEFAULT_LOCALE;
+      sandbox.stub(global.Services.locale, "negotiateLanguages")
+        .returns([locale]);
       feed.updateLocale();
 
       assert.calledOnce(feed.store.dispatch);
       const arg = feed.store.dispatch.firstCall.args[0];
       assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
       assert.propertyVal(arg.data, "locale", locale);
       assert.deepEqual(arg.data.strings, TEST_STRINGS[locale]);
     });
     it("should use strings for other locale", () => {
       const locale = "it";
-      global.Services.locale.getRequestedLocale.returns(locale);
+      sandbox.stub(global.Services.locale, "negotiateLanguages")
+        .returns([locale]);
 
       feed.updateLocale();
 
       assert.calledOnce(feed.store.dispatch);
       const arg = feed.store.dispatch.firstCall.args[0];
       assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
       assert.propertyVal(arg.data, "locale", locale);
       assert.deepEqual(arg.data.strings, TEST_STRINGS[locale]);
     });
     it("should use some fallback strings for partial locale", () => {
       const locale = "ru";
-      global.Services.locale.getRequestedLocale.returns(locale);
+      sandbox.stub(global.Services.locale, "negotiateLanguages")
+        .returns([locale]);
 
       feed.updateLocale();
 
       assert.calledOnce(feed.store.dispatch);
       const arg = feed.store.dispatch.firstCall.args[0];
       assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
       assert.propertyVal(arg.data, "locale", locale);
       assert.deepEqual(arg.data.strings, {
         foo: TEST_STRINGS[locale].foo,
         too: TEST_STRINGS[DEFAULT_LOCALE].too
       });
     });
     it("should use all default strings for unknown locale", () => {
       const locale = "xyz";
-      global.Services.locale.getRequestedLocale.returns(locale);
-
+      sandbox.stub(global.Services.locale, "negotiateLanguages")
+        .returns([locale]);
       feed.updateLocale();
 
       assert.calledOnce(feed.store.dispatch);
       const arg = feed.store.dispatch.firstCall.args[0];
       assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
       assert.propertyVal(arg.data, "locale", locale);
       assert.deepEqual(arg.data.strings, TEST_STRINGS[DEFAULT_LOCALE]);
     });
--- a/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
@@ -120,32 +120,32 @@ describe("PlacesFeed", () => {
       sinon.stub(openWindowAction._target.browser.ownerGlobal, "openLinkIn");
       feed.onAction(openWindowAction);
       assert.calledOnce(openWindowAction._target.browser.ownerGlobal.openLinkIn);
     });
     it("should open link on OPEN_LINK", () => {
       sinon.stub(feed, "openNewWindow");
       const openLinkAction = {
         type: at.OPEN_LINK,
-        data: {url: "foo.com"},
-        _target: {browser: {loadURI: sinon.spy()}}
+        data: {url: "foo.com", event: {where: "current"}},
+        _target: {browser: {ownerGlobal: {openLinkIn: sinon.spy(), whereToOpenLink: e => e.where}}}
       };
       feed.onAction(openLinkAction);
-      assert.calledWith(openLinkAction._target.browser.loadURI, openLinkAction.data.url);
+      assert.calledWith(openLinkAction._target.browser.ownerGlobal.openLinkIn, openLinkAction.data.url, "current");
     });
     it("should open link with referrer on OPEN_LINK", () => {
       globals.set("Services", {io: {newURI: url => `URI:${url}`}});
       sinon.stub(feed, "openNewWindow");
       const openLinkAction = {
         type: at.OPEN_LINK,
-        data: {url: "foo.com", referrer: "foo.com/ref"},
-        _target: {browser: {loadURI: sinon.spy()}}
+        data: {url: "foo.com", referrer: "foo.com/ref", event: {where: "tab"}},
+        _target: {browser: {ownerGlobal: {openLinkIn: sinon.spy(), whereToOpenLink: e => e.where}}}
       };
       feed.onAction(openLinkAction);
-      assert.calledWith(openLinkAction._target.browser.loadURI, openLinkAction.data.url, `URI:${openLinkAction.data.referrer}`);
+      assert.calledWith(openLinkAction._target.browser.ownerGlobal.openLinkIn, openLinkAction.data.url, "tab", {referrerURI: `URI:${openLinkAction.data.referrer}`});
     });
     it("should save to Pocket on SAVE_TO_POCKET", () => {
       feed.onAction({type: at.SAVE_TO_POCKET, data: {site: {url: "raspberry.com", title: "raspberry"}}, _target: {browser: {}}});
       assert.calledWith(global.Pocket.savePage, {}, "raspberry.com", "raspberry");
     });
   });
 
   describe("#observe", () => {
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -44,34 +44,47 @@ describe("Top Sites Feed", () => {
     links = FAKE_LINKS;
     clock = sinon.useFakeTimers();
   });
   afterEach(() => {
     globals.restore();
     clock.restore();
   });
 
-  describe("#init", () => {
-    it("should add defaults on INIT", () => {
-      feed.onAction({type: at.INIT});
-      assert.ok(DEFAULT_TOP_SITES.length);
+  describe("#refreshDefaults", () => {
+    it("should add defaults on PREFS_INITIAL_VALUES", () => {
+      feed.onAction({type: at.PREFS_INITIAL_VALUES, data: {"default.sites": "https://foo.com"}});
+
+      assert.isAbove(DEFAULT_TOP_SITES.length, 0);
+    });
+    it("should add defaults on PREF_CHANGED", () => {
+      feed.onAction({type: at.PREF_CHANGED, data: {name: "default.sites", value: "https://foo.com"}});
+
+      assert.isAbove(DEFAULT_TOP_SITES.length, 0);
     });
     it("should have default sites with .isDefault = true", () => {
-      feed.init();
+      feed.refreshDefaults("https://foo.com");
+
       DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "isDefault", true));
     });
     it("should add no defaults on empty pref", () => {
-      FakePrefs.prototype.prefs["default.sites"] = "";
-      feed.init();
+      feed.refreshDefaults("");
+
+      assert.equal(DEFAULT_TOP_SITES.length, 0);
+    });
+    it("should clear defaults", () => {
+      feed.refreshDefaults("https://foo.com");
+      feed.refreshDefaults("");
+
       assert.equal(DEFAULT_TOP_SITES.length, 0);
     });
   });
   describe("#getLinksWithDefaults", () => {
     beforeEach(() => {
-      feed.init();
+      feed.refreshDefaults("https://foo.com");
     });
 
     it("should get the links from NewTabUtils", async () => {
       const result = await feed.getLinksWithDefaults();
       assert.deepEqual(result, links);
       assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
     });
     it("should add defaults if there are are not enough links", async () => {
--- a/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
@@ -4,54 +4,58 @@ const {FakePrefs} = require("test/unit/u
 const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
 const {GlobalOverrider} = require("test/unit/utils");
 
 describe("Top Stories Feed", () => {
   let TopStoriesFeed;
   let STORIES_UPDATE_TIME;
   let TOPICS_UPDATE_TIME;
   let SECTION_ID;
+  let FEED_PREF;
+  let SECTION_OPTIONS_PREF;
   let instance;
   let clock;
   let globals;
 
   beforeEach(() => {
     FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
-      "stories_endpoint": "https://somedomain.org/stories?key=$apiKey&locale=$locale",
+      "stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
       "stories_referrer": "https://somedomain.org/referrer",
-      "topics_endpoint": "https://somedomain.org/topics?key=$apiKey&locale=$locale",
+      "topics_endpoint": "https://somedomain.org/topics?key=$apiKey",
       "survey_link": "https://www.surveymonkey.com/r/newtabffx",
       "api_key_pref": "apiKeyPref",
       "provider_name": "test-provider",
       "provider_icon": "provider-icon",
       "provider_description": "provider_desc"
     }`;
     FakePrefs.prototype.prefs.apiKeyPref = "test-api-key";
 
     globals = new GlobalOverrider();
     globals.set("Services", {locale: {getRequestedLocale: () => "en-CA"}});
     clock = sinon.useFakeTimers();
 
-    ({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
+    ({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID, FEED_PREF, SECTION_OPTIONS_PREF} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
     instance = new TopStoriesFeed();
     instance.store = {getState() { return {}; }, dispatch: sinon.spy()};
+    instance.storiesLastUpdated = 0;
+    instance.topicsLastUpdated = 0;
   });
   afterEach(() => {
     globals.restore();
     clock.restore();
   });
   describe("#init", () => {
     it("should create a TopStoriesFeed", () => {
       assert.instanceOf(instance, TopStoriesFeed);
     });
     it("should initialize endpoints based on prefs", () => {
       instance.onAction({type: at.INIT});
-      assert.equal("https://somedomain.org/stories?key=test-api-key&locale=en-CA", instance.stories_endpoint);
+      assert.equal("https://somedomain.org/stories?key=test-api-key", instance.stories_endpoint);
       assert.equal("https://somedomain.org/referrer", instance.stories_referrer);
-      assert.equal("https://somedomain.org/topics?key=test-api-key&locale=en-CA", instance.topics_endpoint);
+      assert.equal("https://somedomain.org/topics?key=test-api-key", instance.topics_endpoint);
     });
     it("should register section", () => {
       const expectedSectionOptions = {
         id: SECTION_ID,
         eventSource: "TOP_STORIES",
         icon: "provider-icon",
         title: {id: "header_recommended_by", values: {provider: "test-provider"}},
         rows: [],
@@ -112,39 +116,32 @@ describe("Top Stories Feed", () => {
       FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
         "stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
         "topics_endpoint": "https://somedomain.org/topics?key=$apiKey"
       }`;
       instance.init();
 
       assert.called(Components.utils.reportError);
     });
-    it("should report error for missing locale", () => {
-      let fakeServices = {locale: {getRequestedLocale: sinon.spy()}};
-      globals.set("Services", fakeServices);
-      globals.sandbox.spy(global.Components.utils, "reportError");
-      FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
-        "stories_endpoint": "https://somedomain.org/stories?locale=$locale",
-        "topics_endpoint": "https://somedomain.org/topics?locale=$locale"
-      }`;
-      instance.init();
-
-      assert.called(Components.utils.reportError);
-    });
     it("should deregister section", () => {
       instance.onAction({type: at.UNINIT});
       assert.calledOnce(instance.store.dispatch);
       assert.calledWith(instance.store.dispatch, ac.BroadcastToContent({
         type: at.SECTION_DEREGISTER,
         data: SECTION_ID
       }));
     });
     it("should initialize on FEED_INIT", () => {
       instance.init = sinon.spy();
-      instance.onAction({type: at.FEED_INIT, data: "feeds.section.topstories"});
+      instance.onAction({type: at.FEED_INIT, data: FEED_PREF});
+      assert.calledOnce(instance.init);
+    });
+    it("should initialize on PREF_CHANGED", () => {
+      instance.init = sinon.spy();
+      instance.onAction({type: at.PREF_CHANGED, data: {name: SECTION_OPTIONS_PREF}});
       assert.calledOnce(instance.init);
     });
   });
   describe("#fetch", () => {
     it("should fetch stories and send event", async () => {
       let fetchStub = globals.sandbox.stub();
       globals.set("fetch", fetchStub);
       globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
@@ -158,17 +155,18 @@ describe("Top Stories Feed", () => {
       }]}`;
       const stories = [{
         "guid": "1",
         "type": "now",
         "title": "title",
         "description": "description",
         "image": "image-url",
         "referrer": "referrer",
-        "url": "rec-url"
+        "url": "rec-url",
+        "eTLD": ""
       }];
 
       instance.stories_endpoint = "stories-endpoint";
       instance.stories_referrer = "referrer";
       fetchStub.resolves({ok: true, status: 200, text: () => response});
       await instance.fetchStories();
 
       assert.calledOnce(fetchStub);
@@ -272,26 +270,24 @@ describe("Top Stories Feed", () => {
       assert.calledWithExactly(fetchStub, instance.topics_endpoint);
       assert.notCalled(instance.store.dispatch);
       assert.called(Components.utils.reportError);
     });
   });
   describe("#update", () => {
     it("should fetch stories after update interval", () => {
       instance.fetchStories = sinon.spy();
-      instance.fetchTopics = sinon.spy();
       instance.onAction({type: at.SYSTEM_TICK});
       assert.notCalled(instance.fetchStories);
 
       clock.tick(STORIES_UPDATE_TIME);
       instance.onAction({type: at.SYSTEM_TICK});
       assert.calledOnce(instance.fetchStories);
     });
     it("should fetch topics after update interval", () => {
-      instance.fetchStories = sinon.spy();
       instance.fetchTopics = sinon.spy();
       instance.onAction({type: at.SYSTEM_TICK});
       assert.notCalled(instance.fetchTopics);
 
       clock.tick(TOPICS_UPDATE_TIME);
       instance.onAction({type: at.SYSTEM_TICK});
       assert.calledOnce(instance.fetchTopics);
     });
--- a/browser/extensions/activity-stream/test/unit/unit-entry.js
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -23,42 +23,49 @@ overrider.set({
     }
   },
   // eslint-disable-next-line object-shorthand
   ContentSearchUIController: function() {}, // NB: This is a function/constructor
   dump() {},
   fetch() {},
   Preferences: FakePrefs,
   Services: {
-    locale: {getRequestedLocale() {}},
+    locale: {
+      getAppLocalesAsLangTags() {},
+      getRequestedLocale() {},
+      negotiateLanguages() {}
+    },
     urlFormatter: {formatURL: str => str},
     mm: {
       addMessageListener: (msg, cb) => cb(),
       removeMessageListener() {}
     },
     appShell: {hiddenDOMWindow: {performance: new FakePerformance()}},
     obs: {
       addObserver() {},
       removeObserver() {}
     },
     prefs: {
       addObserver() {},
+      prefHasUserValue() {},
       removeObserver() {},
       getStringPref() {},
       getBoolPref() {},
       getDefaultBranch() {
         return {
           setBoolPref() {},
           setIntPref() {},
           setStringPref() {},
           clearUserPref() {}
         };
       }
     },
-    tm: {dispatchToMainThread: cb => cb()}
+    tm: {dispatchToMainThread: cb => cb()},
+    eTLD: {getPublicSuffix() {}},
+    io: {NewURI() {}}
   },
   XPCOMUtils: {
     defineLazyModuleGetter() {},
     defineLazyServiceGetter() {},
     generateQI() { return {}; }
   }
 });