--- 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 {}; }
}
});