diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js index 206cb17a757b..e45d3545bc79 100644 --- a/browser/base/content/test/static/browser_all_files_referenced.js +++ b/browser/base/content/test/static/browser_all_files_referenced.js @@ -87,6 +87,10 @@ var whitelist = [ {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"}, diff --git a/browser/components/search/test/browser_searchEngine_behaviors.js b/browser/components/search/test/browser_searchEngine_behaviors.js index cf1b7681b8a5..999c12948e8b 100644 --- a/browser/components/search/test/browser_searchEngine_behaviors.js +++ b/browser/components/search/test/browser_searchEngine_behaviors.js @@ -95,14 +95,9 @@ function promiseStateChangeURI() { 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 + ); }); } diff --git a/browser/extensions/activity-stream/common/Actions.jsm b/browser/extensions/activity-stream/common/Actions.jsm index 75f2fbb4b497..fd500f48525a 100644 --- a/browser/extensions/activity-stream/common/Actions.jsm +++ b/browser/extensions/activity-stream/common/Actions.jsm @@ -39,6 +39,7 @@ for (const type of [ "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", + "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", diff --git a/browser/extensions/activity-stream/common/PerfService.jsm b/browser/extensions/activity-stream/common/PerfService.jsm index a340fd6b57ee..4a5255169c62 100644 --- a/browser/extensions/activity-stream/common/PerfService.jsm +++ b/browser/extensions/activity-stream/common/PerfService.jsm @@ -1,26 +1,28 @@ /* globals Services */ "use strict"; -let usablePerfObj; - -let Cu; -const isRunningInChrome = typeof Window === "undefined"; - /* istanbul ignore if */ -if (isRunningInChrome) { - Cu = Components.utils; -} else { - Cu = {import() {}}; +if (typeof Components !== "undefined" && Components.utils) { + Components.utils.import("resource://gre/modules/Services.jsm"); } -Cu.import("resource://gre/modules/Services.jsm"); +let usablePerfObj; /* 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) { diff --git a/browser/extensions/activity-stream/common/PrerenderData.jsm b/browser/extensions/activity-stream/common/PrerenderData.jsm new file mode 100644 index 000000000000..6a1d327cc0d8 --- /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"]; diff --git a/browser/extensions/activity-stream/common/Reducers.jsm b/browser/extensions/activity-stream/common/Reducers.jsm index 2c1f6782ab10..a60bf90bbb23 100644 --- a/browser/extensions/activity-stream/common/Reducers.jsm +++ b/browser/extensions/activity-stream/common/Reducers.jsm @@ -5,6 +5,9 @@ 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; @@ -16,6 +19,8 @@ const INITIAL_STATE = { locale: "", // Localized strings with defaults strings: null, + // The text direction for the locale + textDirection: "", // The version of the system-addon version: null }, @@ -48,7 +53,8 @@ function App(prevState = INITIAL_STATE.App, action) { let {locale, strings} = action.data; return Object.assign({}, prevState, { locale, - strings + strings, + textDirection: RTL_LIST.indexOf(locale.split("-")[0]) >= 0 ? "rtl" : "ltr" }); } default: diff --git a/browser/extensions/activity-stream/data/content/activity-stream-initial-state.js b/browser/extensions/activity-stream/data/content/activity-stream-initial-state.js new file mode 100644 index 000000000000..750c31383c6b --- /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 + } + ] +}; diff --git a/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html b/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html new file mode 100644 index 000000000000..2c4ea5d6e0eb --- /dev/null +++ b/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html @@ -0,0 +1,25 @@ + + + + + + + + + + + +

+
+
+
+ + + + + + + + + + diff --git a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js index c0320b63f8c4..b814b7c5bba9 100644 --- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js +++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js @@ -65,12 +65,6 @@ /************************************************************************/ /******/ ([ /* 0 */ -/***/ (function(module, exports) { - -module.exports = React; - -/***/ }), -/* 1 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100,7 +94,7 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : // 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; } @@ -191,9 +185,7 @@ function UserEvent(data) { * @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 @@ -208,9 +200,7 @@ function UndesiredEvent(data) { * @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 @@ -225,9 +215,7 @@ function PerfEvent(data) { * @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 @@ -235,9 +223,7 @@ function ImpressionStats(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; } @@ -296,6 +282,12 @@ module.exports = { CONTENT_MESSAGE_TYPE }; +/***/ }), +/* 1 */ +/***/ (function(module, exports) { + +module.exports = React; + /***/ }), /* 2 */ /***/ (function(module, exports) { @@ -310,18 +302,46 @@ 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; -module.exports = { - TOP_SITES_SOURCE: "TOP_SITES", - TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"] -}; - /***/ }), /* 5 */ +/***/ (function(module, exports) { + +module.exports = { + TOP_SITES_SOURCE: "TOP_SITES", + TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"], + // minimum size necessary to show a rich icon instead of a screenshot + MIN_RICH_FAVICON_SIZE: 96, + // minimum size necessary to show any icon in the top left corner with a screenshot + MIN_CORNER_FAVICON_SIZE: 32 +}; + +/***/ }), +/* 6 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -330,10 +350,10 @@ module.exports = { * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -var _require = __webpack_require__(1); - -const at = _require.actionTypes; +const { actionTypes: at } = __webpack_require__(0); +// 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; @@ -346,6 +366,8 @@ const INITIAL_STATE = { locale: "", // Localized strings with defaults strings: null, + // The text direction for the locale + textDirection: "", // The version of the system-addon version: null }, @@ -367,10 +389,7 @@ const INITIAL_STATE = { 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 }); @@ -379,13 +398,11 @@ function App() { 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: @@ -428,10 +445,7 @@ function insertPinned(links, pinned) { 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; @@ -456,11 +470,7 @@ function TopSites() { } 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; @@ -497,10 +507,7 @@ function TopSites() { } } -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 }); @@ -513,10 +520,7 @@ function Dialog() { } } -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: @@ -530,10 +534,7 @@ function Prefs() { } } -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) { @@ -590,11 +591,7 @@ function Sections() { 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"; @@ -631,10 +628,7 @@ function Sections() { } } -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); @@ -655,34 +649,36 @@ module.exports = { }; /***/ }), -/* 6 */ +/* 7 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* globals Services */ -let usablePerfObj; - -let Cu; -const isRunningInChrome = typeof Window === "undefined"; - /* istanbul ignore if */ -if (isRunningInChrome) { - Cu = Components.utils; -} else { - Cu = { import() {} }; + +if (typeof Components !== "undefined" && Components.utils) { + Components.utils.import("resource://gre/modules/Services.jsm"); } -Cu.import("resource://gre/modules/Services.jsm"); +let usablePerfObj; /* 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) { @@ -778,47 +774,43 @@ module.exports = { }; /***/ }), -/* 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); +const React = __webpack_require__(1); +const { actionCreators: ac, actionTypes: at } = __webpack_require__(0); -var _require = __webpack_require__(1); - -const ac = _require.actionCreators, - at = _require.actionTypes; - - -const LinkMenu = __webpack_require__(8); - -var _require2 = __webpack_require__(4); - -const TOP_SITES_SOURCE = _require2.TOP_SITES_SOURCE, - TOP_SITES_CONTEXT_MENU_OPTIONS = _require2.TOP_SITES_CONTEXT_MENU_OPTIONS; +const LinkMenu = __webpack_require__(9); +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", @@ -834,7 +826,8 @@ const TopSiteLink = props => { { 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", @@ -895,8 +888,7 @@ class TopSite extends React.Component { 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, @@ -910,10 +902,7 @@ class TopSite extends React.Component { 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, @@ -932,9 +921,8 @@ class TopSite extends React.Component { 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( @@ -993,45 +981,26 @@ 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 = () => { @@ -1069,72 +1038,39 @@ class LinkMenu extends React.Component { 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 */ @@ -1146,52 +1082,42 @@ 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); @@ -1199,9 +1125,7 @@ class Base extends React.Component { } } - updateTitle(_ref3) { - let strings = _ref3.strings; - + updateTitle({ strings }) { if (strings) { document.title = strings.newtab_page_title; } @@ -1209,19 +1133,19 @@ class Base extends React.Component { 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; - 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" }, @@ -1234,7 +1158,7 @@ class Base extends React.Component { React.createElement(Sections, null), React.createElement(ConfirmDialog, null) ), - React.createElement(PreferencesPane, null) + initialized && React.createElement(PreferencesPane, null) ) ); } @@ -1246,28 +1170,13 @@ 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); @@ -1307,23 +1216,10 @@ 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 @@ -1343,7 +1239,6 @@ const perfSvc = _require3.perfService; * @class TopSitesPerfTimer * @extends {React.Component} */ - class TopSitesPerfTimer extends React.Component { constructor(props) { super(props); @@ -1440,38 +1335,15 @@ 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); +const { TopSite, TopSitePlaceholder } = __webpack_require__(8); -var _require3 = __webpack_require__(7); - -const TopSite = _require3.TopSite, - TopSitePlaceholder = _require3.TopSitePlaceholder; - -var _require4 = __webpack_require__(5); - -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) { @@ -1623,6 +1495,7 @@ class TopSitesEdit extends React.Component { 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 }) @@ -1639,24 +1512,11 @@ module.exports._unconnected = TopSitesEdit; /* 16 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; - - -const React = __webpack_require__(0); - -var _require = __webpack_require__(1); - -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 React = __webpack_require__(1); +const { actionCreators: ac, actionTypes: at } = __webpack_require__(0); +const { FormattedMessage } = __webpack_require__(2); +const { TOP_SITES_SOURCE } = __webpack_require__(5); class TopSiteForm extends React.Component { constructor(props) { @@ -1710,6 +1570,13 @@ class TopSiteForm extends React.Component { 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 } @@ -1832,10 +1699,7 @@ 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) { @@ -1885,8 +1749,7 @@ class ContextMenuItem extends React.Component { 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 @@ -1903,8 +1766,7 @@ class ContextMenuItem extends React.Component { } } render() { - const option = this.props.option; - + const { option } = this.props; return React.createElement( "li", { role: "menuitem", className: "context-menu-item" }, @@ -1926,20 +1788,13 @@ 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 => ({ @@ -2052,25 +1907,11 @@ module.exports.CheckPinTopSite = (site, index) => site.isPinned ? module.exports /* 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) { @@ -2160,42 +2001,28 @@ class Search extends React.Component { } } -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. @@ -2216,7 +2043,6 @@ const actionTypes = _require3.actionTypes, * confirm_button_string_id: "menu_action_delete" * }, */ - const ConfirmDialog = React.createClass({ displayName: "ConfirmDialog", @@ -2298,23 +2124,10 @@ 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. @@ -2324,7 +2137,6 @@ const at = _require3.actionTypes, * 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); @@ -2376,30 +2188,11 @@ 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", @@ -2447,9 +2240,7 @@ class PreferencesPane extends React.Component { } 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; @@ -2516,15 +2307,14 @@ class PreferencesPane extends React.Component { 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", @@ -2549,31 +2339,15 @@ 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); - -const injectIntl = _require2.injectIntl, - FormattedMessage = _require2.FormattedMessage; +/* 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__(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"; @@ -2618,8 +2392,7 @@ class Section extends React.Component { } _dispatchImpressionStats() { - const props = this.props; - + const { props } = this; const maxCards = 3 * props.maxRows; props.dispatch(ac.ImpressionStats({ source: props.eventSource, @@ -2632,8 +2405,7 @@ class Section extends React.Component { // 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; @@ -2666,8 +2438,7 @@ class Section extends React.Component { } componentDidUpdate(prevProps) { - const props = this.props; - + const { props } = this; if ( // Don't send impression stats for the empty state props.rows.length && @@ -2689,22 +2460,13 @@ class Section extends React.Component { } 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", @@ -2816,28 +2578,17 @@ 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. @@ -2848,7 +2599,6 @@ const ac = _require2.actionCreators, * 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); @@ -2866,12 +2616,7 @@ class Card extends React.Component { } 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 } }) @@ -2892,21 +2637,11 @@ class Card extends React.Component { 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", @@ -2983,10 +2718,7 @@ module.exports.PlaceholderCard = PlaceholderCard; /***/ }), /* 26 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - +/***/ (function(module, exports) { module.exports = { history: { @@ -3011,22 +2743,12 @@ module.exports = { /* 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, @@ -3041,10 +2763,7 @@ class Topic extends React.Component { 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" }, @@ -3058,7 +2777,7 @@ class Topics extends React.Component { 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" }) @@ -3075,21 +2794,10 @@ module.exports.Topic = Topic; /* 28 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; - - -/* eslint-env mozilla/frame-script */ - -var _require = __webpack_require__(29); - -const createStore = _require.createStore, - combineReducers = _require.combineReducers, - applyMiddleware = _require.applyMiddleware; - -var _require2 = __webpack_require__(1); - -const au = _require2.actionUtils; +/* WEBPACK VAR INJECTION */(function(global) {/* eslint-env mozilla/frame-script */ +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"; @@ -3131,30 +2839,71 @@ const messageMiddleware = store => next => 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)); - 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}`); - } - }); + store._didRehydrate = false; + store._didRequestInitialState = false; + + 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 */ @@ -3166,28 +2915,17 @@ 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); } @@ -3241,23 +2979,21 @@ module.exports = class DetectUserSessionStart { } } }; +/* 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 @@ -3267,7 +3003,6 @@ const at = _require.actionTypes, * previously cached data, if necessary. * */ - class SnippetsMap extends Map { constructor(dispatch) { super(); @@ -3533,6 +3268,14 @@ class SnippetsProvider { } 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; } } @@ -3542,19 +3285,29 @@ class SnippetsProvider { * 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; } @@ -3565,7 +3318,7 @@ module.exports = { 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 diff --git a/browser/extensions/activity-stream/data/content/activity-stream.css b/browser/extensions/activity-stream/data/content/activity-stream.css index e1252e029597..fe238faf8245 100644 --- a/browser/extensions/activity-stream/data/content/activity-stream.css +++ b/browser/extensions/activity-stream/data/content/activity-stream.css @@ -64,7 +64,9 @@ input { .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 { @@ -317,17 +319,26 @@ main { 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; @@ -1017,11 +1028,10 @@ main { 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; } diff --git a/browser/extensions/activity-stream/data/content/activity-stream.html b/browser/extensions/activity-stream/data/content/activity-stream.html index 6630864c64ba..aafd603cd7b3 100644 --- a/browser/extensions/activity-stream/data/content/activity-stream.html +++ b/browser/extensions/activity-stream/data/content/activity-stream.html @@ -1,5 +1,5 @@ - + diff --git a/browser/extensions/activity-stream/data/locales.json b/browser/extensions/activity-stream/data/locales.json index e5b670873298..d1098e9658f2 100644 --- a/browser/extensions/activity-stream/data/locales.json +++ b/browser/extensions/activity-stream/data/locales.json @@ -232,9 +232,11 @@ "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", @@ -257,6 +259,8 @@ "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", @@ -414,9 +418,11 @@ "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": "Настройки на най-посещаваните", @@ -440,6 +446,7 @@ "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": "Не, благодаря", @@ -1055,10 +1062,10 @@ "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", @@ -1077,8 +1084,8 @@ "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.", @@ -1108,8 +1115,8 @@ "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", @@ -1119,7 +1126,7 @@ "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" @@ -1763,6 +1770,7 @@ "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}", @@ -1794,6 +1802,8 @@ "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", @@ -1803,7 +1813,7 @@ "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", @@ -1813,8 +1823,12 @@ "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", @@ -1837,10 +1851,10 @@ "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" }, @@ -2363,7 +2377,7 @@ "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", @@ -3007,6 +3021,7 @@ "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", @@ -3038,6 +3053,8 @@ "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", @@ -3047,7 +3064,7 @@ "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", @@ -3057,8 +3074,12 @@ "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", @@ -3081,10 +3102,10 @@ "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" }, @@ -3123,6 +3144,7 @@ "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}", @@ -3154,6 +3176,8 @@ "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", @@ -3163,7 +3187,7 @@ "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", @@ -3173,8 +3197,12 @@ "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", @@ -3197,10 +3225,10 @@ "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" }, @@ -3488,6 +3516,7 @@ "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}", @@ -3519,6 +3548,8 @@ "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", @@ -3528,7 +3559,7 @@ "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", @@ -3538,8 +3569,12 @@ "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", @@ -3562,10 +3597,10 @@ "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" }, @@ -3628,9 +3663,11 @@ "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": "Топ сайттар санатын баптау", @@ -3653,7 +3690,10 @@ "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": "Қазір импорттау" }, @@ -4376,6 +4416,7 @@ "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.", @@ -4991,7 +5032,7 @@ "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", @@ -5371,6 +5412,8 @@ "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", @@ -5384,8 +5427,8 @@ "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", @@ -5725,6 +5768,7 @@ "default_label_loading": "వస్తోంది…", "header_top_sites": "మేటి సైట్లు", "header_stories": "ముఖ్య కథనాలు", + "header_highlights": "విశేషాలు", "header_visit_again": "మళ్లీ సందర్శించండి", "header_bookmarks": "ఇటీవలి ఇష్టాంశములు", "header_recommended_by": "{provider}చే సిఫార్సు చేయబడినది", @@ -5756,6 +5800,8 @@ "search_web_placeholder": "జాలంలో వెతకండి", "search_settings": "శోధన అమరికలు మార్చు", "section_info_option": "సమాచారం", + "section_info_send_feedback": "అభిప్రాయాన్ని పంపండి", + "section_info_privacy_notice": "గోప్యతా విధానం", "welcome_title": "కొత్త ట్యాబుకు స్వాగతం", "welcome_body": "సముచితమైన మీ ఇష్టాంశాలను, వ్యాసాలను, వీడియోలను, ఇంకా మీరు ఇటీవలే చూసిన పేజీలను మీకు తేలిగ్గా అందుబాటులో ఉంచేందుకు Firefox ఈ జాగాని వాడుకుంటుంది.", "welcome_label": "మీ ముఖ్యాంశాలను గుర్తిస్తున్నది", @@ -5765,7 +5811,7 @@ "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": "మేటి సైట్లు", @@ -5775,8 +5821,9 @@ "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": "మీ మేటి సైట్ల విభాగాన్ని మలచుకోండి", @@ -5799,10 +5846,7 @@ "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": "ఇప్పుడే దిగుమతి చేయండి" }, @@ -5865,6 +5909,7 @@ "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": "ปรับแต่งส่วนไซต์เด่นของคุณ", @@ -5887,6 +5932,7 @@ "pocket_read_more": "หัวข้อยอดนิยม:", "pocket_read_even_more": "ดูเรื่องราวเพิ่มเติม", "pocket_feedback_header": "ที่สุดของเว็บ จัดรายการโดยผู้คนกว่า 25 ล้านคน", + "manual_migration_explanation2": "ลอง Firefox ด้วยที่คั่นหน้า, ประวัติ และรหัสผ่านจากเบราว์เซอร์อื่น", "manual_migration_cancel_button": "ไม่ ขอบคุณ", "manual_migration_import_button": "นำเข้าตอนนี้" }, @@ -6294,8 +6340,8 @@ "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": "固定", diff --git a/browser/extensions/activity-stream/install.rdf.in b/browser/extensions/activity-stream/install.rdf.in index b49a47d9af44..0190ca2d90ea 100644 --- a/browser/extensions/activity-stream/install.rdf.in +++ b/browser/extensions/activity-stream/install.rdf.in @@ -8,7 +8,7 @@ 2 true false - 2017.09.08.0882-3dbf720c + 2017.09.11.1306-373d9fc Activity Stream 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. true diff --git a/browser/extensions/activity-stream/lib/ActivityStream.jsm b/browser/extensions/activity-stream/lib/ActivityStream.jsm index 442db4980079..3a32d86a6f9d 100644 --- a/browser/extensions/activity-stream/lib/ActivityStream.jsm +++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm @@ -77,6 +77,10 @@ const PREFS_CONFIG = new Map([ 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 diff --git a/browser/extensions/activity-stream/lib/HighlightsFeed.jsm b/browser/extensions/activity-stream/lib/HighlightsFeed.jsm index 139a905b2ad7..5c846dd77631 100644 --- a/browser/extensions/activity-stream/lib/HighlightsFeed.jsm +++ b/browser/extensions/activity-stream/lib/HighlightsFeed.jsm @@ -10,6 +10,7 @@ const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.j 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", @@ -17,6 +18,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", 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 { @@ -43,18 +45,39 @@ this.HighlightsFeed = class HighlightsFeed { } 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, this.highlights); - this.highlights = deduped[1]; + 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; + } + } SectionsManager.updateSection(SECTION_ID, {rows: this.highlights}, this.highlightsLastUpdated === 0 || broadcast); this.highlightsLastUpdated = Date.now(); diff --git a/browser/extensions/activity-stream/lib/NewTabInit.jsm b/browser/extensions/activity-stream/lib/NewTabInit.jsm index 850f5a4d57dd..fcf5d34a6a39 100644 --- a/browser/extensions/activity-stream/lib/NewTabInit.jsm +++ b/browser/extensions/activity-stream/lib/NewTabInit.jsm @@ -12,12 +12,30 @@ const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-str * 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; } } diff --git a/browser/extensions/activity-stream/lib/PrefsFeed.jsm b/browser/extensions/activity-stream/lib/PrefsFeed.jsm index 640bd9c231d5..fe0c74111f40 100644 --- a/browser/extensions/activity-stream/lib/PrefsFeed.jsm +++ b/browser/extensions/activity-stream/lib/PrefsFeed.jsm @@ -7,17 +7,39 @@ 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); @@ -29,6 +51,8 @@ this.PrefsFeed = class PrefsFeed { // 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); diff --git a/browser/extensions/activity-stream/lib/SectionsManager.jsm b/browser/extensions/activity-stream/lib/SectionsManager.jsm index 83eb391fba14..e28e20866b1c 100644 --- a/browser/extensions/activity-stream/lib/SectionsManager.jsm +++ b/browser/extensions/activity-stream/lib/SectionsManager.jsm @@ -48,7 +48,7 @@ const BUILT_IN_SECTIONS = { 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" diff --git a/browser/extensions/activity-stream/lib/SnippetsFeed.jsm b/browser/extensions/activity-stream/lib/SnippetsFeed.jsm index 18984da9c26e..aba53e9156a2 100644 --- a/browser/extensions/activity-stream/lib/SnippetsFeed.jsm +++ b/browser/extensions/activity-stream/lib/SnippetsFeed.jsm @@ -121,7 +121,7 @@ this.SnippetsFeed = class SnippetsFeed { 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) { diff --git a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm index fdaba48a3098..8b3105a2587b 100644 --- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm +++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm @@ -21,6 +21,7 @@ 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() { @@ -95,13 +96,13 @@ this.TopSitesFeed = class TopSitesFeed { } } - // 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]) { diff --git a/browser/extensions/activity-stream/test/unit/activity-stream-prerender.test.jsx b/browser/extensions/activity-stream/test/unit/activity-stream-prerender.test.jsx new file mode 100644 index 000000000000..3bc399e5dd6e --- /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"); + }); +}); diff --git a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js index 273db903503c..1f100344cb5d 100644 --- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js +++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js @@ -26,12 +26,20 @@ describe("Reducers", () => { 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", () => { diff --git a/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js b/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js index d20aff9793e9..cb0e6156e53c 100644 --- a/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js +++ b/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js @@ -30,7 +30,7 @@ describe("Top Sites Feed", () => { 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}, @@ -72,7 +72,7 @@ describe("Top Sites Feed", () => { 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 () => { @@ -86,6 +86,30 @@ describe("Top Sites Feed", () => { 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(); diff --git a/browser/extensions/activity-stream/test/unit/lib/NewTabInit.test.js b/browser/extensions/activity-stream/test/unit/lib/NewTabInit.test.js new file mode 100644 index 000000000000..ec0531d3316f --- /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); + }); +}); diff --git a/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js b/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js index d47a7477e392..eff02248a333 100644 --- a/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js +++ b/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js @@ -1,16 +1,20 @@ 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(), @@ -41,4 +45,42 @@ describe("PrefsFeed", () => { 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); + }); + }); }); diff --git a/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js b/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js index 6d1887775a10..16c302135970 100644 --- a/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js +++ b/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js @@ -1,5 +1,5 @@ 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; @@ -78,13 +78,13 @@ describe("SnippetsFeed", () => { 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(); diff --git a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js index 329520811967..05256a890e34 100644 --- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js +++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js @@ -295,6 +295,22 @@ describe("Top Sites Feed", () => { 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 () => { diff --git a/browser/extensions/activity-stream/test/unit/lib/init-store.test.js b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js index 4f01e6718e22..f887619fe948 100644 --- 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,6 +1,7 @@ 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; @@ -16,20 +17,33 @@ describe("initStore", () => { 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); @@ -38,13 +52,60 @@ describe("initStore", () => { 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); + }); + }); });