--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -82,16 +82,20 @@ var whitelist = [
// Add-on API introduced in bug 1118285
{file: "resource://app/modules/NewTabURL.jsm"},
// browser/components/newtab bug 1355166
{file: "resource://app/modules/NewTabSearchProvider.jsm"},
{file: "resource://app/modules/NewTabWebChannel.jsm"},
+ // browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
+ // This will used when Bug 1397875 lands
+ {file: "resource://activity-stream/data/content/activity-stream-prerendered.html"},
+
// layout/mathml/nsMathMLChar.cpp
{file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties"},
{file: "resource://gre/res/fonts/mathfontUnicode.properties"},
// toolkit/components/places/ColorAnalyzer_worker.js
{file: "resource://gre/modules/ClusterLib.js"},
{file: "resource://gre/modules/ColorConversion.js"},
--- a/browser/components/search/test/browser_searchEngine_behaviors.js
+++ b/browser/components/search/test/browser_searchEngine_behaviors.js
@@ -90,24 +90,19 @@ function promiseStateChangeURI() {
}
gBrowser.addProgressListener(listener);
});
}
function promiseContentSearchReady(browser) {
return ContentTask.spawn(browser, {}, async function(args) {
- return new Promise(resolve => {
- content.addEventListener("ContentSearchService", function listener(aEvent) {
- if (aEvent.detail.type == "State") {
- content.removeEventListener("ContentSearchService", listener);
- resolve();
- }
- });
- });
+ await ContentTaskUtils.waitForCondition(() => content.wrappedJSObject.gContentSearchController &&
+ content.wrappedJSObject.gContentSearchController.defaultEngine
+ );
});
}
add_task(async function() {
await SpecialPowers.pushPrefEnv({ set: [
["browser.search.widget.inNavBar", true],
]});
});
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -34,16 +34,17 @@ for (const type of [
"INIT",
"LOCALE_UPDATED",
"MIGRATION_CANCEL",
"MIGRATION_COMPLETED",
"MIGRATION_START",
"NEW_TAB_INIT",
"NEW_TAB_INITIAL_STATE",
"NEW_TAB_LOAD",
+ "NEW_TAB_STATE_REQUEST",
"NEW_TAB_UNLOAD",
"OPEN_LINK",
"OPEN_NEW_WINDOW",
"OPEN_PRIVATE_WINDOW",
"PINNED_SITES_UPDATED",
"PLACES_BOOKMARK_ADDED",
"PLACES_BOOKMARK_CHANGED",
"PLACES_BOOKMARK_REMOVED",
--- a/browser/extensions/activity-stream/common/PerfService.jsm
+++ b/browser/extensions/activity-stream/common/PerfService.jsm
@@ -1,31 +1,33 @@
/* globals Services */
"use strict";
+/* istanbul ignore if */
+if (typeof Components !== "undefined" && Components.utils) {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+}
+
let usablePerfObj;
-let Cu;
-const isRunningInChrome = typeof Window === "undefined";
-
/* istanbul ignore if */
-if (isRunningInChrome) {
- Cu = Components.utils;
-} else {
- Cu = {import() {}};
-}
-
-Cu.import("resource://gre/modules/Services.jsm");
-
-/* istanbul ignore if */
-if (isRunningInChrome) {
+/* istanbul ignore else */
+if (typeof Services !== "undefined") {
// Borrow the high-resolution timer from the hidden window....
usablePerfObj = Services.appShell.hiddenDOMWindow.performance;
-} else { // we must be running in content space
+} else if (typeof performance !== "undefined") {
+ // we must be running in content space
usablePerfObj = performance;
+} else {
+ // This is a dummy object so this file doesn't crash in the node prerendering
+ // task.
+ usablePerfObj = {
+ now() {},
+ mark() {}
+ };
}
this._PerfService = function _PerfService(options) {
// For testing, so that we can use a fake Window.performance object with
// known state.
if (options && options.performanceObj) {
this._perf = options.performanceObj;
} else {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/common/PrerenderData.jsm
@@ -0,0 +1,42 @@
+const prefConfig = {
+ // Prefs listed with "invalidates: true" will prevent the prerendered version
+ // of AS from being used if their value is something other than what is listed
+ // here. This is required because some preferences cause the page layout to be
+ // too different for the prerendered version to be used. Unfortunately, this
+ // will result in users who have modified some of their preferences not being
+ // able to get the benefits of prerendering.
+ "migrationExpired": {value: true},
+ "showTopSites": {
+ value: true,
+ invalidates: true
+ },
+ "showSearch": {
+ value: true,
+ invalidates: true
+ },
+ "topSitesCount": {value: 6},
+ "feeds.section.topstories": {
+ value: true,
+ invalidates: true
+ }
+};
+
+this.PrerenderData = {
+ invalidatingPrefs: Object.keys(prefConfig).filter(key => prefConfig[key].invalidates),
+ initialPrefs: Object.keys(prefConfig).reduce((obj, key) => {
+ obj[key] = prefConfig[key].value;
+ return obj;
+ }, {}), // This creates an object of the form {prefName: value}
+ initialSections: [
+ {
+ enabled: true,
+ icon: "pocket",
+ id: "topstories",
+ order: 1,
+ title: {id: "header_recommended_by", values: {provider: "Pocket"}},
+ topics: [{}]
+ }
+ ]
+};
+
+this.EXPORTED_SYMBOLS = ["PrerenderData"];
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -1,26 +1,31 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {actionTypes: at} = Components.utils.import("resource://activity-stream/common/Actions.jsm", {});
+// Locales that should be displayed RTL
+const RTL_LIST = ["ar", "he", "fa", "ur"];
+
const TOP_SITES_DEFAULT_LENGTH = 6;
const TOP_SITES_SHOWMORE_LENGTH = 12;
const INITIAL_STATE = {
App: {
// Have we received real data from the app yet?
initialized: false,
// The locale of the browser
locale: "",
// Localized strings with defaults
strings: null,
+ // The text direction for the locale
+ textDirection: "",
// The version of the system-addon
version: null
},
Snippets: {initialized: false},
TopSites: {
// Have we received real data from history yet?
initialized: false,
// The history (and possibly default) links
@@ -43,17 +48,18 @@ function App(prevState = INITIAL_STATE.A
return Object.assign({}, prevState, action.data || {}, {initialized: true});
case at.LOCALE_UPDATED: {
if (!action.data) {
return prevState;
}
let {locale, strings} = action.data;
return Object.assign({}, prevState, {
locale,
- strings
+ strings,
+ textDirection: RTL_LIST.indexOf(locale.split("-")[0]) >= 0 ? "rtl" : "ltr"
});
}
default:
return prevState;
}
}
/**
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream-initial-state.js
@@ -0,0 +1,142 @@
+// Note - this is a generated file.
+ window.gActivityStreamPrerenderedState = {
+ "TopSites": {
+ "initialized": false,
+ "rows": []
+ },
+ "App": {
+ "initialized": false,
+ "locale": "en-PRERENDER",
+ "strings": {
+ "newtab_page_title": " ",
+ "default_label_loading": " ",
+ "header_top_sites": " ",
+ "header_stories": " ",
+ "header_highlights": " ",
+ "header_visit_again": " ",
+ "header_bookmarks": " ",
+ "header_recommended_by": " ",
+ "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": " ",
+ "menu_action_pin": " ",
+ "menu_action_unpin": " ",
+ "confirm_history_delete_p1": " ",
+ "confirm_history_delete_notice_p2": " ",
+ "menu_action_save_to_pocket": " ",
+ "search_for_something_with": " ",
+ "search_button": " ",
+ "search_header": " ",
+ "search_web_placeholder": "Search the Web",
+ "search_settings": " ",
+ "section_info_option": " ",
+ "section_info_send_feedback": " ",
+ "section_info_privacy_notice": " ",
+ "welcome_title": " ",
+ "welcome_body": " ",
+ "welcome_label": " ",
+ "time_label_less_than_minute": " ",
+ "time_label_minute": " ",
+ "time_label_hour": " ",
+ "time_label_day": " ",
+ "settings_pane_button_label": " ",
+ "settings_pane_header": " ",
+ "settings_pane_body2": " ",
+ "settings_pane_search_header": " ",
+ "settings_pane_search_body": " ",
+ "settings_pane_topsites_header": " ",
+ "settings_pane_topsites_body": " ",
+ "settings_pane_topsites_options_showmore": " ",
+ "settings_pane_bookmarks_header": " ",
+ "settings_pane_bookmarks_body": " ",
+ "settings_pane_visit_again_header": " ",
+ "settings_pane_visit_again_body": " ",
+ "settings_pane_highlights_header": " ",
+ "settings_pane_highlights_body2": " ",
+ "settings_pane_highlights_options_bookmarks": " ",
+ "settings_pane_highlights_options_visited": " ",
+ "settings_pane_snippets_header": " ",
+ "settings_pane_snippets_body": " ",
+ "settings_pane_done_button": " ",
+ "edit_topsites_button_text": " ",
+ "edit_topsites_button_label": " ",
+ "edit_topsites_showmore_button": " ",
+ "edit_topsites_showless_button": " ",
+ "edit_topsites_done_button": " ",
+ "edit_topsites_pin_button": " ",
+ "edit_topsites_unpin_button": " ",
+ "edit_topsites_edit_button": " ",
+ "edit_topsites_dismiss_button": " ",
+ "edit_topsites_add_button": " ",
+ "topsites_form_add_header": " ",
+ "topsites_form_edit_header": " ",
+ "topsites_form_title_placeholder": " ",
+ "topsites_form_url_placeholder": " ",
+ "topsites_form_add_button": " ",
+ "topsites_form_save_button": " ",
+ "topsites_form_cancel_button": " ",
+ "topsites_form_url_validation": " ",
+ "pocket_read_more": " ",
+ "pocket_read_even_more": " ",
+ "pocket_feedback_header": " ",
+ "pocket_description": " ",
+ "highlights_empty_state": " ",
+ "topstories_empty_state": " ",
+ "manual_migration_explanation2": " ",
+ "manual_migration_cancel_button": " ",
+ "manual_migration_import_button": " "
+ },
+ "textDirection": "ltr",
+ "version": null
+ },
+ "Snippets": {
+ "initialized": false
+ },
+ "Prefs": {
+ "initialized": true,
+ "values": {
+ "migrationExpired": true,
+ "showTopSites": true,
+ "showSearch": true,
+ "topSitesCount": 6,
+ "feeds.section.topstories": true
+ }
+ },
+ "Dialog": {
+ "visible": false,
+ "data": {}
+ },
+ "Sections": [
+ {
+ "title": {
+ "id": "header_recommended_by",
+ "values": {
+ "provider": "Pocket"
+ }
+ },
+ "rows": [],
+ "order": 1,
+ "enabled": true,
+ "icon": "pocket",
+ "id": "topstories",
+ "topics": [
+ {}
+ ],
+ "initialized": false
+ }
+ ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang="" 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="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
+ <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
+ <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
+ </head>
+ <body class="activity-stream">
+ <div id="root"><div class="outer-wrapper" data-reactroot="" data-reactid="1" data-react-checksum="57168132"><main data-reactid="2"><div class="search-wrapper" data-reactid="3"><label for="newtab-search-text" class="search-label" data-reactid="4"><span class="sr-only" data-reactid="5"><span data-reactid="6">Search the Web</span></span></label><input type="search" id="newtab-search-text" maxlength="256" placeholder="Search the Web" title="Search the Web" data-reactid="7"/><button id="searchSubmit" class="search-button" title=" " data-reactid="8"><span class="sr-only" data-reactid="9"><span data-reactid="10"> </span></span></button></div><section class="top-sites" data-reactid="11"><h3 class="section-title" data-reactid="12"><span class="icon icon-small-spacer icon-topsites" data-reactid="13"></span><span data-reactid="14"> </span></h3><ul class="top-sites-list" data-reactid="15"><li class="top-site-outer placeholder" data-reactid="16"><a data-reactid="17"><div class="tile" aria-hidden="true" data-reactid="18"><span class="letter-fallback" data-reactid="19"></span><div class="screenshot" style="background-image:none;" data-reactid="20"></div></div><div class="title " data-reactid="21"><span dir="auto" data-reactid="22"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="23"><a data-reactid="24"><div class="tile" aria-hidden="true" data-reactid="25"><span class="letter-fallback" data-reactid="26"></span><div class="screenshot" style="background-image:none;" data-reactid="27"></div></div><div class="title " data-reactid="28"><span dir="auto" data-reactid="29"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="30"><a data-reactid="31"><div class="tile" aria-hidden="true" data-reactid="32"><span class="letter-fallback" data-reactid="33"></span><div class="screenshot" style="background-image:none;" data-reactid="34"></div></div><div class="title " data-reactid="35"><span dir="auto" data-reactid="36"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="37"><a data-reactid="38"><div class="tile" aria-hidden="true" data-reactid="39"><span class="letter-fallback" data-reactid="40"></span><div class="screenshot" style="background-image:none;" data-reactid="41"></div></div><div class="title " data-reactid="42"><span dir="auto" data-reactid="43"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="44"><a data-reactid="45"><div class="tile" aria-hidden="true" data-reactid="46"><span class="letter-fallback" data-reactid="47"></span><div class="screenshot" style="background-image:none;" data-reactid="48"></div></div><div class="title " data-reactid="49"><span dir="auto" data-reactid="50"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="51"><a data-reactid="52"><div class="tile" aria-hidden="true" data-reactid="53"><span class="letter-fallback" data-reactid="54"></span><div class="screenshot" style="background-image:none;" data-reactid="55"></div></div><div class="title " data-reactid="56"><span dir="auto" data-reactid="57"></span></div></a></li></ul></section><div class="sections-list" data-reactid="58"><section data-reactid="59"><div class="section-top-bar" data-reactid="60"><h3 class="section-title" data-reactid="61"><span class="icon icon-small-spacer icon-pocket" data-reactid="62"></span><span data-reactid="63"> </span></h3></div><ul class="section-list" style="padding:0;" data-reactid="64"><li class="card-outer placeholder" data-reactid="65"><a data-reactid="66"><div class="card" data-reactid="67"><div class="card-details no-image" data-reactid="68"><div class="card-text no-image no-host-name no-context" data-reactid="69"><h4 class="card-title" dir="auto" data-reactid="70"></h4><p class="card-description" dir="auto" data-reactid="71"></p></div></div></div></a></li><li class="card-outer placeholder" data-reactid="72"><a data-reactid="73"><div class="card" data-reactid="74"><div class="card-details no-image" data-reactid="75"><div class="card-text no-image no-host-name no-context" data-reactid="76"><h4 class="card-title" dir="auto" data-reactid="77"></h4><p class="card-description" dir="auto" data-reactid="78"></p></div></div></div></a></li><li class="card-outer placeholder" data-reactid="79"><a data-reactid="80"><div class="card" data-reactid="81"><div class="card-details no-image" data-reactid="82"><div class="card-text no-image no-host-name no-context" data-reactid="83"><h4 class="card-title" dir="auto" data-reactid="84"></h4><p class="card-description" dir="auto" data-reactid="85"></p></div></div></div></a></li></ul><div class="topic" data-reactid="86"><span data-reactid="87"><span data-reactid="88"> </span></span><ul data-reactid="89"><li data-reactid="90"><a class="topic-link" data-reactid="91"></a></li></ul></div></section></div><!-- react-empty: 92 --></main></div></div>
+ <div id="snippets-container">
+ <div id="snippets"></div>
+ </div>
+<script src="resource://activity-stream/data/content/activity-stream-initial-state.js"></script>
+ <script src="chrome://browser/content/contentSearchUI.js"></script>
+ <script src="resource://activity-stream/vendor/react.js"></script>
+ <script src="resource://activity-stream/vendor/react-dom.js"></script>
+ <script src="resource://activity-stream/vendor/react-intl.js"></script>
+ <script src="resource://activity-stream/vendor/redux.js"></script>
+ <script src="resource://activity-stream/vendor/react-redux.js"></script>
+ <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
+ </body>
+</html>
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -60,22 +60,16 @@
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 10);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
-/***/ (function(module, exports) {
-
-module.exports = React;
-
-/***/ }),
-/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -95,17 +89,17 @@ const globalImportContext = typeof Windo
// Create an object that avoids accidental differing key/value pairs:
// {
// INIT: "INIT",
// UNINIT: "UNINIT"
// }
const actionTypes = {};
-for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
+for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
actionTypes[type] = type;
}
// Helper function for creating routed actions between content and main
// Not intended to be used by consumers
function _RouteMessage(action, options) {
const meta = action.meta ? Object.assign({}, action.meta) : {};
if (!options || !options.from || !options.to) {
@@ -186,63 +180,55 @@ function UserEvent(data) {
/**
* UndesiredEvent - A telemetry ping indicating an undesired state.
*
* @param {object} data Fields to include in the ping (value, etc.)
* @param {int} importContext (For testing) Override the import context for testing.
* @return {object} An action. For UI code, a SendToMain action.
*/
-function UndesiredEvent(data) {
- let importContext = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : globalImportContext;
-
+function UndesiredEvent(data, importContext = globalImportContext) {
const action = {
type: actionTypes.TELEMETRY_UNDESIRED_EVENT,
data
};
return importContext === UI_CODE ? SendToMain(action) : action;
}
/**
* PerfEvent - A telemetry ping indicating a performance-related event.
*
* @param {object} data Fields to include in the ping (value, etc.)
* @param {int} importContext (For testing) Override the import context for testing.
* @return {object} An action. For UI code, a SendToMain action.
*/
-function PerfEvent(data) {
- let importContext = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : globalImportContext;
-
+function PerfEvent(data, importContext = globalImportContext) {
const action = {
type: actionTypes.TELEMETRY_PERFORMANCE_EVENT,
data
};
return importContext === UI_CODE ? SendToMain(action) : action;
}
/**
* ImpressionStats - A telemetry ping indicating an impression stats.
*
* @param {object} data Fields to include in the ping
* @param {int} importContext (For testing) Override the import context for testing.
* #return {object} An action. For UI code, a SendToMain action.
*/
-function ImpressionStats(data) {
- let importContext = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : globalImportContext;
-
+function ImpressionStats(data, importContext = globalImportContext) {
const action = {
type: actionTypes.TELEMETRY_IMPRESSION_STATS,
data
};
return importContext === UI_CODE ? SendToMain(action) : action;
}
-function SetPref(name, value) {
- let importContext = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : globalImportContext;
-
+function SetPref(name, value, importContext = globalImportContext) {
const action = { type: actionTypes.SET_PREF, data: { name, value } };
return importContext === UI_CODE ? SendToMain(action) : action;
}
var actionCreators = {
BroadcastToContent,
UserEvent,
UndesiredEvent,
@@ -292,65 +278,101 @@ module.exports = {
globalImportContext,
UI_CODE,
BACKGROUND_PROCESS,
MAIN_MESSAGE_TYPE,
CONTENT_MESSAGE_TYPE
};
/***/ }),
+/* 1 */
+/***/ (function(module, exports) {
+
+module.exports = React;
+
+/***/ }),
/* 2 */
/***/ (function(module, exports) {
module.exports = ReactIntl;
/***/ }),
/* 3 */
/***/ (function(module, exports) {
module.exports = ReactRedux;
/***/ }),
/* 4 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
-"use strict";
+var g;
+
+// This works in non-strict mode
+g = (function() {
+ return this;
+})();
+
+try {
+ // This works if eval is allowed (see CSP)
+ g = g || Function("return this")() || (1,eval)("this");
+} catch(e) {
+ // This works if the window reference is available
+ if(typeof window === "object")
+ g = window;
+}
+
+// g can still be undefined, but nothing to do about it...
+// We return undefined, instead of nothing here, so it's
+// easier to handle this case. if(!global) { ...}
+
+module.exports = g;
+/***/ }),
+/* 5 */
+/***/ (function(module, exports) {
+
module.exports = {
TOP_SITES_SOURCE: "TOP_SITES",
- TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"]
+ TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
+ // minimum size necessary to show a rich icon instead of a screenshot
+ MIN_RICH_FAVICON_SIZE: 96,
+ // minimum size necessary to show any icon in the top left corner with a screenshot
+ MIN_CORNER_FAVICON_SIZE: 32
};
/***/ }),
-/* 5 */
+/* 6 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-var _require = __webpack_require__(1);
+const { actionTypes: at } = __webpack_require__(0);
-const at = _require.actionTypes;
-
+// Locales that should be displayed RTL
+const RTL_LIST = ["ar", "he", "fa", "ur"];
const TOP_SITES_DEFAULT_LENGTH = 6;
const TOP_SITES_SHOWMORE_LENGTH = 12;
const INITIAL_STATE = {
App: {
// Have we received real data from the app yet?
initialized: false,
// The locale of the browser
locale: "",
// Localized strings with defaults
strings: null,
+ // The text direction for the locale
+ textDirection: "",
// The version of the system-addon
version: null
},
Snippets: { initialized: false },
TopSites: {
// Have we received real data from history yet?
initialized: false,
// The history (and possibly default) links
@@ -362,35 +384,30 @@ const INITIAL_STATE = {
},
Dialog: {
visible: false,
data: {}
},
Sections: []
};
-function App() {
- let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.App;
- let action = arguments[1];
-
+function App(prevState = INITIAL_STATE.App, action) {
switch (action.type) {
case at.INIT:
return Object.assign({}, prevState, action.data || {}, { initialized: true });
case at.LOCALE_UPDATED:
{
if (!action.data) {
return prevState;
}
- var _action$data = action.data;
- let locale = _action$data.locale,
- strings = _action$data.strings;
-
+ let { locale, strings } = action.data;
return Object.assign({}, prevState, {
locale,
- strings
+ strings,
+ textDirection: RTL_LIST.indexOf(locale.split("-")[0]) >= 0 ? "rtl" : "ltr"
});
}
default:
return prevState;
}
}
/**
@@ -423,20 +440,17 @@ function insertPinned(links, pinned) {
} else {
newLinks.splice(index, 0, link);
}
});
return newLinks;
}
-function TopSites() {
- let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.TopSites;
- let action = arguments[1];
-
+function TopSites(prevState = INITIAL_STATE.TopSites, action) {
let hasMatch;
let newRows;
let pinned;
switch (action.type) {
case at.TOP_SITES_UPDATED:
if (!action.data) {
return prevState;
}
@@ -451,21 +465,17 @@ function TopSites() {
});
return hasMatch ? Object.assign({}, prevState, { rows: newRows }) : prevState;
case at.PLACES_BOOKMARK_ADDED:
if (!action.data) {
return prevState;
}
newRows = prevState.rows.map(site => {
if (site && site.url === action.data.url) {
- var _action$data2 = action.data;
- const bookmarkGuid = _action$data2.bookmarkGuid,
- bookmarkTitle = _action$data2.bookmarkTitle,
- dateAdded = _action$data2.dateAdded;
-
+ const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
return Object.assign({}, site, { bookmarkGuid, bookmarkTitle, bookmarkDateCreated: dateAdded });
}
return site;
});
return Object.assign({}, prevState, { rows: newRows });
case at.PLACES_BOOKMARK_REMOVED:
if (!action.data) {
return prevState;
@@ -492,53 +502,44 @@ function TopSites() {
pinned = action.data;
newRows = insertPinned(prevState.rows, pinned).slice(0, TOP_SITES_SHOWMORE_LENGTH);
return Object.assign({}, prevState, { rows: newRows });
default:
return prevState;
}
}
-function Dialog() {
- let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Dialog;
- let action = arguments[1];
-
+function Dialog(prevState = INITIAL_STATE.Dialog, action) {
switch (action.type) {
case at.DIALOG_OPEN:
return Object.assign({}, prevState, { visible: true, data: action.data });
case at.DIALOG_CANCEL:
return Object.assign({}, prevState, { visible: false });
case at.DELETE_HISTORY_URL:
return Object.assign({}, INITIAL_STATE.Dialog);
default:
return prevState;
}
}
-function Prefs() {
- let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Prefs;
- let action = arguments[1];
-
+function Prefs(prevState = INITIAL_STATE.Prefs, action) {
let newValues;
switch (action.type) {
case at.PREFS_INITIAL_VALUES:
return Object.assign({}, prevState, { initialized: true, values: action.data });
case at.PREF_CHANGED:
newValues = Object.assign({}, prevState.values);
newValues[action.data.name] = action.data.value;
return Object.assign({}, prevState, { values: newValues });
default:
return prevState;
}
}
-function Sections() {
- let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Sections;
- let action = arguments[1];
-
+function Sections(prevState = INITIAL_STATE.Sections, action) {
let hasMatch;
let newState;
switch (action.type) {
case at.SECTION_DEREGISTER:
return prevState.filter(section => section.id !== action.data);
case at.SECTION_REGISTER:
// If section exists in prevState, update it
newState = prevState.map(section => {
@@ -585,21 +586,17 @@ function Sections() {
case at.PLACES_BOOKMARK_ADDED:
if (!action.data) {
return prevState;
}
return prevState.map(section => Object.assign({}, section, {
rows: section.rows.map(item => {
// find the item within the rows that is attempted to be bookmarked
if (item.url === action.data.url) {
- var _action$data3 = action.data;
- const bookmarkGuid = _action$data3.bookmarkGuid,
- bookmarkTitle = _action$data3.bookmarkTitle,
- dateAdded = _action$data3.dateAdded;
-
+ const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
Object.assign(item, { bookmarkGuid, bookmarkTitle, bookmarkDateCreated: dateAdded });
if (!item.type || item.type === "history") {
item.type = "bookmark";
}
}
return item;
})
}));
@@ -626,20 +623,17 @@ function Sections() {
case at.PLACES_LINK_DELETED:
case at.PLACES_LINK_BLOCKED:
return prevState.map(section => Object.assign({}, section, { rows: section.rows.filter(site => site.url !== action.data.url) }));
default:
return prevState;
}
}
-function Snippets() {
- let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Snippets;
- let action = arguments[1];
-
+function Snippets(prevState = INITIAL_STATE.Snippets, action) {
switch (action.type) {
case at.SNIPPETS_DATA:
return Object.assign({}, prevState, { initialized: true }, action.data);
case at.SNIPPETS_RESET:
return INITIAL_STATE.Snippets;
default:
return prevState;
}
@@ -650,44 +644,46 @@ module.exports = {
reducers,
INITIAL_STATE,
insertPinned,
TOP_SITES_DEFAULT_LENGTH,
TOP_SITES_SHOWMORE_LENGTH
};
/***/ }),
-/* 6 */
+/* 7 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/* globals Services */
-let usablePerfObj;
+/* istanbul ignore if */
-let Cu;
-const isRunningInChrome = typeof Window === "undefined";
+if (typeof Components !== "undefined" && Components.utils) {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+}
+
+let usablePerfObj;
/* istanbul ignore if */
-if (isRunningInChrome) {
- Cu = Components.utils;
-} else {
- Cu = { import() {} };
-}
-
-Cu.import("resource://gre/modules/Services.jsm");
-
-/* istanbul ignore if */
-if (isRunningInChrome) {
+/* istanbul ignore else */
+if (typeof Services !== "undefined") {
// Borrow the high-resolution timer from the hidden window....
usablePerfObj = Services.appShell.hiddenDOMWindow.performance;
-} else {
+} else if (typeof performance !== "undefined") {
// we must be running in content space
usablePerfObj = performance;
+} else {
+ // This is a dummy object so this file doesn't crash in the node prerendering
+ // task.
+ usablePerfObj = {
+ now() {},
+ mark() {}
+ };
}
var _PerfService = function _PerfService(options) {
// For testing, so that we can use a fake Window.performance object with
// known state.
if (options && options.performanceObj) {
this._perf = options.performanceObj;
} else {
@@ -773,73 +769,70 @@ var _PerfService = function _PerfService
var perfService = new _PerfService();
module.exports = {
_PerfService,
perfService
};
/***/ }),
-/* 7 */
+/* 8 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(1);
-
-const ac = _require.actionCreators,
- at = _require.actionTypes;
-
+const React = __webpack_require__(1);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
-const LinkMenu = __webpack_require__(8);
-
-var _require2 = __webpack_require__(4);
+const LinkMenu = __webpack_require__(9);
-const TOP_SITES_SOURCE = _require2.TOP_SITES_SOURCE,
- TOP_SITES_CONTEXT_MENU_OPTIONS = _require2.TOP_SITES_CONTEXT_MENU_OPTIONS;
-
+const { TOP_SITES_SOURCE, TOP_SITES_CONTEXT_MENU_OPTIONS, MIN_RICH_FAVICON_SIZE, MIN_CORNER_FAVICON_SIZE } = __webpack_require__(5);
const TopSiteLink = props => {
- const link = props.link;
-
+ const { link } = props;
const topSiteOuterClassName = `top-site-outer${props.className ? ` ${props.className}` : ""}`;
- const tippyTopIcon = link.tippyTopIcon;
-
+ const { tippyTopIcon, faviconSize } = link;
let imageClassName;
let imageStyle;
- if (tippyTopIcon) {
- imageClassName = "tippy-top-icon";
+ let showSmallFavicon = false;
+ let smallFaviconStyle;
+ if (tippyTopIcon || faviconSize >= MIN_RICH_FAVICON_SIZE) {
+ // styles and class names for top sites with rich icons
+ imageClassName = "top-site-icon rich-icon";
imageStyle = {
backgroundColor: link.backgroundColor,
- backgroundImage: `url(${tippyTopIcon})`
+ backgroundImage: `url(${tippyTopIcon || link.favicon})`
};
} else {
+ // styles and class names for top sites with screenshot + small icon in top left corner
imageClassName = `screenshot${link.screenshot ? " active" : ""}`;
imageStyle = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
+
+ // only show a favicon in top left if it's greater than 32x32
+ if (faviconSize >= MIN_CORNER_FAVICON_SIZE) {
+ showSmallFavicon = true;
+ smallFaviconStyle = { backgroundImage: `url(${link.favicon})` };
+ }
}
return React.createElement(
"li",
{ className: topSiteOuterClassName, key: link.guid || link.url },
React.createElement(
"a",
{ href: link.url, onClick: props.onClick },
React.createElement(
"div",
{ className: "tile", "aria-hidden": true },
React.createElement(
"span",
{ className: "letter-fallback" },
props.title[0]
),
- React.createElement("div", { className: imageClassName, style: imageStyle })
+ React.createElement("div", { className: imageClassName, style: imageStyle }),
+ showSmallFavicon && React.createElement("div", { className: "top-site-icon default-icon", style: smallFaviconStyle })
),
React.createElement(
"div",
{ className: `title ${link.isPinned ? "pinned" : ""}` },
link.isPinned && React.createElement("div", { className: "icon icon-pin-small" }),
React.createElement(
"span",
{ dir: "auto" },
@@ -890,35 +883,31 @@ class TopSite extends React.Component {
onMenuButtonClick(event) {
event.preventDefault();
this.toggleContextMenu(event, this.props.index);
}
onMenuUpdate(showContextMenu) {
this.setState({ showContextMenu });
}
onDismissButtonClick() {
- const link = this.props.link;
-
+ const { link } = this.props;
if (link.isPinned) {
this.props.dispatch(ac.SendToMain({
type: at.TOP_SITES_UNPIN,
data: { site: { url: link.url } }
}));
}
this.props.dispatch(ac.SendToMain({
type: at.BLOCK_URL,
data: link.url
}));
this.userEvent("BLOCK");
}
onPinButtonClick() {
- var _props = this.props;
- const link = _props.link,
- index = _props.index;
-
+ const { link, index } = this.props;
if (link.isPinned) {
this.props.dispatch(ac.SendToMain({
type: at.TOP_SITES_UNPIN,
data: { site: { url: link.url } }
}));
this.userEvent("UNPIN");
} else {
this.props.dispatch(ac.SendToMain({
@@ -927,19 +916,18 @@ class TopSite extends React.Component {
}));
this.userEvent("PIN");
}
}
onEditButtonClick() {
this.props.onEdit(this.props.index);
}
render() {
- const props = this.props;
- const link = props.link;
-
+ const { props } = this;
+ const { link } = props;
const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === props.index;
const title = link.label || link.hostname;
return React.createElement(
TopSiteLink,
_extends({}, props, { onClick: this.onLinkClick, className: isContextMenuOpen ? "active" : "", title: title }),
!props.editMode && React.createElement(
"div",
null,
@@ -988,55 +976,36 @@ TopSite.defaultProps = {
const TopSitePlaceholder = () => React.createElement(TopSiteLink, { className: "placeholder" });
module.exports.TopSite = TopSite;
module.exports.TopSiteLink = TopSiteLink;
module.exports.TopSitePlaceholder = TopSitePlaceholder;
/***/ }),
-/* 8 */
+/* 9 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(2);
-
-const injectIntl = _require.injectIntl;
-
+const React = __webpack_require__(1);
+const { injectIntl } = __webpack_require__(2);
const ContextMenu = __webpack_require__(17);
-
-var _require2 = __webpack_require__(1);
-
-const ac = _require2.actionCreators;
-
+const { actionCreators: ac } = __webpack_require__(0);
const linkMenuOptions = __webpack_require__(18);
const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"];
class LinkMenu extends React.Component {
getOptions() {
const props = this.props;
- const site = props.site,
- index = props.index,
- source = props.source;
+ const { site, index, source } = props;
// Handle special case of default site
-
const propOptions = !site.isDefault ? props.options : DEFAULT_SITE_MENU_OPTIONS;
const options = propOptions.map(o => linkMenuOptions[o](site, index, source)).map(option => {
- const action = option.action,
- impression = option.impression,
- id = option.id,
- type = option.type,
- userEvent = option.userEvent;
-
+ const { action, impression, id, type, userEvent } = option;
if (!type && id) {
option.label = props.intl.formatMessage(option);
option.onClick = () => {
props.dispatch(action);
if (userEvent) {
props.dispatch(ac.UserEvent({
event: userEvent,
source,
@@ -1065,214 +1034,154 @@ class LinkMenu extends React.Component {
options: this.getOptions() });
}
}
module.exports = injectIntl(LinkMenu);
module.exports._unconnected = LinkMenu;
/***/ }),
-/* 9 */
-/***/ (function(module, exports) {
-
-var g;
-
-// This works in non-strict mode
-g = (function() {
- return this;
-})();
-
-try {
- // This works if eval is allowed (see CSP)
- g = g || Function("return this")() || (1,eval)("this");
-} catch(e) {
- // This works if the window reference is available
- if(typeof window === "object")
- g = window;
-}
-
-// g can still be undefined, but nothing to do about it...
-// We return undefined, instead of nothing here, so it's
-// easier to handle this case. if(!global) { ...}
-
-module.exports = g;
-
-
-/***/ }),
/* 10 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
+/* WEBPACK VAR INJECTION */(function(global) {const React = __webpack_require__(1);
const ReactDOM = __webpack_require__(11);
const Base = __webpack_require__(12);
-
-var _require = __webpack_require__(3);
-
-const Provider = _require.Provider;
-
+const { Provider } = __webpack_require__(3);
const initStore = __webpack_require__(28);
-
-var _require2 = __webpack_require__(5);
-
-const reducers = _require2.reducers;
-
+const { reducers } = __webpack_require__(6);
const DetectUserSessionStart = __webpack_require__(30);
-
-var _require3 = __webpack_require__(31);
-
-const addSnippetsSubscriber = _require3.addSnippetsSubscriber;
-
+const { addSnippetsSubscriber } = __webpack_require__(31);
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
new DetectUserSessionStart().sendEventOrAddListener();
-const store = initStore(reducers);
+const store = initStore(reducers, global.gActivityStreamPrerenderedState);
+
+// If we are starting in a prerendered state, we must wait until the first render
+// to request state rehydration (see Base.jsx). If we are NOT in a prerendered state,
+// we can request it immedately.
+if (!global.gActivityStreamPrerenderedState) {
+ store.dispatch(ac.SendToMain({ type: at.NEW_TAB_STATE_REQUEST }));
+}
ReactDOM.render(React.createElement(
Provider,
{ store: store },
- React.createElement(Base, null)
+ React.createElement(Base, { isPrerendered: !!global.gActivityStreamPrerenderedState })
), document.getElementById("root"));
addSnippetsSubscriber(store);
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 11 */
/***/ (function(module, exports) {
module.exports = ReactDOM;
/***/ }),
/* 12 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(3);
-
-const connect = _require.connect;
-
-var _require2 = __webpack_require__(2);
-
-const addLocaleData = _require2.addLocaleData,
- IntlProvider = _require2.IntlProvider;
-
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { addLocaleData, IntlProvider } = __webpack_require__(2);
const TopSites = __webpack_require__(13);
const Search = __webpack_require__(19);
const ConfirmDialog = __webpack_require__(21);
const ManualMigration = __webpack_require__(22);
const PreferencesPane = __webpack_require__(23);
const Sections = __webpack_require__(24);
-
-// Locales that should be displayed RTL
-const RTL_LIST = ["ar", "he", "fa", "ur"];
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
// Add the locale data for pluralization and relative-time formatting for now,
// this just uses english locale data. We can make this more sophisticated if
// more features are needed.
-function addLocaleDataForReactIntl(_ref) {
- let locale = _ref.locale;
-
+function addLocaleDataForReactIntl({ locale, textDirection }) {
addLocaleData([{ locale, parentLocale: "en" }]);
document.documentElement.lang = locale;
- document.documentElement.dir = RTL_LIST.indexOf(locale.split("-")[0]) >= 0 ? "rtl" : "ltr";
+ document.documentElement.dir = textDirection;
}
class Base extends React.Component {
componentDidMount() {
+ // Request state AFTER the first render to ensure we don't cause the
+ // prerendered DOM to be unmounted. Otherwise, NEW_TAB_STATE_REQUEST is
+ // dispatched right after the store is ready.
+ if (this.props.isPrerendered) {
+ this.props.dispatch(ac.SendToMain({ type: at.NEW_TAB_STATE_REQUEST }));
+ }
+
// Also wait for the preloaded page to show, so the tab's title and favicon updates
addEventListener("visibilitychange", () => {
this.updateTitle(this.props.App);
document.getElementById("favicon").href += "#";
}, { once: true });
}
- componentWillUpdate(_ref2) {
- let App = _ref2.App;
-
+ componentWillUpdate({ App }) {
// Early loads might not have locale yet, so wait until we do
if (App.locale && App.locale !== this.props.App.locale) {
addLocaleDataForReactIntl(App);
this.updateTitle(App);
}
}
- updateTitle(_ref3) {
- let strings = _ref3.strings;
-
+ updateTitle({ strings }) {
if (strings) {
document.title = strings.newtab_page_title;
}
}
render() {
const props = this.props;
- var _props$App = props.App;
- const locale = _props$App.locale,
- strings = _props$App.strings,
- initialized = _props$App.initialized;
+ const { locale, strings, initialized } = props.App;
+ const prefs = props.Prefs.values;
- const prefs = props.Prefs.values;
- if (!initialized || !strings) {
+ if (!props.isPrerendered && !initialized) {
return null;
}
+ // Note: the key on IntlProvider must be static in order to not blow away
+ // all elements on a locale change (such as after preloading).
+ // See https://github.com/yahoo/react-intl/issues/695 for more info.
return React.createElement(
IntlProvider,
- { key: locale, locale: locale, messages: strings },
+ { key: "STATIC", locale: locale, messages: strings },
React.createElement(
"div",
{ className: "outer-wrapper" },
React.createElement(
"main",
null,
prefs.showSearch && React.createElement(Search, null),
!prefs.migrationExpired && React.createElement(ManualMigration, null),
prefs.showTopSites && React.createElement(TopSites, null),
React.createElement(Sections, null),
React.createElement(ConfirmDialog, null)
),
- React.createElement(PreferencesPane, null)
+ initialized && React.createElement(PreferencesPane, null)
)
);
}
}
module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
/***/ }),
/* 13 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(3);
-
-const connect = _require.connect;
-
-var _require2 = __webpack_require__(2);
-
-const FormattedMessage = _require2.FormattedMessage;
-
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { FormattedMessage } = __webpack_require__(2);
const TopSitesPerfTimer = __webpack_require__(14);
const TopSitesEdit = __webpack_require__(15);
-
-var _require3 = __webpack_require__(7);
-
-const TopSite = _require3.TopSite,
- TopSitePlaceholder = _require3.TopSitePlaceholder;
-
+const { TopSite, TopSitePlaceholder } = __webpack_require__(8);
const TopSites = props => {
const realTopSites = props.TopSites.rows.slice(0, props.TopSitesCount);
const placeholderCount = props.TopSitesCount - realTopSites.length;
return React.createElement(
TopSitesPerfTimer,
null,
React.createElement(
@@ -1302,33 +1211,20 @@ const TopSites = props => {
module.exports = connect(state => ({ TopSites: state.TopSites, TopSitesCount: state.Prefs.values.topSitesCount }))(TopSites);
module.exports._unconnected = TopSites;
/***/ }),
/* 14 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(3);
-
-const connect = _require.connect;
-
-var _require2 = __webpack_require__(1);
-
-const ac = _require2.actionCreators,
- at = _require2.actionTypes;
-
-var _require3 = __webpack_require__(6);
-
-const perfSvc = _require3.perfService;
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
+const { perfService: perfSvc } = __webpack_require__(7);
/**
* A proxy class that uses double requestAnimationFrame from
* componentDidMount to dispatch a SAVE_SESSION_PERF_DATA to the main procsess
* after the paint.
*
* This uses two callbacks because, after one callback, this part of the tree
* may have rendered but not yet reflowed. This strategy is modeled after
@@ -1338,17 +1234,16 @@ const perfSvc = _require3.perfService;
* make it lag too long.
*
* XXX Should be made more generic by using this.props.children, or potentially
* even split out into a higher-order component to wrap whatever.
*
* @class TopSitesPerfTimer
* @extends {React.Component}
*/
-
class TopSitesPerfTimer extends React.Component {
constructor(props) {
super(props);
// Just for test dependency injection:
this.perfSvc = this.props.perfSvc || perfSvc;
this._sendPaintedEvent = this._sendPaintedEvent.bind(this);
this._timestampHandled = false;
@@ -1435,48 +1330,25 @@ class TopSitesPerfTimer extends React.Co
module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSitesPerfTimer);
module.exports._unconnected = TopSitesPerfTimer;
/***/ }),
/* 15 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(2);
-
-const FormattedMessage = _require.FormattedMessage,
- injectIntl = _require.injectIntl;
-
-var _require2 = __webpack_require__(1);
-
-const ac = _require2.actionCreators,
- at = _require2.actionTypes;
-
+const React = __webpack_require__(1);
+const { FormattedMessage, injectIntl } = __webpack_require__(2);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
const TopSiteForm = __webpack_require__(16);
-
-var _require3 = __webpack_require__(7);
-
-const TopSite = _require3.TopSite,
- TopSitePlaceholder = _require3.TopSitePlaceholder;
-
-var _require4 = __webpack_require__(5);
+const { TopSite, TopSitePlaceholder } = __webpack_require__(8);
-const TOP_SITES_DEFAULT_LENGTH = _require4.TOP_SITES_DEFAULT_LENGTH,
- TOP_SITES_SHOWMORE_LENGTH = _require4.TOP_SITES_SHOWMORE_LENGTH;
-
-var _require5 = __webpack_require__(4);
-
-const TOP_SITES_SOURCE = _require5.TOP_SITES_SOURCE;
-
+const { TOP_SITES_DEFAULT_LENGTH, TOP_SITES_SHOWMORE_LENGTH } = __webpack_require__(6);
+const { TOP_SITES_SOURCE } = __webpack_require__(5);
class TopSitesEdit extends React.Component {
constructor(props) {
super(props);
this.state = {
showEditModal: false,
showAddForm: false,
showEditForm: false,
@@ -1618,50 +1490,38 @@ class TopSitesEdit extends React.Compone
React.createElement(
"div",
{ className: "modal" },
React.createElement(TopSiteForm, {
label: this.props.TopSites.rows[this.state.editIndex].label || this.props.TopSites.rows[this.state.editIndex].hostname,
url: this.props.TopSites.rows[this.state.editIndex].url,
index: this.state.editIndex,
editMode: true,
+ link: this.props.TopSites.rows[this.state.editIndex],
onClose: this.onFormClose,
dispatch: this.props.dispatch,
intl: this.props.intl })
)
)
);
}
}
module.exports = injectIntl(TopSitesEdit);
module.exports._unconnected = TopSitesEdit;
/***/ }),
/* 16 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(1);
+const React = __webpack_require__(1);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
+const { FormattedMessage } = __webpack_require__(2);
-const ac = _require.actionCreators,
- at = _require.actionTypes;
-
-var _require2 = __webpack_require__(2);
-
-const FormattedMessage = _require2.FormattedMessage;
-
-var _require3 = __webpack_require__(4);
-
-const TOP_SITES_SOURCE = _require3.TOP_SITES_SOURCE;
-
+const { TOP_SITES_SOURCE } = __webpack_require__(5);
class TopSiteForm extends React.Component {
constructor(props) {
super(props);
this.state = {
label: props.label || "",
url: props.url || "",
validationError: false
@@ -1705,16 +1565,23 @@ class TopSiteForm extends React.Componen
}
onSaveButtonClick(ev) {
ev.preventDefault();
if (this.validateForm()) {
let site = { url: this.cleanUrl() };
if (this.state.label !== "") {
site.label = this.state.label;
}
+ // Unpin links if the URL changed.
+ if (this.props.link.isPinned && this.props.link.url !== site.url) {
+ this.props.dispatch(ac.SendToMain({
+ type: at.TOP_SITES_UNPIN,
+ data: { site: { url: this.props.link.url } }
+ }));
+ }
this.props.dispatch(ac.SendToMain({
type: at.TOP_SITES_PIN,
data: { site, index: this.props.index }
}));
this.props.dispatch(ac.UserEvent({
source: TOP_SITES_SOURCE,
event: "TOP_SITES_EDIT",
action_position: this.props.index
@@ -1827,20 +1694,17 @@ TopSiteForm.defaultProps = {
};
module.exports = TopSiteForm;
/***/ }),
/* 17 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
+const React = __webpack_require__(1);
class ContextMenu extends React.Component {
constructor(props) {
super(props);
this.hideContext = this.hideContext.bind(this);
}
hideContext() {
this.props.onUpdate(false);
@@ -1880,36 +1744,34 @@ class ContextMenuItem extends React.Comp
this.onClick = this.onClick.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
onClick() {
this.props.hideContext();
this.props.option.onClick();
}
onKeyDown(event) {
- const option = this.props.option;
-
+ const { option } = this.props;
switch (event.key) {
case "Tab":
// tab goes down in context menu, shift + tab goes up in context menu
// if we're on the last item, one more tab will close the context menu
// similarly, if we're on the first item, one more shift + tab will close it
if (event.shiftKey && option.first || !event.shiftKey && option.last) {
this.props.hideContext();
}
break;
case "Enter":
this.props.hideContext();
option.onClick();
break;
}
}
render() {
- const option = this.props.option;
-
+ const { option } = this.props;
return React.createElement(
"li",
{ role: "menuitem", className: "context-menu-item" },
React.createElement(
"a",
{ onClick: this.onClick, onKeyDown: this.onKeyDown, tabIndex: "0" },
option.icon && React.createElement("span", { className: `icon icon-spacer icon-${option.icon}` }),
option.label
@@ -1921,30 +1783,23 @@ class ContextMenuItem extends React.Comp
module.exports = ContextMenu;
module.exports.ContextMenu = ContextMenu;
module.exports.ContextMenuItem = ContextMenuItem;
/***/ }),
/* 18 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-var _require = __webpack_require__(1);
-
-const at = _require.actionTypes,
- ac = _require.actionCreators;
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
/**
* List of functions that return items that can be included as menu options in a
* LinkMenu. All functions take the site as the first parameter, and optionally
* the index of the site.
*/
-
module.exports = {
Separator: () => ({ type: "separator" }),
RemoveBookmark: site => ({
id: "menu_action_remove_bookmark",
icon: "bookmark-added",
action: ac.SendToMain({
type: at.DELETE_BOOKMARK_BY_ID,
data: site.bookmarkGuid
@@ -2047,35 +1902,21 @@ module.exports.CheckPinTopSite = (site,
/***/ }),
/* 19 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/* globals ContentSearchUIController */
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(3);
-
-const connect = _require.connect;
-
-var _require2 = __webpack_require__(2);
-
-const FormattedMessage = _require2.FormattedMessage,
- injectIntl = _require2.injectIntl;
-
-var _require3 = __webpack_require__(1);
-
-const ac = _require3.actionCreators;
-
-var _require4 = __webpack_require__(20);
-
-const IS_NEWTAB = _require4.IS_NEWTAB;
-
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { FormattedMessage, injectIntl } = __webpack_require__(2);
+const { actionCreators: ac } = __webpack_require__(0);
+const { IS_NEWTAB } = __webpack_require__(20);
class Search extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.onInputMount = this.onInputMount.bind(this);
}
@@ -2155,52 +1996,38 @@ class Search extends React.Component {
{ className: "sr-only" },
React.createElement(FormattedMessage, { id: "search_button" })
)
)
);
}
}
-module.exports = connect()(injectIntl(Search));
+// initialized is passed to props so that Search will rerender when it receives strings
+module.exports = connect(state => ({ locale: state.App.locale }))(injectIntl(Search));
module.exports._unconnected = Search;
/***/ }),
/* 20 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-module.exports = {
+/* WEBPACK VAR INJECTION */(function(global) {module.exports = {
// constant to know if the page is about:newtab or about:home
- IS_NEWTAB: document.documentURI === "about:newtab"
+ IS_NEWTAB: global.document && global.document.documentURI === "about:newtab"
};
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 21 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(3);
-
-const connect = _require.connect;
-
-var _require2 = __webpack_require__(2);
-
-const FormattedMessage = _require2.FormattedMessage;
-
-var _require3 = __webpack_require__(1);
-
-const actionTypes = _require3.actionTypes,
- ac = _require3.actionCreators;
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { FormattedMessage } = __webpack_require__(2);
+const { actionTypes, actionCreators: ac } = __webpack_require__(0);
/**
* ConfirmDialog component.
* One primary action button, one cancel button.
*
* Content displayed is controlled by `data` prop the component receives.
* Example:
* data: {
@@ -2211,17 +2038,16 @@ const actionTypes = _require3.actionType
* // Primary button USerEvent action.
* userEvent: "DELETE",
* // Array of locale ids to display.
* message_body: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"],
* // Text for primary button.
* confirm_button_string_id: "menu_action_delete"
* },
*/
-
const ConfirmDialog = React.createClass({
displayName: "ConfirmDialog",
getDefaultProps() {
return {
visible: false,
data: {}
};
@@ -2293,43 +2119,29 @@ const ConfirmDialog = React.createClass(
module.exports = connect(state => state.Dialog)(ConfirmDialog);
module.exports._unconnected = ConfirmDialog;
module.exports.Dialog = ConfirmDialog;
/***/ }),
/* 22 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(3);
-
-const connect = _require.connect;
-
-var _require2 = __webpack_require__(2);
-
-const FormattedMessage = _require2.FormattedMessage;
-
-var _require3 = __webpack_require__(1);
-
-const at = _require3.actionTypes,
- ac = _require3.actionCreators;
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { FormattedMessage } = __webpack_require__(2);
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
/**
* Manual migration component used to start the profile import wizard.
* Message is presented temporarily and will go away if:
* 1. User clicks "No Thanks"
* 2. User completed the data import
* 3. After 3 active days
* 4. User clicks "Cancel" on the import wizard (currently not implemented).
*/
-
class ManualMigration extends React.Component {
constructor(props) {
super(props);
this.onLaunchTour = this.onLaunchTour.bind(this);
this.onCancelTour = this.onCancelTour.bind(this);
}
onLaunchTour() {
this.props.dispatch(ac.SendToMain({ type: at.MIGRATION_START }));
@@ -2371,40 +2183,21 @@ class ManualMigration extends React.Comp
module.exports = connect()(ManualMigration);
module.exports._unconnected = ManualMigration;
/***/ }),
/* 23 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(3);
-
-const connect = _require.connect;
-
-var _require2 = __webpack_require__(2);
-
-const injectIntl = _require2.injectIntl,
- FormattedMessage = _require2.FormattedMessage;
-
-var _require3 = __webpack_require__(1);
-
-const ac = _require3.actionCreators,
- at = _require3.actionTypes;
-
-var _require4 = __webpack_require__(5);
-
-const TOP_SITES_DEFAULT_LENGTH = _require4.TOP_SITES_DEFAULT_LENGTH,
- TOP_SITES_SHOWMORE_LENGTH = _require4.TOP_SITES_SHOWMORE_LENGTH;
-
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { injectIntl, FormattedMessage } = __webpack_require__(2);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
+const { TOP_SITES_DEFAULT_LENGTH, TOP_SITES_SHOWMORE_LENGTH } = __webpack_require__(6);
const getFormattedMessage = message => typeof message === "string" ? React.createElement(
"span",
null,
message
) : React.createElement(FormattedMessage, message);
const PreferencesInput = props => React.createElement(
@@ -2442,19 +2235,17 @@ class PreferencesPane extends React.Comp
handleClickOutside(event) {
// if we are showing the sidebar and there is a click outside, close it.
if (this.state.visible && !this.wrapper.contains(event.target)) {
this.togglePane();
}
}
handlePrefChange(event) {
const target = event.target;
- const name = target.name,
- checked = target.checked;
-
+ const { name, checked } = target;
let value = checked;
if (name === "topSitesCount") {
value = checked ? TOP_SITES_SHOWMORE_LENGTH : TOP_SITES_DEFAULT_LENGTH;
}
this.props.dispatch(ac.SetPref(name, value));
}
handleSectionChange(event) {
const target = event.target;
@@ -2511,25 +2302,24 @@ class PreferencesPane extends React.Comp
React.createElement(PreferencesInput, { className: "showTopSites", prefName: "showTopSites", value: prefs.showTopSites, onChange: this.handlePrefChange,
titleString: { id: "settings_pane_topsites_header" }, descString: { id: "settings_pane_topsites_body" } }),
React.createElement(
"div",
{ className: "options" },
React.createElement(PreferencesInput, { className: "showMoreTopSites", prefName: "topSitesCount", value: prefs.topSitesCount !== TOP_SITES_DEFAULT_LENGTH, onChange: this.handlePrefChange,
titleString: { id: "settings_pane_topsites_options_showmore" }, labelClassName: "icon icon-topsites" })
),
- sections.filter(section => !section.shouldHidePref).map((_ref) => {
- let id = _ref.id,
- title = _ref.title,
- enabled = _ref.enabled,
- pref = _ref.pref;
- return React.createElement(PreferencesInput, { key: id, className: "showSection", prefName: pref && pref.feed || id,
- value: enabled, onChange: pref && pref.feed ? this.handlePrefChange : this.handleSectionChange,
- titleString: pref && pref.titleString || title, descString: pref && pref.descString });
- })
+ sections.filter(section => !section.shouldHidePref).map(({ id, title, enabled, pref }) => React.createElement(PreferencesInput, { key: id, className: "showSection", prefName: pref && pref.feed || id,
+ value: enabled, onChange: pref && pref.feed ? this.handlePrefChange : this.handleSectionChange,
+ titleString: pref && pref.titleString || title, descString: pref && pref.descString })),
+ React.createElement("hr", null),
+ React.createElement(PreferencesInput, { className: "showSnippets", prefName: "feeds.snippets",
+ value: prefs["feeds.snippets"], onChange: this.handlePrefChange,
+ titleString: { id: "settings_pane_snippets_header" },
+ descString: { id: "settings_pane_snippets_body" } })
),
React.createElement(
"section",
{ className: "actions" },
React.createElement(
"button",
{ className: "done", onClick: this.togglePane },
React.createElement(FormattedMessage, { id: "settings_pane_done_button" })
@@ -2544,41 +2334,25 @@ class PreferencesPane extends React.Comp
module.exports = connect(state => ({ Prefs: state.Prefs, Sections: state.Sections }))(injectIntl(PreferencesPane));
module.exports.PreferencesPane = PreferencesPane;
module.exports.PreferencesInput = PreferencesInput;
/***/ }),
/* 24 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-/* WEBPACK VAR INJECTION */(function(global) {
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(3);
-
-const connect = _require.connect;
-
-var _require2 = __webpack_require__(2);
+/* WEBPACK VAR INJECTION */(function(global) {var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-const injectIntl = _require2.injectIntl,
- FormattedMessage = _require2.FormattedMessage;
-
+const React = __webpack_require__(1);
+const { connect } = __webpack_require__(3);
+const { injectIntl, FormattedMessage } = __webpack_require__(2);
const Card = __webpack_require__(25);
-const PlaceholderCard = Card.PlaceholderCard;
-
+const { PlaceholderCard } = Card;
const Topics = __webpack_require__(27);
-
-var _require3 = __webpack_require__(1);
-
-const ac = _require3.actionCreators;
-
+const { actionCreators: ac } = __webpack_require__(0);
const VISIBLE = "visible";
const VISIBILITY_CHANGE_EVENT = "visibilitychange";
const CARDS_PER_ROW = 3;
class Section extends React.Component {
constructor(props) {
super(props);
@@ -2613,32 +2387,30 @@ class Section extends React.Component {
return typeof message === "string" ? React.createElement(
"span",
null,
message
) : React.createElement(FormattedMessage, message);
}
_dispatchImpressionStats() {
- const props = this.props;
-
+ const { props } = this;
const maxCards = 3 * props.maxRows;
props.dispatch(ac.ImpressionStats({
source: props.eventSource,
tiles: props.rows.slice(0, maxCards).map(link => ({ id: link.guid })),
incognito: props.options && props.options.personalized
}));
}
// This sends an event when a user sees a set of new content. If content
// changes while the page is hidden (i.e. preloaded or on a hidden tab),
// only send the event if the page becomes visible again.
sendImpressionStatsOrAddListener() {
- const props = this.props;
-
+ const { props } = this;
if (!props.dispatch) {
return;
}
if (props.document.visibilityState === VISIBLE) {
this._dispatchImpressionStats();
} else {
@@ -2661,18 +2433,17 @@ class Section extends React.Component {
componentDidMount() {
if (this.props.rows.length) {
this.sendImpressionStatsOrAddListener();
}
}
componentDidUpdate(prevProps) {
- const props = this.props;
-
+ const { props } = this;
if (
// Don't send impression stats for the empty state
props.rows.length &&
// We only want to send impression stats if the content of the cards has changed
props.rows !== prevProps.rows) {
this.sendImpressionStatsOrAddListener();
}
}
@@ -2684,32 +2455,23 @@ class Section extends React.Component {
const remainder = items % CARDS_PER_ROW;
if (remainder === 0) {
return 0;
}
return CARDS_PER_ROW - remainder;
}
render() {
- var _props = this.props;
- const id = _props.id,
- eventSource = _props.eventSource,
- title = _props.title,
- icon = _props.icon,
- rows = _props.rows,
- infoOption = _props.infoOption,
- emptyState = _props.emptyState,
- dispatch = _props.dispatch,
- maxRows = _props.maxRows,
- contextMenuOptions = _props.contextMenuOptions,
- intl = _props.intl,
- initialized = _props.initialized;
-
+ const {
+ id, eventSource, title, icon, rows,
+ infoOption, emptyState, dispatch, maxRows,
+ contextMenuOptions, intl, initialized
+ } = this.props;
const maxCards = CARDS_PER_ROW * maxRows;
- const shouldShowTopics = id === "topstories" && this.props.topics && this.props.topics.length > 0 && this.props.read_more_endpoint;
+ const shouldShowTopics = id === "topstories" && this.props.topics && this.props.topics.length > 0;
const infoOptionIconA11yAttrs = {
"aria-haspopup": "true",
"aria-controls": "info-option",
"aria-expanded": this.state.infoActive ? "true" : "false",
"role": "note",
"tabIndex": 0
};
@@ -2811,49 +2573,37 @@ class Sections extends React.Component {
);
}
}
module.exports = connect(state => ({ Sections: state.Sections }))(Sections);
module.exports._unconnected = Sections;
module.exports.SectionIntl = SectionIntl;
module.exports._unconnectedSection = Section;
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(9)))
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 25 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-const LinkMenu = __webpack_require__(8);
-
-var _require = __webpack_require__(2);
-
-const FormattedMessage = _require.FormattedMessage;
-
+const React = __webpack_require__(1);
+const LinkMenu = __webpack_require__(9);
+const { FormattedMessage } = __webpack_require__(2);
const cardContextTypes = __webpack_require__(26);
-
-var _require2 = __webpack_require__(1);
-
-const ac = _require2.actionCreators,
- at = _require2.actionTypes;
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
/**
* Card component.
* Cards are found within a Section component and contain information about a link such
* as preview image, page title, page description, and some context about if the page
* was visited, bookmarked, trending etc...
* Each Section can make an unordered list of Cards which will create one instane of
* this class. Each card will then get a context menu which reflects the actions that
* can be done on this Card.
*/
-
class Card extends React.Component {
constructor(props) {
super(props);
this.state = { showContextMenu: false, activeCard: null };
this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
this.onMenuUpdate = this.onMenuUpdate.bind(this);
this.onLinkClick = this.onLinkClick.bind(this);
}
@@ -2861,22 +2611,17 @@ class Card extends React.Component {
event.preventDefault();
this.setState({
activeCard: this.props.index,
showContextMenu: true
});
}
onLinkClick(event) {
event.preventDefault();
- const altKey = event.altKey,
- button = event.button,
- ctrlKey = event.ctrlKey,
- metaKey = event.metaKey,
- shiftKey = event.shiftKey;
-
+ const { altKey, button, ctrlKey, metaKey, shiftKey } = event;
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
@@ -2887,31 +2632,21 @@ class Card extends React.Component {
incognito: true,
tiles: [{ id: this.props.link.guid, pos: this.props.index }]
}));
}
onMenuUpdate(showContextMenu) {
this.setState({ showContextMenu });
}
render() {
- var _props = this.props;
- const index = _props.index,
- link = _props.link,
- dispatch = _props.dispatch,
- contextMenuOptions = _props.contextMenuOptions,
- eventSource = _props.eventSource;
- const props = this.props;
-
+ const { index, link, dispatch, contextMenuOptions, eventSource } = this.props;
+ const { props } = this;
const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
-
- var _ref = link.type ? cardContextTypes[link.type] : {};
-
- const icon = _ref.icon,
- intlID = _ref.intlID;
-
+ // Display "now" as "trending" until we have new strings #3402
+ const { icon, intlID } = cardContextTypes[link.type === "now" ? "trending" : link.type] || {};
return React.createElement(
"li",
{ className: `card-outer${isContextMenuOpen ? " active" : ""}${props.placeholder ? " placeholder" : ""}` },
React.createElement(
"a",
{ href: link.url, onClick: !props.placeholder && this.onLinkClick },
React.createElement(
@@ -2978,20 +2713,17 @@ Card.defaultProps = { link: {} };
const PlaceholderCard = () => React.createElement(Card, { placeholder: true });
module.exports = Card;
module.exports.PlaceholderCard = PlaceholderCard;
/***/ }),
/* 26 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
+/***/ (function(module, exports) {
module.exports = {
history: {
intlID: "type_label_visited",
icon: "historyItem"
},
bookmark: {
intlID: "type_label_bookmarked",
@@ -3006,95 +2738,71 @@ module.exports = {
icon: "now"
}
};
/***/ }),
/* 27 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-const React = __webpack_require__(0);
-
-var _require = __webpack_require__(2);
-
-const FormattedMessage = _require.FormattedMessage;
-
+const React = __webpack_require__(1);
+const { FormattedMessage } = __webpack_require__(2);
class Topic extends React.Component {
render() {
- var _props = this.props;
- const url = _props.url,
- name = _props.name;
-
+ const { url, name } = this.props;
return React.createElement(
"li",
null,
React.createElement(
"a",
{ key: name, className: "topic-link", href: url },
name
)
);
}
}
class Topics extends React.Component {
render() {
- var _props2 = this.props;
- const topics = _props2.topics,
- read_more_endpoint = _props2.read_more_endpoint;
-
+ const { topics, read_more_endpoint } = this.props;
return React.createElement(
"div",
{ className: "topic" },
React.createElement(
"span",
null,
React.createElement(FormattedMessage, { id: "pocket_read_more" })
),
React.createElement(
"ul",
null,
topics.map(t => React.createElement(Topic, { key: t.name, url: t.url, name: t.name }))
),
- React.createElement(
+ read_more_endpoint && React.createElement(
"a",
{ className: "topic-read-more", href: read_more_endpoint },
React.createElement(FormattedMessage, { id: "pocket_read_even_more" })
)
);
}
}
module.exports = Topics;
module.exports._unconnected = Topics;
module.exports.Topic = Topic;
/***/ }),
/* 28 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-/* eslint-env mozilla/frame-script */
-
-var _require = __webpack_require__(29);
+/* WEBPACK VAR INJECTION */(function(global) {/* eslint-env mozilla/frame-script */
-const createStore = _require.createStore,
- combineReducers = _require.combineReducers,
- applyMiddleware = _require.applyMiddleware;
-
-var _require2 = __webpack_require__(1);
-
-const au = _require2.actionUtils;
-
+const { createStore, combineReducers, applyMiddleware } = __webpack_require__(29);
+const { actionTypes: at, actionCreators: ac, actionUtils: au } = __webpack_require__(0);
const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
/**
* A higher-order function which returns a reducer that, on MERGE_STORE action,
* will return the action.data object merged into the previous state.
@@ -3126,73 +2834,103 @@ function mergeStateReducer(mainReducer)
*/
const messageMiddleware = store => next => action => {
if (au.isSendToMain(action)) {
sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
}
next(action);
};
+const rehydrationMiddleware = store => next => action => {
+ if (store._didRehydrate) {
+ return next(action);
+ }
+
+ const isMergeStoreAction = action.type === MERGE_STORE_ACTION;
+ const isRehydrationRequest = action.type === at.NEW_TAB_STATE_REQUEST;
+
+ if (isRehydrationRequest) {
+ store._didRequestInitialState = true;
+ return next(action);
+ }
+
+ if (isMergeStoreAction) {
+ store._didRehydrate = true;
+ return next(action);
+ }
+
+ // If init happened after our request was made, we need to re-request
+ if (store._didRequestInitialState && action.type === at.INIT) {
+ return next(ac.SendToMain({ type: at.NEW_TAB_STATE_REQUEST }));
+ }
+
+ if (au.isBroadcastToContent(action) || au.isSendToContent(action)) {
+ // Note that actions received before didRehydrate will not be dispatched
+ // because this could negatively affect preloading and the the state
+ // will be replaced by rehydration anyway.
+ return null;
+ }
+
+ return next(action);
+};
+
/**
* initStore - Create a store and listen for incoming actions
*
* @param {object} reducers An object containing Redux reducers
+ * @param {object} intialState (optional) The initial state of the store, if desired
* @return {object} A redux store
*/
-module.exports = function initStore(reducers) {
- const store = createStore(mergeStateReducer(combineReducers(reducers)), applyMiddleware(messageMiddleware));
+module.exports = function initStore(reducers, initialState) {
+ const store = createStore(mergeStateReducer(combineReducers(reducers)), initialState, global.addMessageListener && applyMiddleware(rehydrationMiddleware, messageMiddleware));
+
+ store._didRehydrate = false;
+ store._didRequestInitialState = false;
- addMessageListener(INCOMING_MESSAGE_NAME, msg => {
- try {
- store.dispatch(msg.data);
- } catch (ex) {
- console.error("Content msg:", msg, "Dispatch error: ", ex); // eslint-disable-line no-console
- dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`);
- }
- });
+ if (global.addMessageListener) {
+ global.addMessageListener(INCOMING_MESSAGE_NAME, msg => {
+ try {
+ store.dispatch(msg.data);
+ } catch (ex) {
+ console.error("Content msg:", msg, "Dispatch error: ", ex); // eslint-disable-line no-console
+ dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`);
+ }
+ });
+ }
return store;
};
+module.exports.rehydrationMiddleware = rehydrationMiddleware;
module.exports.MERGE_STORE_ACTION = MERGE_STORE_ACTION;
module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME;
module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 29 */
/***/ (function(module, exports) {
module.exports = Redux;
/***/ }),
/* 30 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-
-
-var _require = __webpack_require__(1);
-
-const at = _require.actionTypes;
-
-var _require2 = __webpack_require__(6);
-
-const perfSvc = _require2.perfService;
-
+/* WEBPACK VAR INJECTION */(function(global) {const { actionTypes: at } = __webpack_require__(0);
+const { perfService: perfSvc } = __webpack_require__(7);
const VISIBLE = "visible";
const VISIBILITY_CHANGE_EVENT = "visibilitychange";
module.exports = class DetectUserSessionStart {
- constructor() {
- let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
-
+ constructor(options = {}) {
// Overrides for testing
- this.sendAsyncMessage = options.sendAsyncMessage || window.sendAsyncMessage;
- this.document = options.document || document;
+ this.sendAsyncMessage = options.sendAsyncMessage || global.sendAsyncMessage;
+ this.document = options.document || global.document;
this._perfService = options.perfService || perfSvc;
this._onVisibilityChange = this._onVisibilityChange.bind(this);
}
/**
* sendEventOrAddListener - Notify immediately if the page is already visible,
* or else set up a listener for when visibility changes.
* This is needed for accurate session tracking for telemetry,
@@ -3236,43 +2974,40 @@ module.exports = class DetectUserSession
*/
_onVisibilityChange() {
if (this.document.visibilityState === VISIBLE) {
this._sendEvent();
this.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
}
}
};
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 31 */
/***/ (function(module, exports, __webpack_require__) {
-"use strict";
-/* WEBPACK VAR INJECTION */(function(global) {
-
-const DATABASE_NAME = "snippets_db";
+/* WEBPACK VAR INJECTION */(function(global) {const DATABASE_NAME = "snippets_db";
const DATABASE_VERSION = 1;
const SNIPPETS_OBJECTSTORE_NAME = "snippets";
const SNIPPETS_UPDATE_INTERVAL_MS = 14400000; // 4 hours.
-var _require = __webpack_require__(1);
+const SNIPPETS_ENABLED_EVENT = "Snippets:Enabled";
+const SNIPPETS_DISABLED_EVENT = "Snippets:Disabled";
-const at = _require.actionTypes,
- ac = _require.actionCreators;
+const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
/**
* SnippetsMap - A utility for cacheing values related to the snippet. It has
* the same interface as a Map, but is optionally backed by
* indexedDB for persistent storage.
* Call .connect() to open a database connection and restore any
* previously cached data, if necessary.
*
*/
-
class SnippetsMap extends Map {
constructor(dispatch) {
super();
this._db = null;
this._dispatch = dispatch;
}
set(key, value) {
@@ -3528,44 +3263,62 @@ class SnippetsProvider {
await this._refreshSnippets();
// Try showing remote snippets, falling back to defaults if necessary.
try {
this._showRemoteSnippets();
} catch (e) {
this._showDefaultSnippets(e);
}
+
+ window.dispatchEvent(new Event(SNIPPETS_ENABLED_EVENT));
+ this.initialized = true;
+ }
+
+ uninit() {
+ window.dispatchEvent(new Event(SNIPPETS_DISABLED_EVENT));
+ this.initialized = false;
}
}
/**
* addSnippetsSubscriber - Creates a SnippetsProvider that Initializes
* when the store has received the appropriate
* Snippet data.
*
* @param {obj} store The redux store
- * @return {obj} Returns the snippets instance and unsubscribe function
+ * @return {obj} Returns the snippets instance and unsubscribe function
*/
function addSnippetsSubscriber(store) {
const snippets = new SnippetsProvider(store.dispatch);
- const unsubscribe = store.subscribe(() => {
+
+ let initializing = false;
+
+ store.subscribe(async () => {
const state = store.getState();
- if (state.Snippets.initialized) {
- if (state.Snippets.onboardingFinished) {
- snippets.init({ appData: state.Snippets });
+ // state.Snippets.initialized: Should snippets be initialised?
+ // snippets.initialized: Is SnippetsProvider currently initialised?
+ if (state.Snippets.initialized && !snippets.initialized && state.Snippets.onboardingFinished) {
+ // Don't call init multiple times
+ if (!initializing) {
+ initializing = true;
+ await snippets.init({ appData: state.Snippets });
+ initializing = false;
}
- unsubscribe();
+ } else if (state.Snippets.initialized === false && snippets.initialized) {
+ snippets.uninit();
}
});
+
// These values are returned for testing purposes
return snippets;
}
module.exports = {
addSnippetsSubscriber,
SnippetsMap,
SnippetsProvider,
SNIPPETS_UPDATE_INTERVAL_MS
};
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(9)))
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ })
/******/ ]);
\ No newline at end of file
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -59,17 +59,19 @@ input {
background-image: url("assets/glyph-unpin-16.svg"); }
.icon.icon-edit {
background-image: url("assets/glyph-edit-16.svg"); }
.icon.icon-pocket {
background-image: url("assets/glyph-pocket-16.svg"); }
.icon.icon-historyItem {
background-image: url("assets/glyph-historyItem-16.svg"); }
.icon.icon-trending {
- background-image: url("assets/glyph-trending-16.svg"); }
+ background-image: url("assets/glyph-trending-16.svg");
+ transform: translateY(2px);
+ /* trending bolt is visually top heavy */ }
.icon.icon-now {
background-image: url("chrome://browser/skin/history.svg"); }
.icon.icon-topsites {
background-image: url("assets/glyph-topsites-16.svg"); }
.icon.icon-pin-small {
background-image: url("assets/glyph-pin-12.svg");
background-size: 12px;
height: 12px;
@@ -312,27 +314,36 @@ main {
border-radius: 6px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
background-size: cover;
background-position: top left;
transition: opacity 1s;
opacity: 0; }
.top-sites-list .top-site-outer .screenshot.active {
opacity: 1; }
- .top-sites-list .top-site-outer .tippy-top-icon {
+ .top-sites-list .top-site-outer .top-site-icon {
position: absolute;
- top: 0;
- left: 0;
- height: 100%;
- width: 100%;
border-radius: 6px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
background-position: center center;
- background-size: 96px;
- background-repeat: no-repeat; }
+ background-repeat: no-repeat;
+ background-color: #F9F9FA; }
+ .top-sites-list .top-site-outer .rich-icon {
+ top: 0;
+ offset-inline-start: 0;
+ height: 100%;
+ width: 100%;
+ background-size: 96px; }
+ .top-sites-list .top-site-outer .default-icon {
+ z-index: 1;
+ top: -6px;
+ offset-inline-start: -6px;
+ height: 42px;
+ width: 42px;
+ background-size: 32px; }
.top-sites-list .top-site-outer .title {
font: message-box;
height: 30px;
line-height: 30px;
text-align: center;
width: 96px;
position: relative; }
.top-sites-list .top-site-outer .title .icon {
@@ -1012,21 +1023,20 @@ main {
bottom: 0;
left: 0;
right: 0;
color: #737373;
font-size: 11px;
display: flex; }
.card-outer .card-context-icon {
fill: rgba(12, 12, 13, 0.6);
- font-size: 13px;
- margin-inline-end: 6px;
- display: block; }
+ margin-inline-end: 6px; }
.card-outer .card-context-label {
flex-grow: 1;
+ line-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.manual-migration-container {
color: #4A4A4F;
font-size: 13px;
line-height: 15px;
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -1,10 +1,10 @@
<!doctype html>
-<html lang="en-us" dir="ltr">
+<html lang="" 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="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
<link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
<link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
</head>
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -227,19 +227,21 @@
"settings_pane_topsites_header": "Qabaqcıl Saytlar",
"settings_pane_topsites_body": "Ən çox ziyarət etdiyiniz saytları görün.",
"settings_pane_topsites_options_showmore": "İki sətir göstər",
"settings_pane_bookmarks_header": "Son Əlfəcinlər",
"settings_pane_bookmarks_body": "Yeni yaradılan əlfəcinlər tək bir əlverişli yerdə.",
"settings_pane_visit_again_header": "Təkrar ziyarət et",
"settings_pane_visit_again_body": "Firefox tarixçənizdən yadda saxlamaq və ya geri qayıtmaq istəyə biləcəyiniz hissələri göstərəcək.",
"settings_pane_highlights_header": "Seçilmişlər",
+ "settings_pane_highlights_body2": "Son ziyarət etdiyiniz və ya əlfəcinlədiyiniz maraqlı məzmunlara rahat qayıdın.",
"settings_pane_highlights_options_bookmarks": "Əlfəcinlər",
"settings_pane_highlights_options_visited": "Baxılmış Saytlar",
"settings_pane_snippets_header": "Hissələr",
+ "settings_pane_snippets_body": "Mozilladan Firefox, internet mədəniyyəti və digər yeniliklər haqqında qısa bildirişlər oxuyun.",
"settings_pane_done_button": "Oldu",
"edit_topsites_button_text": "Redaktə et",
"edit_topsites_button_label": "Qabaqcıl Saytlar bölümünüzü fərdiləşdirin",
"edit_topsites_showmore_button": "Daha çox göstər",
"edit_topsites_showless_button": "Daha az göstər",
"edit_topsites_done_button": "Oldu",
"edit_topsites_pin_button": "Bu saytı sabitlə",
"edit_topsites_unpin_button": "Bu saytı çıxart",
@@ -252,16 +254,18 @@
"topsites_form_url_placeholder": "Ünvanı yazın və ya yapışdırın",
"topsites_form_add_button": "Əlavə et",
"topsites_form_save_button": "Saxla",
"topsites_form_cancel_button": "Ləğv et",
"topsites_form_url_validation": "Doğru ünvan tələb olunur",
"pocket_read_more": "Məşhur Mövzular:",
"pocket_read_even_more": "Daha çox hekayə gör",
"pocket_feedback_header": "25 milyon nəfərin dəstəyi ilə internetin ən yaxşıları.",
+ "pocket_description": "Mozilla ailəsinin yeni üzvü olan Pocket ilə yüksək keyfiyyətli məzmunları kəşf edin.",
+ "highlights_empty_state": "İnternetdə gəzməyə başlayın, burada ziyarət edəcəyiniz və ya əlfəcinləyəcəyiniz məqalə, video və digər səhifələri göstərəcəyik.",
"topstories_empty_state": "Hamısını oxudunuz. Yeni {provider} məqalələri üçün daha sonra təkrar yoxlayın. Gözləyə bilmirsiz? Məşhur mövzu seçərək internetdən daha çox gözəl məqalələr tapın.",
"manual_migration_explanation2": "Firefox səyyahını digər səyyahlardan olan əlfəcin, tarixçə və parollar ilə yoxlayın.",
"manual_migration_cancel_button": "Xeyr, Təşəkkürlər",
"manual_migration_import_button": "İndi idxal et"
},
"be": {
"newtab_page_title": "Новая картка",
"default_label_loading": "Загрузка…",
@@ -409,19 +413,21 @@
"settings_pane_topsites_header": "Най-посещавани",
"settings_pane_topsites_body": "Достъп до сайтовете, които посещавате най-често.",
"settings_pane_topsites_options_showmore": "Показване на два реда",
"settings_pane_bookmarks_header": "Последни отметки",
"settings_pane_bookmarks_body": "Всички нови отметки на едно място.",
"settings_pane_visit_again_header": "Посещаване",
"settings_pane_visit_again_body": "Firefox ще ви показва части от вашата история на разглеждане, към която бихте желали да се върнете или запомните.",
"settings_pane_highlights_header": "Акценти",
+ "settings_pane_highlights_body2": "Намерете интересните неща, които скоро сте посетили или отметнали.",
"settings_pane_highlights_options_bookmarks": "Отметки",
"settings_pane_highlights_options_visited": "Посетени страници",
"settings_pane_snippets_header": "Изрезки",
+ "settings_pane_snippets_body": "Четете кратки и радостни новини от Mozilla относно Firefox, интернет-културата и случайни мемета.",
"settings_pane_done_button": "Готово",
"edit_topsites_button_text": "Редактиране",
"edit_topsites_button_label": "Настройки на най-посещаваните",
"edit_topsites_showmore_button": "Повече",
"edit_topsites_showless_button": "По-малко",
"edit_topsites_done_button": "Готово",
"edit_topsites_pin_button": "Закачане",
"edit_topsites_unpin_button": "Премахване от закачените",
@@ -435,16 +441,17 @@
"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_description": "Открийте висококачествено съдържание, което иначе може да пропуснете, с помощта на Pocket, вече част от Mozilla.",
+ "highlights_empty_state": "Разглеждайте и тук ще ви покажем някои от най-добрите статии, видео и други страници, които сте посетили или отметнали наскоро.",
"topstories_empty_state": "Разгледахте всичко. Проверете по-късно за повече истории от {provider}. Нямате търпение? Изберете популярна тема, за да откриете повече истории из цялата Мрежа.",
"manual_migration_explanation2": "Опитайте Firefox с отметките, историята и паролите от друг четец.",
"manual_migration_cancel_button": "Не, благодаря",
"manual_migration_import_button": "Внасяне"
},
"bn-BD": {
"newtab_page_title": "নতুন ট্যাব",
"default_label_loading": "লোড হচ্ছে…",
@@ -1050,20 +1057,20 @@
"type_label_topic": "Thema",
"type_label_now": "Jetzt",
"menu_action_bookmark": "Lesezeichen",
"menu_action_remove_bookmark": "Lesezeichen entfernen",
"menu_action_copy_address": "Adresse kopieren",
"menu_action_email_link": "Link per E-Mail versenden…",
"menu_action_open_new_window": "In neuem Fenster öffnen",
"menu_action_open_private_window": "In neuem privaten Fenster öffnen",
- "menu_action_dismiss": "Schließen",
+ "menu_action_dismiss": "Entfernen",
"menu_action_delete": "Aus Chronik löschen",
"menu_action_pin": "Anheften",
- "menu_action_unpin": "Lösen",
+ "menu_action_unpin": "Ablösen",
"confirm_history_delete_p1": "Soll wirklich jede Instanz dieser Seite aus Ihrer Chronik gelöscht werden?",
"confirm_history_delete_notice_p2": "Diese Aktion kann nicht rückgängig gemacht werden.",
"menu_action_save_to_pocket": "Bei Pocket speichern",
"search_for_something_with": "Nach {search_term} suchen mit:",
"search_button": "Suchen",
"search_header": "{search_engine_name}-Suche",
"search_web_placeholder": "Das Web durchsuchen",
"search_settings": "Sucheinstellungen ändern",
@@ -1072,18 +1079,18 @@
"section_info_privacy_notice": "Datenschutzhinweis",
"welcome_title": "Willkommen im neuen Tab",
"welcome_body": "Firefox nutzt diesen Bereich, um Ihnen Ihre wichtigsten Lesezeichen, Artikel, Videos und kürzlich besuchten Seiten anzuzeigen, damit Sie diese einfach wiederfinden.",
"welcome_label": "Auswahl Ihrer wichtigsten Seiten",
"time_label_less_than_minute": "< 1 min",
"time_label_minute": "{number} m",
"time_label_hour": "{number} h",
"time_label_day": "{number} t",
- "settings_pane_button_label": "Neuer-Tab-Seite anpassen",
- "settings_pane_header": "Einstellungen zum neuen Tab",
+ "settings_pane_button_label": "Einstellungen für neue Tabs anpassen",
+ "settings_pane_header": "Einstellungen für neue Tabs",
"settings_pane_body2": "Wählen Sie aus, was auf dieser Seite angezeigt wird.",
"settings_pane_search_header": "Suche",
"settings_pane_search_body": "Suchen Sie aus einem neuen Tab im Internet.",
"settings_pane_topsites_header": "Meistbesuchte Seiten",
"settings_pane_topsites_body": "Schneller Zugriff auf Ihre meistbesuchten Websites.",
"settings_pane_topsites_options_showmore": "Zwei Reihen anzeigen",
"settings_pane_bookmarks_header": "Neue Lesezeichen",
"settings_pane_bookmarks_body": "Ihre neu erstellten Lesezeichen praktisch an einem Ort.",
@@ -1103,28 +1110,28 @@
"edit_topsites_done_button": "Fertig",
"edit_topsites_pin_button": "Website immer in aktueller Position anzeigen",
"edit_topsites_unpin_button": "Diese Website lösen",
"edit_topsites_edit_button": "Diese Website bearbeiten",
"edit_topsites_dismiss_button": "Website entfernen",
"edit_topsites_add_button": "Hinzufügen",
"topsites_form_add_header": "Neue meistbesuchte Seite",
"topsites_form_edit_header": "Meistbesuchte Seite bearbeiten",
- "topsites_form_title_placeholder": "Titel eingeben",
- "topsites_form_url_placeholder": "Eine URL eingeben oder einfügen",
+ "topsites_form_title_placeholder": "Name eingeben",
+ "topsites_form_url_placeholder": "Eine Adresse eingeben oder einfügen",
"topsites_form_add_button": "Hinzufügen",
"topsites_form_save_button": "Speichern",
"topsites_form_cancel_button": "Abbrechen",
"topsites_form_url_validation": "Gültige URL erforderlich",
"pocket_read_more": "Beliebte Themen:",
"pocket_read_even_more": "Weitere Nachrichten ansehen",
"pocket_feedback_header": "Das Beste aus dem Web, zusammengetragen von 25 Millionen Menschen.",
"pocket_description": "Entdecken Sie qualitativ hochwertige Inhalte mithilfe von Pocket (jetzt Teil von von Mozilla), die Sie ansonsten verpassen würden.",
"highlights_empty_state": "Surfen Sie los und wir zeigen Ihnen hier tolle Artikel, Videos und andere Seiten, die Sie kürzlich besucht oder als Lesezeichen gespeichert haben.",
- "topstories_empty_state": "Jetzt kennen Sie die Neuigkeiten. Schauen Sie später wieder vorbei, um neue Informationen von {provider} zu erhalten. Können sie nicht warten? Wählen Sie ein beliebtes Thema und lesen Sie weitere interessante Geschichten aus dem Internet.",
+ "topstories_empty_state": "Jetzt kennen Sie die Neuigkeiten. Schauen Sie später wieder vorbei, um neue Informationen von {provider} zu erhalten. Können Sie nicht warten? Wählen Sie ein beliebtes Thema und lesen Sie weitere interessante Geschichten aus dem Internet.",
"manual_migration_explanation2": "Probieren Sie Firefox aus und importieren Sie die Lesezeichen, Chronik und Passwörter eines anderen Browsers.",
"manual_migration_cancel_button": "Nein, danke",
"manual_migration_import_button": "Jetzt importieren"
},
"dsb": {
"newtab_page_title": "Nowy rejtark",
"default_label_loading": "Zacytujo se…",
"header_top_sites": "Nejcesćej woglědane sedła",
@@ -1758,16 +1765,17 @@
"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_highlights": "Destacados",
"header_visit_again": "Visitar de nuevo",
"header_bookmarks": "Marcadores recientes",
"header_recommended_by": "Recomendado por {provider}",
"header_bookmarks_placeholder": "Todavía no tienes ningún marcador.",
"header_stories_from": "desde",
"type_label_visited": "Visitados",
"type_label_bookmarked": "En marcadores",
"type_label_synced": "Sincronizado desde otro dispositivo",
@@ -1789,37 +1797,43 @@
"confirm_history_delete_notice_p2": "Esta acción no se puede deshacer.",
"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",
+ "section_info_send_feedback": "Enviar comentario",
+ "section_info_privacy_notice": "Aviso de privacidad",
"welcome_title": "Bienvenido a la nueva pestaña",
"welcome_body": "Firefox utilizará este espacio para mostrarte los marcadores, artículos y vídeos más relevantes y las páginas que has visitado recientemente, para que puedas acceder más rápido.",
"welcome_label": "Identificar lo más destacado para ti",
"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": "Personalizar la página Nueva pestaña",
"settings_pane_header": "Preferencias de nueva pestaña",
- "settings_pane_body": "Elige qué quieres ver al abrir una nueva pestaña",
+ "settings_pane_body2": "Elige lo quieras ver en esta página.",
"settings_pane_search_header": "Buscar",
"settings_pane_search_body": "Busca en la Web desde tu nueva pestaña.",
"settings_pane_topsites_header": "Sitios populares",
"settings_pane_topsites_body": "Accede a las páginas que más visitas.",
"settings_pane_topsites_options_showmore": "Mostrar dos líneas",
"settings_pane_bookmarks_header": "Marcadores recientes",
"settings_pane_bookmarks_body": "Tus marcadores recién creados, fácilmente accesibles.",
"settings_pane_visit_again_header": "Visitar de nuevo",
"settings_pane_visit_again_body": "Firefox te mostrará partes de tu historial de navegación que te gustaría recordar o volver a visitar.",
- "settings_pane_pocketstories_header": "Historias populares",
- "settings_pane_pocketstories_body": "Pocket, que forma parte de la familia de Mozilla, te ayudará a encontrar contenido de alta calidad que puede que no encuentres de otra forma.",
+ "settings_pane_highlights_header": "Destacados",
+ "settings_pane_highlights_body2": "Vuelve a encontrar todas las cosas interesantes que hayas visitado o marcado recientemente.",
+ "settings_pane_highlights_options_bookmarks": "Marcadores",
+ "settings_pane_highlights_options_visited": "Sitios visitados",
+ "settings_pane_snippets_header": "Fragmentos de código",
+ "settings_pane_snippets_body": "Lee actualizaciones breves de Mozilla sobre Firefox, la cultura de internet y el típico meme aleatorio.",
"settings_pane_done_button": "Hecho",
"edit_topsites_button_text": "Editar",
"edit_topsites_button_label": "Personalizar la sección de Sitios populares",
"edit_topsites_showmore_button": "Mostrar más",
"edit_topsites_showless_button": "Mostrar menos",
"edit_topsites_done_button": "Hecho",
"edit_topsites_pin_button": "Fijar este sitio",
"edit_topsites_unpin_button": "Eliminar este sitio fijo",
@@ -1832,20 +1846,20 @@
"topsites_form_url_placeholder": "Escribir o pegar una URL",
"topsites_form_add_button": "Agregar",
"topsites_form_save_button": "Guardar",
"topsites_form_cancel_button": "Cancelar",
"topsites_form_url_validation": "Se requiere una URL válida",
"pocket_read_more": "Temas populares:",
"pocket_read_even_more": "Ver más historias",
"pocket_feedback_header": "Lo mejor de la web, confirmado por más de 25 millones de personas.",
- "pocket_feedback_body": "Pocket, que forma parte de la familia de Mozilla, te ayudará a encontrar contenido de alta calidad que puede que no encuentres de otra forma.",
- "pocket_send_feedback": "Enviar comentario",
+ "pocket_description": "Gracias a Pocket, que ahora forma parte de Mozilla, podrás descubrir contenido de alta calidad que de otra forma te perderías.",
+ "highlights_empty_state": "Empieza a navegar y nosotros te mostraremos aquí algunos de los mejores artículos, videos y otras páginas que hayas visitado recientemente o agregado a marcadores.",
"topstories_empty_state": "Ya estás al día. Vuelve luego y busca más historias de {provider}. ¿No puedes esperar? Selecciona un tema popular y encontrás más historias alucinantes por toda la web.",
- "manual_migration_explanation": "Prueba Firefox con tusmarcadores y sitios favoritos importados desde otro navegador.",
+ "manual_migration_explanation2": "Prueba Firefox con los marcadores, historial y contraseñas de otro navegador.",
"manual_migration_cancel_button": "No, gracias",
"manual_migration_import_button": "Importar ahora"
},
"es-MX": {
"newtab_page_title": "Nueva pestaña",
"default_label_loading": "Cargando…",
"header_top_sites": "Sitios favoritos",
"header_stories": "Historias populares",
@@ -2358,17 +2372,17 @@
"settings_pane_topsites_header": "Sites les plus visités",
"settings_pane_topsites_body": "Accédez aux sites que vous consultez le plus.",
"settings_pane_topsites_options_showmore": "Afficher deux lignes",
"settings_pane_bookmarks_header": "Marque-pages récents",
"settings_pane_bookmarks_body": "Vos nouveaux marque-pages, facilement accessibles.",
"settings_pane_visit_again_header": "Visiter à nouveau",
"settings_pane_visit_again_body": "Firefox affichera des extraits de votre historique de navigation dont vous pourriez vouloir vous souvenir ou que vous pourriez vouloir revisiter.",
"settings_pane_highlights_header": "Éléments-clés",
- "settings_pane_highlights_body2": "Retrouvez des pages inintéressantes que vous avez déjà visitées récemment ou ajoutées aux marque-pages.",
+ "settings_pane_highlights_body2": "Retrouvez des pages intéressantes que vous avez visitées récemment ou ajoutées aux marque-pages.",
"settings_pane_highlights_options_bookmarks": "Marque-pages",
"settings_pane_highlights_options_visited": "Sites visités",
"settings_pane_snippets_header": "Brèves",
"settings_pane_snippets_body": "Consultez les brèves de Mozilla à propos de Firefox, la culture Internet, mais aussi quelques mèmes Internet de temps en temps.",
"settings_pane_done_button": "Terminé",
"edit_topsites_button_text": "Modifier",
"edit_topsites_button_label": "Personnaliser la section Sites les plus visités",
"edit_topsites_showmore_button": "Afficher plus",
@@ -3002,16 +3016,17 @@
"manual_migration_cancel_button": "Ně, dźakuju so",
"manual_migration_import_button": "Nětko importować"
},
"hu": {
"newtab_page_title": "Új lap",
"default_label_loading": "Betöltés…",
"header_top_sites": "Népszerű oldalak",
"header_stories": "Népszerű történetek",
+ "header_highlights": "Kiemelések",
"header_visit_again": "Látogasson el ismét",
"header_bookmarks": "Friss könyvjelzők",
"header_recommended_by": "A(z) {provider} ajánlásával",
"header_bookmarks_placeholder": "Még nincs könyvjelzője.",
"header_stories_from": "innen:",
"type_label_visited": "Látogatott",
"type_label_bookmarked": "Könyvjelzőzött",
"type_label_synced": "Másik eszközről szinkronizálva",
@@ -3033,37 +3048,43 @@
"confirm_history_delete_notice_p2": "Ez a művelet nem vonható vissza.",
"menu_action_save_to_pocket": "Mentés a Pocketbe",
"search_for_something_with": "„{search_term}” keresése ezzel:",
"search_button": "Keresés",
"search_header": "{search_engine_name} keresés",
"search_web_placeholder": "Keresés a weben",
"search_settings": "Keresési beállítások módosítása",
"section_info_option": "Információ",
+ "section_info_send_feedback": "Visszajelzés küldése",
+ "section_info_privacy_notice": "Adatvédelmi nyilatkozat",
"welcome_title": "Üdvözöljük az új lapon",
"welcome_body": "A Firefox ezt a területet a leginkább releváns könyvjelzők, cikkek, videók és nemrég látogatott oldalak megjelenítésére fogja használni, így könnyedén visszatalálhat hozzájuk.",
"welcome_label": "A kiemeléseinek azonosítása",
"time_label_less_than_minute": "<1 p",
"time_label_minute": "{number} p",
"time_label_hour": "{number} ó",
"time_label_day": "{number} n",
"settings_pane_button_label": "Az Új lap oldal személyre szabása",
"settings_pane_header": "Új lap beállításai",
- "settings_pane_body": "Válassza ki, hogy mit lát, amikor megnyit egy új lapot.",
+ "settings_pane_body2": "Válassza ki, hogy mit akar látni ezen az oldalon.",
"settings_pane_search_header": "Keresés",
"settings_pane_search_body": "Keresés a weben az új lapon.",
"settings_pane_topsites_header": "Népszerű oldalak",
"settings_pane_topsites_body": "A leggyakrabban látogatott webhelyek elérése.",
"settings_pane_topsites_options_showmore": "Két sor megjelenítése",
"settings_pane_bookmarks_header": "Friss könyvjelzők",
"settings_pane_bookmarks_body": "A frissen létrehozott könyvjelzői egy praktikus helyen.",
"settings_pane_visit_again_header": "Látogasson el ismét",
"settings_pane_visit_again_body": "A Firefox megjeleníti a böngészési előzményeinek azt a részét, amelyet lehet hogy meg szeretne jegyezni, vagy ahová vissza akar térni.",
- "settings_pane_pocketstories_header": "Népszerű történetek",
- "settings_pane_pocketstories_body": "A Pocket a Mozilla család tagja, segít az olyan jó minőségű tartalmak fellelésében, melyekkel egyébként nem is találkozott volna.",
+ "settings_pane_highlights_header": "Kiemelések",
+ "settings_pane_highlights_body2": "Találjon vissza azokhoz az érdekes dolgokhoz, amelyeket meglátogatott vagy könyvjelzőzött.",
+ "settings_pane_highlights_options_bookmarks": "Könyvjelzők",
+ "settings_pane_highlights_options_visited": "Látogatott helyek",
+ "settings_pane_snippets_header": "Töredékek",
+ "settings_pane_snippets_body": "Olvasson rövid és érdekes híreket a Mozillától, a Firefoxról, az internetes kultúráról, és időnként kapjon mémeket.",
"settings_pane_done_button": "Kész",
"edit_topsites_button_text": "Szerkesztés",
"edit_topsites_button_label": "A Népszerű oldalak rész testreszabása",
"edit_topsites_showmore_button": "Több megjelenítése",
"edit_topsites_showless_button": "Kevesebb megjelenítése",
"edit_topsites_done_button": "Kész",
"edit_topsites_pin_button": "Webhely rögzítése",
"edit_topsites_unpin_button": "Rögzítés feloldása",
@@ -3076,20 +3097,20 @@
"topsites_form_url_placeholder": "Írjon vagy illesszen be egy URL-t",
"topsites_form_add_button": "Hozzáadás",
"topsites_form_save_button": "Mentés",
"topsites_form_cancel_button": "Mégse",
"topsites_form_url_validation": "Érvényes URL szükséges",
"pocket_read_more": "Népszerű témák:",
"pocket_read_even_more": "További történetek",
"pocket_feedback_header": "A web legjava, több mint 25 millió ember válogatásában.",
- "pocket_feedback_body": "A Pocket a Mozilla család tagja, segít az olyan jó minőségű tartalmak fellelésében, melyekkel egyébként nem is találkozott volna.",
- "pocket_send_feedback": "Visszajelzés küldése",
+ "pocket_description": "Fedezzen fel olyan, magas minőségű tartalmakat, amelyek egyébként elkerülnék a figyelmét, a Pocket segítségével, amely most már a Mozilla része.",
+ "highlights_empty_state": "Kezdjen el böngészni, és itt fognak megjelenni azok a nagyszerű cikkek, videók és más lapok, amelyeket nemrég meglátogatott vagy könyvjelzőzött.",
"topstories_empty_state": "Már felzárkózott. Nézzen vissza később a legújabb {provider} hírekért. Nem tud várni? Válasszon egy népszerű témát, hogy még több sztorit találjon a weben.",
- "manual_migration_explanation": "Próbálja ki a Firefoxot egy másik böngészőben lévő kedvenc oldalaival és könyvjelzőivel.",
+ "manual_migration_explanation2": "Próbálja ki a Firefoxot másik böngészőből származó könyvjelzőkkel, előzményekkel és jelszavakkal.",
"manual_migration_cancel_button": "Köszönöm, nem",
"manual_migration_import_button": "Importálás most"
},
"hy-AM": {
"newtab_page_title": "Նոր ներդիր",
"default_label_loading": "Բեռնվում է...",
"header_top_sites": "Լավագույն կայքեր",
"header_highlights": "Գունանշում",
@@ -3118,16 +3139,17 @@
"time_label_hour": "{number} ժ",
"time_label_day": "{number} օր"
},
"id": {
"newtab_page_title": "Tab Baru",
"default_label_loading": "Memuat…",
"header_top_sites": "Situs Teratas",
"header_stories": "Cerita Utama",
+ "header_highlights": "Sorotan",
"header_visit_again": "Kunjungi Lagi",
"header_bookmarks": "Markah Terbaru",
"header_recommended_by": "Disarankan oleh {provider}",
"header_bookmarks_placeholder": "Anda belum memiliki markah.",
"header_stories_from": "dari",
"type_label_visited": "Dikunjungi",
"type_label_bookmarked": "Dimarkahi",
"type_label_synced": "Disinkronkan dari perangkat lain",
@@ -3149,37 +3171,43 @@
"confirm_history_delete_notice_p2": "Tindakan ini tidak bisa diurungkan.",
"menu_action_save_to_pocket": "Simpan ke Pocket",
"search_for_something_with": "Cari {search_term} lewat:",
"search_button": "Cari",
"search_header": "Pencarian {search_engine_name}",
"search_web_placeholder": "Cari di Web",
"search_settings": "Ubah Pengaturan Pencarian",
"section_info_option": "Info",
+ "section_info_send_feedback": "Kirim Umpan Balik",
+ "section_info_privacy_notice": "Kebijakan Privasi",
"welcome_title": "Selamat datang di tab baru",
"welcome_body": "Firefox akan menggunakan ruang ini untuk menampilkan markah, artikel, video, dan laman yang baru-baru ini dikunjungi, yang paling relevan agar Anda bisa kembali mengunjunginya dengan mudah.",
"welcome_label": "Mengidentifikasi Sorotan Anda",
"time_label_less_than_minute": "<1 mnt",
"time_label_minute": "{number} mnt",
"time_label_hour": "{number} jam",
"time_label_day": "{number} hr",
"settings_pane_button_label": "Ubahsuai laman Tab Baru Anda",
"settings_pane_header": "Preferensi Tab Baru",
- "settings_pane_body": "Pilih apa yang Anda lihat ketika Anda membuka tab baru.",
+ "settings_pane_body2": "Pilih apa yang Anda lihat di halaman ini.",
"settings_pane_search_header": "Pencarian",
"settings_pane_search_body": "Cari Web dari tab baru Anda.",
"settings_pane_topsites_header": "Situs Teratas",
"settings_pane_topsites_body": "Mengakses situs web yang paling sering Anda kunjungi.",
"settings_pane_topsites_options_showmore": "Tampilkan dua baris",
"settings_pane_bookmarks_header": "Markah Terbaru",
"settings_pane_bookmarks_body": "Markah Anda dibuat di lokasi yang praktis.",
"settings_pane_visit_again_header": "Kunjungi Lagi",
"settings_pane_visit_again_body": "Firefox akan menunjukkan bagian dari riwayat penjelajahan yang mungkin ingin Anda ingat atau kunjungi lagi.",
- "settings_pane_pocketstories_header": "Cerita Utama",
- "settings_pane_pocketstories_body": "Pocket, bagian dari keluarga Mozilla, akan membantu hubungkan Anda dengan konten berkualitas tinggi yang tak dapat Anda temukan di tempat lain.",
+ "settings_pane_highlights_header": "Sorotan",
+ "settings_pane_highlights_body2": "Temukan jalan kembali ke hal menarik yang baru saja Anda kunjungi atau dimarkah.",
+ "settings_pane_highlights_options_bookmarks": "Markah",
+ "settings_pane_highlights_options_visited": "Situs Terkunjungi",
+ "settings_pane_snippets_header": "Catatan Kecil",
+ "settings_pane_snippets_body": "Baca info pendek terbaru dari Mozilla tentang Firefox, budaya internet dan beberapa meme acak.",
"settings_pane_done_button": "Selesai",
"edit_topsites_button_text": "Sunting",
"edit_topsites_button_label": "Ubahsuai bagian Situs Teratas Anda",
"edit_topsites_showmore_button": "Tampilkan lainnya",
"edit_topsites_showless_button": "Tampilkan lebih sedikit",
"edit_topsites_done_button": "Selesai",
"edit_topsites_pin_button": "Sematkan situs ini",
"edit_topsites_unpin_button": "Lepaskan situs ini",
@@ -3192,20 +3220,20 @@
"topsites_form_url_placeholder": "Ketik atau tempel URL",
"topsites_form_add_button": "Tambah",
"topsites_form_save_button": "Simpan",
"topsites_form_cancel_button": "Batalkan",
"topsites_form_url_validation": "URL valid diperlukan",
"pocket_read_more": "Topik Populer:",
"pocket_read_even_more": "Lihat Cerita Lainnya",
"pocket_feedback_header": "Yang terbaik dari Web, dikurasi lebih dari 25 juta orang.",
- "pocket_feedback_body": "Pocket, bagian dari keluarga Mozilla, akan membantu hubungkan Anda dengan konten berkualitas tinggi yang tak dapat Anda temukan di tempat lain.",
- "pocket_send_feedback": "Kirim Umpanbalik",
+ "pocket_description": "Temukan konten berkualitas tinggi yang mungkin Anda lewatkan dengan bantuan Pocket, yang sekarang menjadi bagian dari Mozilla.",
+ "highlights_empty_state": "Mulai menjelajah, dan kami akan menampilkan beberapa artikel bagus, video, dan halaman lain yang baru saja Anda kunjungi atau termarkah di sini.",
"topstories_empty_state": "Maaf Anda tercegat. Periksa lagi nanti untuk lebih banyak cerita terbaik dari {provider}. Tidak mau menunggu? Pilih topik populer untuk menemukan lebih banyak cerita hebat dari seluruh web.",
- "manual_migration_explanation": "Cobalah Firefox dengan situs dan markah kesukaan Anda dari peramban yang lain.",
+ "manual_migration_explanation2": "Coba Firefox dengan markah, riwayat, dan sandi dari peramban lain.",
"manual_migration_cancel_button": "Tidak, Terima kasih",
"manual_migration_import_button": "Impor Sekarang"
},
"it": {
"newtab_page_title": "Nuova scheda",
"default_label_loading": "Caricamento…",
"header_top_sites": "Siti principali",
"header_stories": "Storie principali",
@@ -3483,16 +3511,17 @@
"manual_migration_cancel_button": "არა, გმადლობთ",
"manual_migration_import_button": "ახლავე გადმოტანა"
},
"kab": {
"newtab_page_title": "Iccer amaynut",
"default_label_loading": "Asali…",
"header_top_sites": "Ismal ifazen",
"header_stories": "Tiqsiɣin ifazen",
+ "header_highlights": "Asebrureq",
"header_visit_again": "Rzu tikelt-nniḍen",
"header_bookmarks": "Ticraḍ n melmi kan",
"header_recommended_by": "Iwelleh-it-id {provider}",
"header_bookmarks_placeholder": "Ur ɣur-k ara ticraḍ yakan.",
"header_stories_from": "seg",
"type_label_visited": "Yettwarza",
"type_label_bookmarked": "Yettwacreḍ",
"type_label_synced": "Yemtawi seg ibenk-nniḍen",
@@ -3514,37 +3543,43 @@
"confirm_history_delete_notice_p2": "Tigawt-agi ur tettuɣal ara ar deffir.",
"menu_action_save_to_pocket": "Sekles ɣer Pocket",
"search_for_something_with": "Nadi γef {search_term} s:",
"search_button": "Nadi",
"search_header": "Anadi {search_engine_name}",
"search_web_placeholder": "Nadi di Web",
"search_settings": "Snifel iγewwaṛen n unadi",
"section_info_option": "Talɣut",
+ "section_info_send_feedback": "Azen tikti",
+ "section_info_privacy_notice": "Tasertit n tbaḍnit",
"welcome_title": "Ansuf ar yiccer amaynut",
"welcome_body": "Firefox ad iseqdec tallunt akken ad d-yesken akk ticraḍ n isebtar iwulmen, imagraden, tividyutin, akked isebtar aniɣer terziḍ melmi kan, ihi tzemreḍ ad d-uɣaleḍ ɣer-sen s wudem fessusen.",
"welcome_label": "Asulu n iferdisen tisura",
"time_label_less_than_minute": "<1 n tesdat",
"time_label_minute": "{number} n tesdatin",
"time_label_hour": "{number} n isragen",
"time_label_day": "{number}n wussan",
"settings_pane_button_label": "Sagen asebter n yiccer-ik amaynut",
"settings_pane_header": "Ismenyifen n yiccer amaynut",
- "settings_pane_body": "Fren ayen ara twaliḍ ticki teldiḍ iccer imaynut.",
+ "settings_pane_body2": "Fren ayen ad twaliḍ deg usebter-agi.",
"settings_pane_search_header": "Nadi",
"settings_pane_search_body": "Nadi di Web seg iccer-ik amaynut.",
"settings_pane_topsites_header": "Ismal ifazen",
"settings_pane_topsites_body": "Kcem ar yesmal web i trezzuḍ s waṭas.",
"settings_pane_topsites_options_showmore": "Sken sin izirigen",
"settings_pane_bookmarks_header": "Ticraḍ n melmi kan",
"settings_pane_bookmarks_body": "Ticraḍ yettwarnan melmi kan deg iwen n umdiq ɣef afus.",
"settings_pane_visit_again_header": "Rzu tikelt-nniḍen",
"settings_pane_visit_again_body": "Firefox ad d-yesken tukkist n umazray-ik n tunigin i tzemreḍ ad twalid tikelt-nniḍen.",
- "settings_pane_pocketstories_header": "Tiqsiɣin ifazen",
- "settings_pane_pocketstories_body": "Pocket, aɛeggal n twaxult n Mozilla, ak-d-yefk afus ad twaliḍ agbur n tɣara meqqren i tzemred ad tzegleḍ.",
+ "settings_pane_highlights_header": "Asebrureq",
+ "settings_pane_highlights_body2": "Aff abrid-ik γer wayen i tḥemmleḍ i γef terziḍ yakan neγ tcerḍeḍ-t.",
+ "settings_pane_highlights_options_bookmarks": "Ticraḍ n isebtar",
+ "settings_pane_highlights_options_visited": "Ismal yettwarzan",
+ "settings_pane_snippets_header": "Tiwzillin",
+ "settings_pane_snippets_body": "Wali issalen n Mozilla γef Firefox, adlis internet, akked issalen nniṣen sya γer da.",
"settings_pane_done_button": "Immed",
"edit_topsites_button_text": "Ẓreg",
"edit_topsites_button_label": "Sagen tigezmi n ismal ifazen",
"edit_topsites_showmore_button": "Sken ugar",
"edit_topsites_showless_button": "Sken qel",
"edit_topsites_done_button": "Immed",
"edit_topsites_pin_button": "Ṭṭef asmel-agi",
"edit_topsites_unpin_button": "Serreḥ asmel-agi",
@@ -3557,20 +3592,20 @@
"topsites_form_url_placeholder": "Aru neɣ sekcem tansa URL",
"topsites_form_add_button": "Rnu",
"topsites_form_save_button": "Sekles",
"topsites_form_cancel_button": "Sefsex",
"topsites_form_url_validation": "Tansa URL tameɣtut tettwasra",
"pocket_read_more": "Isental ittwasnen aṭas:",
"pocket_read_even_more": "Wali ugar n teqsiḍin",
"pocket_feedback_header": "D amezwaru n Web, ittwafren sγur ugar 25 imelyan n imdanen.",
- "pocket_feedback_body": "Pocket, aɛeggal n twaxult n Mozilla, ak-d-yefk afus ad twaliḍ agbur n tɣara meqqren i tzemred ad tzegleḍ.",
- "pocket_send_feedback": "Azen tikti",
+ "pocket_description": "S lmendad n Pocket n Mozillan wali aqbur ifazen aṭas, s ttawil-a werǧin ad tzegleḍ taγawsa.",
+ "highlights_empty_state": "Bdu tuniginn sakin nekkni ad k-n-sken imagraden, tividyutin, akked isebtar nniḍen i γef terziḍ yakan neγ i tceṛḍeḍ dagi.",
"topstories_empty_state": "Ulac wiyaḍ. Uɣal-d ticki s wugar n imagraden seg {provider}. Ur tebɣiḍ ara ad terǧuḍ? Fren asentel seg wid yettwasnen akken ad twaliḍ imagraden yelhan di Web.",
- "manual_migration_explanation": "Ɛreḍ Firefox s ismal-ik inurifen akked ticraḍ seg iminig-nniḍen.",
+ "manual_migration_explanation2": "Σreḍ Firefox s ticṛaḍ n isebtar, amazray akked awalen uffiren sγur ilinigen nniḍen.",
"manual_migration_cancel_button": "Ala, tanemmirt",
"manual_migration_import_button": "Kter tura"
},
"kk": {
"newtab_page_title": "Жаңа бет",
"default_label_loading": "Жүктелуде…",
"header_top_sites": "Топ сайттар",
"header_stories": "Топ хикаялар",
@@ -3623,19 +3658,21 @@
"settings_pane_topsites_header": "Топ сайттар",
"settings_pane_topsites_body": "Көбірек қаралатын сайттарға қатынау.",
"settings_pane_topsites_options_showmore": "Екі жолды көрсету",
"settings_pane_bookmarks_header": "Соңғы бетбелгілер",
"settings_pane_bookmarks_body": "Сіздің жаңадан жасалған бетбелгілер бір ыңғайлы жерде.",
"settings_pane_visit_again_header": "Қайтадан шолу",
"settings_pane_visit_again_body": "Firefox сізге есте сақтауды немесе қайта шолуды қалауыңыз мүмкін тарихыңыздың бөліктерін көрсетеді.",
"settings_pane_highlights_header": "Ерекше жаңалықтар",
+ "settings_pane_highlights_body2": "Сіз жақында қараған немесе бетбелгілерге қосқан қызықты нәрселерге қайтатын жолды табыңыз.",
"settings_pane_highlights_options_bookmarks": "Бетбелгілер",
"settings_pane_highlights_options_visited": "Ашылған сайттар",
"settings_pane_snippets_header": "Үзінділер",
+ "settings_pane_snippets_body": "Mozilla-дан Firefox және интернет мәдениеті туралы қысқа жаңалықтарды, және кездейсоқ мемдерді оқыңыз.",
"settings_pane_done_button": "Дайын",
"edit_topsites_button_text": "Түзету",
"edit_topsites_button_label": "Топ сайттар санатын баптау",
"edit_topsites_showmore_button": "Көбірек көрсету",
"edit_topsites_showless_button": "Азырақ көрсету",
"edit_topsites_done_button": "Дайын",
"edit_topsites_pin_button": "Бұл сайтты жапсыру",
"edit_topsites_unpin_button": "Бұл сайтты бекітуден алып тастау",
@@ -3648,17 +3685,20 @@
"topsites_form_url_placeholder": "Сілтемені теріңіз немесе кірістіріңіз",
"topsites_form_add_button": "Қосу",
"topsites_form_save_button": "Сақтау",
"topsites_form_cancel_button": "Бас тарту",
"topsites_form_url_validation": "Жарамды сілтеме керек",
"pocket_read_more": "Әйгілі тақырыптар:",
"pocket_read_even_more": "Көбірек хикаяларды қарау",
"pocket_feedback_header": "Интернеттің ең жақсысы, 25 миллион адаммен танылған.",
+ "pocket_description": "Ол болмаса, сіз жіберіп алатын мүмкіндігі бар жоғары сапалы құраманы Pocket көмегімен табыңыз, ол енді Mozilla-ның бөлігі болып табылады.",
+ "highlights_empty_state": "Шолуды бастаңыз, сіз жақында шолған немесе бетбелгілерге қосқан тамаша мақалалар, видеолар немесе басқа парақтардың кейбіреулері осында көрсетіледі.",
"topstories_empty_state": "Дайын. {provider} ұсынған көбірек мақалаларды алу үшін кейінірек тексеріңіз. Күте алмайсыз ба? Интернеттен көбірек тамаша мақалаларды алу үшін әйгілі теманы таңдаңыз.",
+ "manual_migration_explanation2": "Firefox-ты басқа браузер бетбелгілері, тарихы және парольдерімен қолданып көріңіз.",
"manual_migration_cancel_button": "Жоқ, рахмет",
"manual_migration_import_button": "Қазір импорттау"
},
"km": {
"newtab_page_title": "ផ្ទាំងថ្មី",
"default_label_loading": "កំពុងផ្ទុក...",
"header_top_sites": "វិបសាយលើគេ",
"header_highlights": "ការរំលេច",
@@ -4371,16 +4411,17 @@
"topsites_form_url_placeholder": "Skriv eller lim inn en URL",
"topsites_form_add_button": "Legg til",
"topsites_form_save_button": "Lagre",
"topsites_form_cancel_button": "Avbryt",
"topsites_form_url_validation": "Gyldig URL er nødvendig",
"pocket_read_more": "Populære emner:",
"pocket_read_even_more": "Vis flere saker",
"pocket_feedback_header": "Det beste av nettet, kurert av over 25 millioner mennesker.",
+ "pocket_description": "Oppdag høykvalitetsinnhold som du ellers ville gå glipp av, ved hjelp av Pocket, som nå er en del av Mozilla.",
"highlights_empty_state": "Begynn å surfe, og vi viser noen av de beste artiklene, videoer og andre sider du nylig har besøkt eller bokmerket her.",
"topstories_empty_state": "Du har tatt igjen. Kom tilbake senere for flere topphistorier fra {provider}. Kan du ikke vente? Velg et populært emne for å finne flere gode artikler fra hele Internett.",
"manual_migration_explanation2": "Prøv Firefox med bokmerkene, historikk og passord fra en annen nettleser.",
"manual_migration_cancel_button": "Nei takk",
"manual_migration_import_button": "Importer nå"
},
"ne-NP": {
"newtab_page_title": "नयाँ ट्याब",
@@ -4986,17 +5027,17 @@
"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_dismiss": "Sbittar",
"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",
@@ -5366,31 +5407,33 @@
"settings_pane_bookmarks_header": "Nedavni zaznamki",
"settings_pane_bookmarks_body": "Vaši novo ustvarjeni zaznamki na enem mestu.",
"settings_pane_visit_again_header": "Obiščite znova",
"settings_pane_visit_again_body": "Firefox vam bo prikazoval dele zgodovine brskanja, ki bi se jih morda želeli spomniti ali se nanje vrniti.",
"settings_pane_highlights_header": "Poudarki",
"settings_pane_highlights_body2": "Najdite pot nazaj do zanimivih strani, ki ste jih nedavno obiskali ali dodali med zaznamke.",
"settings_pane_highlights_options_bookmarks": "Zaznamki",
"settings_pane_highlights_options_visited": "Obiskane strani",
+ "settings_pane_snippets_header": "Izrezki",
+ "settings_pane_snippets_body": "Spremljajte kratke novice o Mozilli in Firefoxu, kulturi interneta in si občasno oglejte kak meme.",
"settings_pane_done_button": "Končano",
"edit_topsites_button_text": "Uredi",
"edit_topsites_button_label": "Prilagodite odsek Glavne strani",
"edit_topsites_showmore_button": "Prikaži več",
"edit_topsites_showless_button": "Prikaži manj",
"edit_topsites_done_button": "Končano",
"edit_topsites_pin_button": "Pripni to stran",
"edit_topsites_unpin_button": "Odpni to stran",
"edit_topsites_edit_button": "Uredi to stran",
"edit_topsites_dismiss_button": "Odstrani to stran",
"edit_topsites_add_button": "Dodaj",
"topsites_form_add_header": "Nova glavna stran",
"topsites_form_edit_header": "Uredi glavno stran",
- "topsites_form_title_placeholder": "Vnesite naslov",
- "topsites_form_url_placeholder": "Vnesite ali prilepite URL",
+ "topsites_form_title_placeholder": "Vnesite ime",
+ "topsites_form_url_placeholder": "Vnesite ali prilepite spletni naslov",
"topsites_form_add_button": "Dodaj",
"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_description": "Odkrijte kakovostno vsebino, ki bi jo sicer spregledali, s pomočjo Pocketa (zdaj dela Mozille).",
@@ -5720,16 +5763,17 @@
"manual_migration_cancel_button": "பரவாயில்லை",
"manual_migration_import_button": "இப்போது இறக்கு"
},
"te": {
"newtab_page_title": "కొత్త ట్యాబు",
"default_label_loading": "వస్తోంది…",
"header_top_sites": "మేటి సైట్లు",
"header_stories": "ముఖ్య కథనాలు",
+ "header_highlights": "విశేషాలు",
"header_visit_again": "మళ్లీ సందర్శించండి",
"header_bookmarks": "ఇటీవలి ఇష్టాంశములు",
"header_recommended_by": "{provider}చే సిఫార్సు చేయబడినది",
"header_bookmarks_placeholder": "మీకు ఇంకా ఎటువంటి ఇష్టాంశాలు లేవు.",
"header_stories_from": "నుండి",
"type_label_visited": "సందర్శించినవి",
"type_label_bookmarked": "ఇష్టాంశము చేయబడినది",
"type_label_synced": "మరో పరికరం నుంచి సమకాలీకరించి తెచ్చుకున్నవి",
@@ -5751,37 +5795,40 @@
"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": "సమాచారం",
+ "section_info_send_feedback": "అభిప్రాయాన్ని పంపండి",
+ "section_info_privacy_notice": "గోప్యతా విధానం",
"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": "మీ కొత్త ట్యాబు పేజీని మలచుకోండి",
"settings_pane_header": "కొత్త ట్యాబు అభిరుచులు",
- "settings_pane_body": "మీరు కొత్త ట్యాబు తెరిచినప్పుడు ఏం చూడాలో ఎంచుకోండి.",
+ "settings_pane_body2": "మీరు ఈ పేజీలో చూసేదాన్ని ఎంచుకోండి.",
"settings_pane_search_header": "వెతకడం",
"settings_pane_search_body": "కొత్త ట్యాబు నుండే జాలంలో వెతకండి.",
"settings_pane_topsites_header": "మేటి సైట్లు",
"settings_pane_topsites_body": "మీరు ఎక్కువగా చూసే వెబ్సైట్లను చూడండి.",
"settings_pane_topsites_options_showmore": "రెండు వరుసలు చూపించు",
"settings_pane_bookmarks_header": "ఇటీవలి బుక్మార్క్లు",
"settings_pane_bookmarks_body": "ఒక సులభ స్థానంలో మీ క్రొత్తగా సృష్టించిన బుక్మార్క్లు.",
"settings_pane_visit_again_header": "మళ్లీ సందర్శించండి",
"settings_pane_visit_again_body": "మీరు బ్రౌజింగ్ చరిత్రలో గుర్తుంచుకోవాల్సిన లేదా తిరిగి పొందవలసిన భాగాలను చూపిస్తుంది.",
- "settings_pane_pocketstories_header": "ముఖ్య కథనాలు",
- "settings_pane_pocketstories_body": "Mozilla కుటుంబం యొక్క Pocket, మీరు కనుగొనలేకపోయే అధిక-నాణ్యత విషయముకి మిమ్మల్ని అనుసంధానించడానికి సహాయపడుతుంది.",
+ "settings_pane_highlights_header": "విశేషాలు",
+ "settings_pane_highlights_options_bookmarks": "ఇష్టాంశాలు",
+ "settings_pane_highlights_options_visited": "చూసిన సైటులు",
"settings_pane_done_button": "పూర్తయింది",
"edit_topsites_button_text": "మార్చు",
"edit_topsites_button_label": "మీ మేటి సైట్ల విభాగాన్ని మలచుకోండి",
"edit_topsites_showmore_button": "ఇంకా చూపించు",
"edit_topsites_showless_button": "కొన్నే చూపించు",
"edit_topsites_done_button": "పూర్తయింది",
"edit_topsites_pin_button": "ఈ సైటును ఇక్కడ గుచ్చు",
"edit_topsites_unpin_button": "ఈ సైటుకి పిన్నుని తీసివేయండి",
@@ -5794,20 +5841,17 @@
"topsites_form_url_placeholder": "URL ను టైప్ చేయండి లేదా అతికించండి",
"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": "Mozilla కుటుంబం యొక్క Pocket, మీరు కనుగొనలేకపోయే అధిక-నాణ్యత విషయముకి మిమ్మల్ని అనుసంధానించడానికి సహాయపడుతుంది.",
- "pocket_send_feedback": "అభిప్రాయాన్ని పంపండి",
"topstories_empty_state": "మీరు పట్టుబడ్డారు. {provider} నుండి మరింత అగ్ర కథనాల కోసం తరువాత తనిఖీ చేయండి. వేచి ఉండలేరా? జాలములోని అంతటి నుండి మరింత గొప్ప కథనాలను కనుగొనడానికి ప్రసిద్ధ అంశం ఎంచుకోండి.",
- "manual_migration_explanation": "మరొక విహరణి నుండి మీకు ఇష్టమైన సైట్లు మరియు ఇష్టంశాలతో Firefox ను ప్రయత్నించండి.",
"manual_migration_cancel_button": "అడిగినందుకు ధన్యవాదాలు, వద్దు",
"manual_migration_import_button": "ఇప్పుడే దిగుమతి చేయండి"
},
"th": {
"newtab_page_title": "แท็บใหม่",
"default_label_loading": "กำลังโหลด…",
"header_top_sites": "ไซต์เด่น",
"header_stories": "เรื่องราวเด่น",
@@ -5860,16 +5904,17 @@
"settings_pane_topsites_header": "ไซต์เด่น",
"settings_pane_topsites_body": "เข้าถึงเว็บไซต์ที่คุณเยี่ยมชมมากที่สุด",
"settings_pane_topsites_options_showmore": "แสดงสองแถว",
"settings_pane_bookmarks_header": "ที่คั่นหน้าเมื่อเร็ว ๆ นี้",
"settings_pane_bookmarks_body": "ที่คั่นหน้าที่สร้างใหม่ของคุณในตำแหน่งที่ตั้งเดียวที่สะดวก",
"settings_pane_visit_again_header": "เยี่ยมชมอีกครั้ง",
"settings_pane_highlights_header": "รายการเด่น",
"settings_pane_highlights_options_bookmarks": "ที่คั่นหน้า",
+ "settings_pane_highlights_options_visited": "ไซต์ที่เยี่ยมชมแล้ว",
"settings_pane_done_button": "เสร็จสิ้น",
"edit_topsites_button_text": "แก้ไข",
"edit_topsites_button_label": "ปรับแต่งส่วนไซต์เด่นของคุณ",
"edit_topsites_showmore_button": "แสดงเพิ่มเติม",
"edit_topsites_showless_button": "แสดงน้อยลง",
"edit_topsites_done_button": "เสร็จสิ้น",
"edit_topsites_pin_button": "ปักหมุดไซต์นี้",
"edit_topsites_unpin_button": "ถอนหมุดไซต์นี้",
@@ -5882,16 +5927,17 @@
"topsites_form_url_placeholder": "พิมพ์หรือวาง URL",
"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 ล้านคน",
+ "manual_migration_explanation2": "ลอง Firefox ด้วยที่คั่นหน้า, ประวัติ และรหัสผ่านจากเบราว์เซอร์อื่น",
"manual_migration_cancel_button": "ไม่ ขอบคุณ",
"manual_migration_import_button": "นำเข้าตอนนี้"
},
"tl": {
"newtab_page_title": "Bagong Tab",
"default_label_loading": "Pagkarga…",
"header_top_sites": "Tuktok na mga Site",
"header_highlights": "Highlights",
@@ -6289,18 +6335,18 @@
"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_open_new_window": "新建窗口打开",
+ "menu_action_open_private_window": "新建隐私浏览窗口打开",
"menu_action_dismiss": "隐藏",
"menu_action_delete": "从历史记录中删除",
"menu_action_pin": "固定",
"menu_action_unpin": "取消固定",
"confirm_history_delete_p1": "确定删除此页面在您的历史记录中的所有记录?",
"confirm_history_delete_notice_p2": "此操作不能撤销。",
"menu_action_save_to_pocket": "保存到 Pocket",
"search_for_something_with": "搜索 {search_term},使用:",
--- a/browser/extensions/activity-stream/install.rdf.in
+++ b/browser/extensions/activity-stream/install.rdf.in
@@ -3,17 +3,17 @@
#filter substitution
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>activity-stream@mozilla.org</em:id>
<em:type>2</em:type>
<em:bootstrap>true</em:bootstrap>
<em:unpack>false</em:unpack>
- <em:version>2017.09.08.0882-3dbf720c</em:version>
+ <em:version>2017.09.11.1306-373d9fc</em:version>
<em:name>Activity Stream</em:name>
<em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -72,16 +72,20 @@ const PREFS_CONFIG = new Map([
["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
}],
+ ["prerender", {
+ title: "Use the prerendered version of activity-stream.html. This is set automatically by PrefsFeed.jsm.",
+ value: true
+ }],
["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
}],
--- a/browser/extensions/activity-stream/lib/HighlightsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/HighlightsFeed.jsm
@@ -5,23 +5,25 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {shortURL} = Cu.import("resource://activity-stream/lib/ShortURL.jsm", {});
const {SectionsManager} = Cu.import("resource://activity-stream/lib/SectionsManager.jsm", {});
+const {TOP_SITES_SHOWMORE_LENGTH} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
const HIGHLIGHTS_MAX_LENGTH = 9;
const HIGHLIGHTS_UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
+const MANY_EXTRA_LENGTH = HIGHLIGHTS_MAX_LENGTH * 5 + TOP_SITES_SHOWMORE_LENGTH;
const SECTION_ID = "highlights";
this.HighlightsFeed = class HighlightsFeed {
constructor() {
this.highlightsLastUpdated = 0;
this.highlights = [];
this.dedupe = new Dedupe(this._dedupeKey);
}
@@ -38,29 +40,50 @@ this.HighlightsFeed = class HighlightsFe
SectionsManager.enableSection(SECTION_ID);
}
uninit() {
SectionsManager.disableSection(SECTION_ID);
}
async fetchHighlights(broadcast = false) {
- this.highlights = await NewTabUtils.activityStreamLinks.getHighlights();
- for (let highlight of this.highlights) {
- highlight.hostname = shortURL(Object.assign({}, highlight, {url: highlight.url}));
- highlight.image = highlight.preview_image_url;
- if (highlight.bookmarkGuid) {
- highlight.type = "bookmark";
+ // Request more than the expected length to allow for items being removed by
+ // deduping against Top Sites or multiple history from the same domain, etc.
+ const manyPages = await NewTabUtils.activityStreamLinks.getHighlights({numItems: MANY_EXTRA_LENGTH});
+
+ // Remove any Highlights that are in Top Sites already
+ const deduped = this.dedupe.group(this.store.getState().TopSites.rows, manyPages)[1];
+
+ // Keep all "bookmark"s and at most one (most recent) "history" per host
+ this.highlights = [];
+ const hosts = new Set();
+ for (const page of deduped) {
+ const hostname = shortURL(page);
+ // Skip this history page if we already something from the same host
+ if (page.type === "history" && hosts.has(hostname)) {
+ continue;
+ }
+
+ // We want the page, so update various fields for UI
+ Object.assign(page, {
+ hostname,
+ image: page.preview_image_url,
+ type: page.bookmarkGuid ? "bookmark" : page.type
+ });
+
+ // Add the "bookmark" or not-skipped "history"
+ this.highlights.push(page);
+ hosts.add(hostname);
+
+ // Skip the rest if we have enough items
+ if (this.highlights.length === HIGHLIGHTS_MAX_LENGTH) {
+ break;
}
}
- // Remove any Highlights that are in Top Sites already
- const deduped = this.dedupe.group(this.store.getState().TopSites.rows, this.highlights);
- this.highlights = deduped[1];
-
SectionsManager.updateSection(SECTION_ID, {rows: this.highlights}, this.highlightsLastUpdated === 0 || broadcast);
this.highlightsLastUpdated = Date.now();
}
onAction(action) {
switch (action.type) {
case at.INIT:
this.init();
--- a/browser/extensions/activity-stream/lib/NewTabInit.jsm
+++ b/browser/extensions/activity-stream/lib/NewTabInit.jsm
@@ -7,20 +7,38 @@ const {utils: Cu} = Components;
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
/**
* NewTabInit - A placeholder for now. This will send a copy of the state to all
* newly opened tabs.
*/
this.NewTabInit = class NewTabInit {
+ constructor() {
+ this._queue = new Set();
+ }
+ reply(target) {
+ const action = {type: at.NEW_TAB_INITIAL_STATE, data: this.store.getState()};
+ this.store.dispatch(ac.SendToContent(action, target));
+ }
onAction(action) {
- let newAction;
switch (action.type) {
- case at.NEW_TAB_LOAD:
- newAction = {type: at.NEW_TAB_INITIAL_STATE, data: this.store.getState()};
- this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
+ case at.NEW_TAB_STATE_REQUEST:
+ // If localization hasn't been loaded yet, we should wait for it.
+ if (!this.store.getState().App.strings) {
+ this._queue.add(action.meta.fromTarget);
+ return;
+ }
+ this.reply(action.meta.fromTarget);
+ break;
+ case at.LOCALE_UPDATED:
+ // If the queue is full because we were waiting for strings,
+ // dispatch them now.
+ if (this._queue.size > 0 && this.store.getState().App.strings) {
+ this._queue.forEach(target => this.reply(target));
+ this._queue.clear();
+ }
break;
}
}
};
this.EXPORTED_SYMBOLS = ["NewTabInit"];
--- a/browser/extensions/activity-stream/lib/PrefsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PrefsFeed.jsm
@@ -2,38 +2,62 @@
* 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;
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
+const {PrerenderData} = Cu.import("resource://activity-stream/common/PrerenderData.jsm", {});
this.PrefsFeed = class PrefsFeed {
constructor(prefMap) {
this._prefMap = prefMap;
this._prefs = new Prefs();
}
+
+ // If the any prefs are set to something other than what the prerendered version
+ // of AS expects, we can't use it.
+ _setPrerenderPref() {
+ for (const prefName of PrerenderData.invalidatingPrefs) {
+ if (this._prefs.get(prefName) !== PrerenderData.initialPrefs[prefName]) {
+ this._prefs.set("prerender", false);
+ return;
+ }
+ }
+ this._prefs.set("prerender", true);
+ }
+
+ _checkPrerender(name) {
+ if (PrerenderData.invalidatingPrefs.includes(name)) {
+ this._setPrerenderPref();
+ }
+ }
+
onPrefChanged(name, value) {
if (this._prefMap.has(name)) {
this.store.dispatch(ac.BroadcastToContent({type: at.PREF_CHANGED, data: {name, value}}));
}
+ this._checkPrerender(name, value);
}
+
init() {
this._prefs.observeBranch(this);
// Get the initial value of each activity stream pref
const values = {};
for (const name of this._prefMap.keys()) {
values[name] = this._prefs.get(name);
}
// Set the initial state of all prefs in redux
this.store.dispatch(ac.BroadcastToContent({type: at.PREFS_INITIAL_VALUES, data: values}));
+
+ this._setPrerenderPref();
}
removeListeners() {
this._prefs.ignoreBranch(this);
}
onAction(action) {
switch (action.type) {
case at.INIT:
this.init();
--- a/browser/extensions/activity-stream/lib/SectionsManager.jsm
+++ b/browser/extensions/activity-stream/lib/SectionsManager.jsm
@@ -43,17 +43,17 @@ const BUILT_IN_SECTIONS = {
titleString: {id: "settings_pane_highlights_header"},
descString: {id: "settings_pane_highlights_body2"}
},
shouldHidePref: false,
eventSource: "HIGHLIGHTS",
icon: "highlights",
title: {id: "header_highlights"},
maxRows: 3,
- availableContextMenuOptions: ["CheckBookmark", "SaveToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
+ availableContextMenuOptions: ["CheckBookmark", "SaveToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
emptyState: {
message: {id: "highlights_empty_state"},
icon: "highlights"
},
order: 1
})
};
--- a/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
@@ -116,17 +116,17 @@ this.SnippetsFeed = class SnippetsFeed {
}
uninit() {
Services.prefs.removeObserver(ONBOARDING_FINISHED_PREF, this._refresh);
Services.prefs.removeObserver(SNIPPETS_URL_PREF, this._refresh);
Services.prefs.removeObserver(TELEMETRY_PREF, this._refresh);
Services.prefs.removeObserver(FXA_USERNAME_PREF, this._refresh);
Services.obs.removeObserver(this, SEARCH_ENGINE_OBSERVER_TOPIC);
- this.store.dispatch({type: at.SNIPPETS_RESET});
+ this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_RESET}));
}
showFirefoxAccounts(browser) {
// We want to replace the current tab.
browser.loadURI("about:accounts?action=signup&entrypoint=snippets");
}
onAction(action) {
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -16,16 +16,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Screenshots",
"resource://activity-stream/lib/Screenshots.jsm");
const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
const DEFAULT_SITES_PREF = "default.sites";
const DEFAULT_TOP_SITES = [];
const FRECENCY_THRESHOLD = 100; // 1 visit (skip first-run/one-time pages)
+const MIN_FAVICON_SIZE = 96;
this.TopSitesFeed = class TopSitesFeed {
constructor() {
this.lastUpdated = 0;
this._tippyTopProvider = new TippyTopProvider();
this.dedupe = new Dedupe(this._dedupeKey);
}
_dedupeKey(site) {
@@ -90,23 +91,23 @@ this.TopSitesFeed = class TopSitesFeed {
// First, cache existing screenshots in case we need to reuse them
const currentScreenshots = {};
for (const link of this.store.getState().TopSites.rows) {
if (link && link.screenshot) {
currentScreenshots[link.url] = link.screenshot;
}
}
- // Now, get a tippy top icon or screenshot for every item
+ // Now, get a tippy top icon, a rich icon, or screenshot for every item
for (let link of links) {
if (!link) { continue; }
- // Check for tippy top icon.
+ // Check for tippy top icon or a rich icon.
link = this._tippyTopProvider.processSite(link);
- if (link.tippyTopIcon) { continue; }
+ if (link.tippyTopIcon || link.faviconSize >= MIN_FAVICON_SIZE) { continue; }
// If no tippy top, then we get a screenshot.
if (currentScreenshots[link.url]) {
link.screenshot = currentScreenshots[link.url];
} else {
this.getScreenshot(link.url);
}
}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/activity-stream-prerender.test.jsx
@@ -0,0 +1,70 @@
+const prerender = require("content-src/activity-stream-prerender");
+const {prerenderStore} = prerender;
+const {PrerenderData} = require("common/PrerenderData.jsm");
+
+describe("prerenderStore", () => {
+ it("should create a store", () => {
+ const store = prerenderStore();
+
+ assert.isFunction(store.getState);
+ });
+ it("should start uninitialized", () => {
+ const store = prerenderStore();
+
+ const state = store.getState();
+ assert.equal(state.App.initialized, false);
+ });
+ it("should set the right locale, strings, and text direction", () => {
+ const strings = {foo: "foo"};
+
+ const store = prerenderStore("en-FOO", strings);
+
+ const state = store.getState();
+ assert.equal(state.App.locale, "en-FOO");
+ assert.equal(state.App.strings, strings);
+ assert.equal(state.App.textDirection, "ltr");
+ });
+ it("should add the right initial prefs", () => {
+ const store = prerenderStore();
+
+ const state = store.getState();
+ assert.equal(state.Prefs.values, PrerenderData.initialPrefs);
+ });
+ it("should add TopStories as the first section", () => {
+ const store = prerenderStore();
+
+ const state = store.getState();
+ // TopStories
+ const firstSection = state.Sections[0];
+ assert.equal(firstSection.id, "topstories");
+ // it should start uninitialized
+ assert.equal(firstSection.initialized, false);
+ });
+});
+
+describe("prerender", () => {
+ it("should set the locale and get the right strings of whatever is passed in", () => {
+ const {store} = prerender("en-US");
+
+ const state = store.getState();
+ assert.equal(state.App.locale, "en-US");
+ assert.equal(state.App.strings.newtab_page_title, "New Tab");
+ });
+ it("should throw if an unknown locale is passed in", () => {
+ assert.throws(() => prerender("en-FOO"));
+ });
+ it("should set the locale to en-PRERENDER and have empty strings if no locale is passed in", () => {
+ const {store} = prerender();
+
+ const state = store.getState();
+ assert.equal(state.App.locale, "en-PRERENDER");
+ assert.equal(state.App.strings.newtab_page_title, " ");
+ });
+ // # TODO: Remove when #3370 is resolved.
+ it("should render a real English string for search_web_placeholder", () => {
+ const {store} = prerender();
+
+ const state = store.getState();
+ assert.equal(state.App.strings.search_web_placeholder, "Search the Web");
+ });
+});
--- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -21,22 +21,30 @@ describe("Reducers", () => {
assert.propertyVal(nextState, "version", "1.2.3");
assert.propertyVal(nextState, "locale", INITIAL_STATE.App.locale);
});
it("should not update state for empty action.data on LOCALE_UPDATED", () => {
const nextState = App(undefined, {type: at.LOCALE_UPDATED});
assert.equal(nextState, INITIAL_STATE.App);
});
- it("should set locale, strings on LOCALE_UPDATE", () => {
+ it("should set locale, strings and text direction on LOCALE_UPDATE", () => {
const strings = {};
const action = {type: "LOCALE_UPDATED", data: {locale: "zh-CN", strings}};
const nextState = App(undefined, action);
assert.propertyVal(nextState, "locale", "zh-CN");
assert.propertyVal(nextState, "strings", strings);
+ assert.propertyVal(nextState, "textDirection", "ltr");
+ });
+ it("should set rtl text direction for RTL locales", () => {
+ const action = {type: "LOCALE_UPDATED", data: {locale: "ar"}};
+
+ const nextState = App(undefined, action);
+
+ assert.propertyVal(nextState, "textDirection", "rtl");
});
});
describe("TopSites", () => {
it("should return the initial state", () => {
const nextState = TopSites(undefined, {type: "FOO"});
assert.equal(nextState, INITIAL_STATE.TopSites);
});
it("should add top sites on TOP_SITES_UPDATED", () => {
--- a/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
@@ -25,17 +25,17 @@ describe("Top Sites Feed", () => {
fakeNewTabUtils = {activityStreamLinks: {getHighlights: sandbox.spy(() => Promise.resolve(links))}};
sectionsManagerStub = {
onceInitialized: sinon.stub().callsFake(callback => callback()),
enableSection: sinon.spy(),
disableSection: sinon.spy(),
updateSection: sinon.spy(),
sections: new Map([["highlights", {}]])
};
- shortURLStub = sinon.stub().callsFake(site => site.url);
+ shortURLStub = sinon.stub().callsFake(site => site.url.match(/\/([^/]+)/)[1]);
globals.set("NewTabUtils", fakeNewTabUtils);
({HighlightsFeed, HIGHLIGHTS_UPDATE_TIME, SECTION_ID} = injector({
"lib/ShortURL.jsm": {shortURL: shortURLStub},
"lib/SectionsManager.jsm": {SectionsManager: sectionsManagerStub},
"common/Dedupe.jsm": {Dedupe}
}));
feed = new HighlightsFeed();
feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill(null).map((v, i) => ({url: `http://www.topsite${i}.com`}))}}; }};
@@ -67,30 +67,54 @@ describe("Top Sites Feed", () => {
assert.notCalled(feed.fetchHighlights);
});
});
describe("#fetchHighlights", () => {
it("should add hostname and image to each link", async () => {
links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
await feed.fetchHighlights();
- assert.equal(feed.highlights[0].hostname, links[0].url);
+ assert.equal(feed.highlights[0].hostname, "mozilla.org");
assert.equal(feed.highlights[0].image, links[0].preview_image_url);
});
it("should not include any links already in Top Sites", async () => {
links = [
{url: "https://mozilla.org"},
{url: "http://www.topsite0.com"},
{url: "http://www.topsite1.com"},
{url: "http://www.topsite2.com"}
];
await feed.fetchHighlights();
assert.equal(feed.highlights.length, 1);
assert.deepEqual(feed.highlights[0], links[0]);
});
+ it("should not include history of same hostname as a bookmark", async () => {
+ links = [
+ {url: "https://site.com/bookmark", type: "bookmark"},
+ {url: "https://site.com/history", type: "history"}
+ ];
+
+ await feed.fetchHighlights();
+
+ assert.equal(feed.highlights.length, 1);
+ assert.deepEqual(feed.highlights[0], links[0]);
+ });
+ it("should take the first history of a hostname", async () => {
+ links = [
+ {url: "https://site.com/first", type: "history"},
+ {url: "https://site.com/second", type: "history"},
+ {url: "https://other", type: "history"}
+ ];
+
+ await feed.fetchHighlights();
+
+ assert.equal(feed.highlights.length, 2);
+ assert.deepEqual(feed.highlights[0], links[0]);
+ assert.deepEqual(feed.highlights[1], links[2]);
+ });
it("should set type to bookmark if there is a bookmarkGuid", async () => {
links = [{url: "https://mozilla.org", type: "history", bookmarkGuid: "1234567890"}];
await feed.fetchHighlights();
assert.equal(feed.highlights[0].type, "bookmark");
});
});
describe("#uninit", () => {
it("should disable its section", () => {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/NewTabInit.test.js
@@ -0,0 +1,62 @@
+const {NewTabInit} = require("lib/NewTabInit.jsm");
+const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
+
+describe("NewTabInit", () => {
+ let instance;
+ let store;
+ let STATE;
+ beforeEach(() => {
+ STATE = {};
+ store = {getState: sinon.stub().returns(STATE), dispatch: sinon.stub()};
+ instance = new NewTabInit();
+ instance.store = store;
+ });
+ it("should reply with a copy of the state immediately if localization is ready", () => {
+ STATE.App = {strings: {}};
+
+ instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, 123));
+
+ const resp = ac.SendToContent({type: at.NEW_TAB_INITIAL_STATE, data: STATE}, 123);
+ assert.calledWith(store.dispatch, resp);
+ });
+ it("should not reply immediately if localization is not ready", () => {
+ STATE.App = {strings: null};
+
+ instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, 123));
+
+ assert.notCalled(store.dispatch);
+ });
+ it("should dispatch responses for queued targets when LOCALE_UPDATED is received", () => {
+ STATE.App = {strings: null};
+
+ // Send requests before strings are ready
+ instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "foo"));
+ instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "bar"));
+ instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "baz"));
+ assert.notCalled(store.dispatch);
+
+ // Update strings
+ STATE.App = {strings: {}};
+ instance.onAction({type: at.LOCALE_UPDATED});
+
+ assert.calledThrice(store.dispatch);
+ const action = {type: at.NEW_TAB_INITIAL_STATE, data: STATE};
+ assert.calledWith(store.dispatch, ac.SendToContent(action, "foo"));
+ assert.calledWith(store.dispatch, ac.SendToContent(action, "bar"));
+ assert.calledWith(store.dispatch, ac.SendToContent(action, "baz"));
+ });
+ it("should clear targets from the queue once they have been sent", () => {
+ STATE.App = {strings: null};
+ instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "foo"));
+ instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "bar"));
+ instance.onAction(ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST}, "baz"));
+
+ STATE.App = {strings: {}};
+ instance.onAction({type: at.LOCALE_UPDATED});
+ assert.calledThrice(store.dispatch);
+
+ store.dispatch.reset();
+ instance.onAction({type: at.LOCALE_UPDATED});
+ assert.notCalled(store.dispatch);
+ });
+});
--- a/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js
@@ -1,21 +1,25 @@
const {PrefsFeed} = require("lib/PrefsFeed.jsm");
const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
+const {PrerenderData} = require("common/PrerenderData.jsm");
+const {initialPrefs} = PrerenderData;
-const FAKE_PREFS = new Map([["foo", {value: 1}], ["bar", {value: 2}]]);
+const PRERENDER_PREF_NAME = "prerender";
describe("PrefsFeed", () => {
let feed;
+ let FAKE_PREFS;
beforeEach(() => {
+ FAKE_PREFS = new Map([["foo", 1], ["bar", 2]]);
feed = new PrefsFeed(FAKE_PREFS);
feed.store = {dispatch: sinon.spy()};
feed._prefs = {
- get: sinon.spy(item => FAKE_PREFS.get(item).value),
- set: sinon.spy(),
+ get: sinon.spy(item => FAKE_PREFS.get(item)),
+ set: sinon.spy((name, value) => FAKE_PREFS.set(name, value)),
observe: sinon.spy(),
observeBranch: sinon.spy(),
ignore: sinon.spy(),
ignoreBranch: sinon.spy()
};
});
it("should set a pref when a SET_PREF action is received", () => {
feed.onAction(ac.SetPref("foo", 2));
@@ -36,9 +40,47 @@ describe("PrefsFeed", () => {
feed.onAction({type: at.UNINIT});
assert.calledOnce(feed._prefs.ignoreBranch);
assert.calledWith(feed._prefs.ignoreBranch, feed);
});
it("should send a PREF_CHANGED action when onPrefChanged is called", () => {
feed.onPrefChanged("foo", 2);
assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({type: at.PREF_CHANGED, data: {name: "foo", value: 2}}));
});
+ describe("INIT prerendering", () => {
+ it("should set a prerender pref on init", () => {
+ feed.onAction({type: at.INIT});
+ assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME);
+ });
+ it("should set prerender pref to true if prefs match initial values", () => {
+ Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
+ feed.onAction({type: at.INIT});
+ assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, true);
+ });
+ it("should set prerender pref to false if a pref does not match its initial value", () => {
+ Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
+ FAKE_PREFS.set("feeds.section.topstories", false);
+ feed.onAction({type: at.INIT});
+ assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, false);
+ });
+ });
+ describe("onPrefChanged prerendering", () => {
+ it("should not change the prerender pref if the pref is not included in invalidatingPrefs", () => {
+ feed.onPrefChanged("foo123", true);
+ assert.notCalled(feed._prefs.set);
+ });
+ it("should set the prerender pref to false if a pref in invalidatingPrefs is changed from its original value", () => {
+ Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
+
+ feed._prefs.set("feeds.section.topstories", false);
+ feed.onPrefChanged("feeds.section.topstories", false);
+ assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, false);
+ });
+ it("should set the prerender pref back to true if the invalidatingPrefs are changed back to their original values", () => {
+ Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
+ FAKE_PREFS.set("feeds.section.topstories", false);
+
+ feed._prefs.set("feeds.section.topstories", true);
+ feed.onPrefChanged("feeds.section.topstories", true);
+ assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, true);
+ });
+ });
});
--- a/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
@@ -1,10 +1,10 @@
const {SnippetsFeed} = require("lib/SnippetsFeed.jsm");
-const {actionTypes: at} = require("common/Actions.jsm");
+const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
const {GlobalOverrider} = require("test/unit/utils");
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
const searchData = {searchEngineIdentifier: "google", engines: ["searchEngine-google", "searchEngine-bing"]};
let overrider = new GlobalOverrider();
describe("SnippetsFeed", () => {
@@ -73,23 +73,23 @@ describe("SnippetsFeed", () => {
it("should call .uninit on an UNINIT action", () => {
const feed = new SnippetsFeed();
sandbox.stub(feed, "uninit");
feed.store = {dispatch: sandbox.stub()};
feed.onAction({type: at.UNINIT});
assert.calledOnce(feed.uninit);
});
- it("should dispatch a SNIPPETS_RESET on uninit", () => {
+ it("should broadcast a SNIPPETS_RESET on uninit", () => {
const feed = new SnippetsFeed();
feed.store = {dispatch: sandbox.stub()};
feed.uninit();
- assert.calledWith(feed.store.dispatch, {type: at.SNIPPETS_RESET});
+ assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({type: at.SNIPPETS_RESET}));
});
it("should dispatch an update event when the Search observer is called", async () => {
const feed = new SnippetsFeed();
feed.store = {dispatch: sandbox.stub()};
sandbox.stub(feed, "getSelectedSearchEngine")
.returns(Promise.resolve(searchData));
await feed.observe(null, "browser-search-engine-modified");
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -290,16 +290,32 @@ describe("Top Sites Feed", () => {
site.tippyTopIcon = "icon.png";
site.backgroundColor = "#fff";
return site;
};
await feed.refresh(action);
assert.calledOnce(feed.store.dispatch);
assert.notCalled(feed.getScreenshot);
});
+ it("should skip getting screenshot if there is an icon of size greater than 96x96 and no tippy top", async () => {
+ sandbox.stub(feed, "getScreenshot");
+ feed.getLinksWithDefaults = () => [{
+ url: "foo.com",
+ favicon: "data:foo",
+ faviconSize: 196
+ }];
+ feed._tippyTopProvider.processSite = site => {
+ site.tippyTopIcon = null;
+ site.backgroundColor = null;
+ return site;
+ };
+ await feed.refresh(action);
+ assert.calledOnce(feed.store.dispatch);
+ assert.notCalled(feed.getScreenshot);
+ });
});
describe("getScreenshot", () => {
it("should call Screenshots.getScreenshotForURL with the right url", async () => {
const url = "foo.com";
await feed.getScreenshot(url);
assert.calledWith(fakeScreenshot.getScreenshotForURL, url);
});
});
--- a/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
@@ -1,50 +1,111 @@
const initStore = require("content-src/lib/init-store");
+const {MERGE_STORE_ACTION, rehydrationMiddleware} = initStore;
const {GlobalOverrider, addNumberReducer} = require("test/unit/utils");
-const {actionCreators: ac} = require("common/Actions.jsm");
+const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
describe("initStore", () => {
let globals;
let store;
beforeEach(() => {
globals = new GlobalOverrider();
globals.set("sendAsyncMessage", globals.sandbox.spy());
globals.set("addMessageListener", globals.sandbox.spy());
store = initStore({number: addNumberReducer});
});
afterEach(() => globals.restore());
it("should create a store with the provided reducers", () => {
assert.ok(store);
assert.property(store.getState(), "number");
});
- it("should add a listener for incoming actions", () => {
+ it("should add a listener that dispatches actions", () => {
assert.calledWith(global.addMessageListener, initStore.INCOMING_MESSAGE_NAME);
- const callback = global.addMessageListener.firstCall.args[1];
+ const listener = global.addMessageListener.firstCall.args[1];
globals.sandbox.spy(store, "dispatch");
const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
- callback(message);
+
+ listener(message);
+
assert.calledWith(store.dispatch, message.data);
});
+ it("should not throw if addMessageListener is not defined", () => {
+ // Note: this is being set/restored by GlobalOverrider
+ delete global.addMessageListener;
+
+ assert.doesNotThrow(() => initStore({number: addNumberReducer}));
+ });
+ it("should initialize with an initial state if provided as the second argument", () => {
+ store = initStore({number: addNumberReducer}, {number: 42});
+
+ assert.equal(store.getState().number, 42);
+ });
it("should log errors from failed messages", () => {
const callback = global.addMessageListener.firstCall.args[1];
globals.sandbox.stub(global.console, "error");
globals.sandbox.stub(store, "dispatch").throws(Error("failed"));
- const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
+ const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: MERGE_STORE_ACTION}};
callback(message);
assert.calledOnce(global.console.error);
});
it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => {
store.dispatch({type: initStore.MERGE_STORE_ACTION, data: {number: 42}});
assert.deepEqual(store.getState(), {number: 42});
});
- it("should send out SendToMain ations", () => {
+ it("should send out SendToMain actions", () => {
const action = ac.SendToMain({type: "FOO"});
store.dispatch(action);
assert.calledWith(global.sendAsyncMessage, initStore.OUTGOING_MESSAGE_NAME, action);
});
- it("should not send out other types of ations", () => {
+ it("should not send out other types of actions", () => {
store.dispatch({type: "FOO"});
assert.notCalled(global.sendAsyncMessage);
});
+ describe("rehydrationMiddleware", () => {
+ it("should allow NEW_TAB_STATE_REQUEST to go through", () => {
+ const action = ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST});
+ const next = sinon.spy();
+ rehydrationMiddleware(store)(next)(action);
+ assert.calledWith(next, action);
+ });
+ it("should dispatch an additional NEW_TAB_STATE_REQUEST if INIT was received after a request", () => {
+ const requestAction = ac.SendToMain({type: at.NEW_TAB_STATE_REQUEST});
+ const next = sinon.spy();
+
+ rehydrationMiddleware(store)(next)(requestAction);
+
+ next.reset();
+ rehydrationMiddleware(store)(next)({type: at.INIT});
+ assert.calledWith(next, requestAction);
+ });
+ it("should allow MERGE_STORE_ACTION to go through", () => {
+ const action = {type: MERGE_STORE_ACTION};
+ const next = sinon.spy();
+ rehydrationMiddleware(store)(next)(action);
+ assert.calledWith(next, action);
+ });
+ it("should not allow actions from main to go through before MERGE_STORE_ACTION was received", () => {
+ const next = sinon.spy();
+
+ rehydrationMiddleware(store)(next)(ac.BroadcastToContent({type: "FOO"}));
+ rehydrationMiddleware(store)(next)(ac.SendToContent({type: "FOO"}, 123));
+
+ assert.notCalled(next);
+ });
+ it("should allow all local actions to go through", () => {
+ const action = {type: "FOO"};
+ const next = sinon.spy();
+ rehydrationMiddleware(store)(next)(action);
+ assert.calledWith(next, action);
+ });
+ it("should allow actions from main to go through after MERGE_STORE_ACTION has been received", () => {
+ const next = sinon.spy();
+ rehydrationMiddleware(store)(next)({type: MERGE_STORE_ACTION});
+ next.reset();
+
+ const action = ac.SendToContent({type: "FOO"}, 123);
+ rehydrationMiddleware(store)(next)(action);
+ assert.calledWith(next, action);
+ });
+ });
});