summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/data/content
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/data/content')
-rw-r--r--browser/components/newtab/data/content/abouthomecache/page.html.template46
-rw-r--r--browser/components/newtab/data/content/abouthomecache/script.js.template19
-rw-r--r--browser/components/newtab/data/content/activity-stream.bundle.js9558
-rw-r--r--browser/components/newtab/data/content/assets/firefox.svg168
-rw-r--r--browser/components/newtab/data/content/assets/glyph-cfr-feature-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/glyph-mail-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/glyph-maximize-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/glyph-minimize-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/glyph-modal-delete-20.svg8
-rw-r--r--browser/components/newtab/data/content/assets/glyph-newWindow-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/glyph-open-file-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/glyph-pin-16.svg6
-rw-r--r--browser/components/newtab/data/content/assets/glyph-pocket-archive-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/glyph-pocket-delete-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/glyph-unpin-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/glyph-webextension-16.svg4
-rw-r--r--browser/components/newtab/data/content/assets/icon-removed-bookmark.svg4
-rw-r--r--browser/components/newtab/data/content/assets/pocket-onboarding.avifbin0 -> 7462 bytes
-rw-r--r--browser/components/newtab/data/content/assets/pocket-onboarding@2x.avifbin0 -> 18590 bytes
-rw-r--r--browser/components/newtab/data/content/assets/pocket-swoosh.svg11
-rw-r--r--browser/components/newtab/data/content/assets/remote/mountain.svg12
-rw-r--r--browser/components/newtab/data/content/assets/remote/umbrella.pngbin0 -> 4292 bytes
-rw-r--r--browser/components/newtab/data/content/assets/spinner.svg4
-rw-r--r--browser/components/newtab/data/content/newtab-render.js11
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/adidas.pngbin0 -> 3226 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/aliexpress-com.icobin0 -> 4286 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/allegro-pl.icobin0 -> 1150 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/amazon.icobin0 -> 1407 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/avito-ru.icobin0 -> 5430 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/baidu-com.pngbin0 -> 1983 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/bbc-uk.icobin0 -> 958 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/bing-com.icobin0 -> 4286 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/ctrip-com.icobin0 -> 1150 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/duckduckgo-com.icobin0 -> 2799 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/ebay.icobin0 -> 1455 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/etsy.icobin0 -> 4286 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/facebook-com.icobin0 -> 5430 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/geico.pngbin0 -> 1472 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/google-com.icobin0 -> 5430 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/hrblock.icobin0 -> 3950 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/ifeng-com.icobin0 -> 4038 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/iqiyi-com.icobin0 -> 5430 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/leboncoin-fr.pngbin0 -> 454 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/nike.icobin0 -> 1150 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/ok-ru.icobin0 -> 5430 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/olx-pl.icobin0 -> 5430 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/reddit-com.pngbin0 -> 2094 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/samsung.icobin0 -> 4286 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/turbotax.pngbin0 -> 3744 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/twitter-com.icobin0 -> 1650 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/vk-com.icobin0 -> 302 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/vodafone.pngbin0 -> 1757 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/weibo-com.icobin0 -> 10134 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/wikipedia-org.icobin0 -> 2734 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/wix.icobin0 -> 1061 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/wykop-pl.pngbin0 -> 1705 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/yandex-com.pngbin0 -> 1338 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/yandex-ru.pngbin0 -> 1368 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/youtube-com.pngbin0 -> 348 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/favicons/zhihu-com.icobin0 -> 6518 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/adidas@2x.pngbin0 -> 5448 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/aliexpress-com@2x.pngbin0 -> 12459 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/allegro-pl@2x.pngbin0 -> 5041 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/amazon@2x.pngbin0 -> 6061 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/avito-ru@2x.pngbin0 -> 1568 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/baidu-com@2x.pngbin0 -> 8198 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/bbc-uk@2x.pngbin0 -> 18207 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/bing-com@2x.svg106
-rw-r--r--browser/components/newtab/data/content/tippytop/images/ctrip-com@2x.pngbin0 -> 15862 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/duckduckgo-com@2x.svg12
-rw-r--r--browser/components/newtab/data/content/tippytop/images/ebay@2x.pngbin0 -> 5361 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/etsy@2x.jpgbin0 -> 4094 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/facebook-com@2x.pngbin0 -> 10780 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/geico@2x.jpgbin0 -> 11834 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/google-com@2x.pngbin0 -> 3035 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/hrblock@2x.pngbin0 -> 4642 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/ifeng-com@2x.pngbin0 -> 22282 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/iqiyi-com@2x.pngbin0 -> 14340 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/leboncoin-fr@2x.pngbin0 -> 7146 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/nike@2x.jpgbin0 -> 5163 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/ok-ru@2x.pngbin0 -> 2526 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/olx-pl@2x.pngbin0 -> 5287 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/reddit-com@2x.pngbin0 -> 5180 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/samsung@2x.jpgbin0 -> 3347 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/turbotax@2x.jpgbin0 -> 11930 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/twitter-com@2x.pngbin0 -> 1260 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/vk-com@2x.pngbin0 -> 9897 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/vodafone@2x.jpgbin0 -> 7050 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/weibo-com@2x.pngbin0 -> 15507 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/wikipedia-org@2x.pngbin0 -> 19001 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/wix@2x.jpgbin0 -> 8714 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/wykop-pl@2x.pngbin0 -> 4415 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/yandex-com@2x.pngbin0 -> 6516 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/yandex-ru@2x.pngbin0 -> 6638 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/youtube-com@2x.pngbin0 -> 2924 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/images/zhihu-com@2x.pngbin0 -> 10225 bytes
-rw-r--r--browser/components/newtab/data/content/tippytop/top_sites.json182
97 files changed, 10187 insertions, 0 deletions
diff --git a/browser/components/newtab/data/content/abouthomecache/page.html.template b/browser/components/newtab/data/content/abouthomecache/page.html.template
new file mode 100644
index 0000000000..60898ed6b8
--- /dev/null
+++ b/browser/components/newtab/data/content/abouthomecache/page.html.template
@@ -0,0 +1,46 @@
+#if 0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This template file is used to construct the cached about:home document.
+# The following template strings are used:
+#
+# {{ CACHE_TIME }}:
+# A date string representing when the cache was generated.
+#
+# {{ MARKUP }}:
+# The generated DOM content from ReactDOMServer for the cache.
+#
+# Also note the final script load of about:home?jscache. This loads the cached
+# script, which does the important work of telling React how to connect the
+# cached page state to the pre-existing DOM that's being rendered.
+#
+#endif
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob: chrome:; style-src 'unsafe-inline';">
+ <meta name="color-scheme" content="light dark">
+ <title data-l10n-id="newtab-page-title"></title>
+ <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="toolkit/branding/brandings.ftl" />
+ <link rel="localization" href="browser/newtab/newtab.ftl" />
+ <link rel="stylesheet" href="chrome://global/skin/design-system/tokens-brand.css">
+ <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
+ <link rel="stylesheet" href="chrome://activity-stream/content/css/activity-stream.css" />
+ </head>
+ <!-- Cached: {{ CACHE_TIME }} -->
+ <body class="activity-stream">
+ <div id="header-asrouter-container" role="presentation"></div>
+ <div id="root">
+ {{ MARKUP }}
+ </div>
+ <div id="footer-asrouter-container" role="presentation"></div>
+ <script src="about:home?jscache"></script>
+ <script async type="module" src="chrome://global/content/elements/moz-toggle.mjs"></script>
+ </body>
+</html>
diff --git a/browser/components/newtab/data/content/abouthomecache/script.js.template b/browser/components/newtab/data/content/abouthomecache/script.js.template
new file mode 100644
index 0000000000..5ba70ea7f5
--- /dev/null
+++ b/browser/components/newtab/data/content/abouthomecache/script.js.template
@@ -0,0 +1,19 @@
+#if 0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This template file is used to construct the script that is loaded by the
+# cached about:home document. It is loaded in the cached about:home document
+# by loading about:home?jscache.
+#
+# The only template string used in this file is {{ STATE }}, which can only
+# be used once. {{ STATE }} will be replaced with the state of about:home
+# at the time that the cache was generated, which is needed by React in order
+# to make the cached document interactive.
+#
+#endif
+window.__FROM_STARTUP_CACHE__ = true;
+window.__STARTUP_STATE__ = {{ STATE }};
+
diff --git a/browser/components/newtab/data/content/activity-stream.bundle.js b/browser/components/newtab/data/content/activity-stream.bundle.js
new file mode 100644
index 0000000000..14691a95f4
--- /dev/null
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -0,0 +1,9558 @@
+/*! THIS FILE IS AUTO-GENERATED: webpack.system-addon.config.js */
+var NewtabRenderUtils;
+/******/ (() => { // webpackBootstrap
+/******/ "use strict";
+/******/ // The require scope
+/******/ var __webpack_require__ = {};
+/******/
+/************************************************************************/
+/******/ /* webpack/runtime/compat get default export */
+/******/ (() => {
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = (module) => {
+/******/ var getter = module && module.__esModule ?
+/******/ () => (module['default']) :
+/******/ () => (module);
+/******/ __webpack_require__.d(getter, { a: getter });
+/******/ return getter;
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/define property getters */
+/******/ (() => {
+/******/ // define getter functions for harmony exports
+/******/ __webpack_require__.d = (exports, definition) => {
+/******/ for(var key in definition) {
+/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ }
+/******/ }
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/global */
+/******/ (() => {
+/******/ __webpack_require__.g = (function() {
+/******/ if (typeof globalThis === 'object') return globalThis;
+/******/ try {
+/******/ return this || new Function('return this')();
+/******/ } catch (e) {
+/******/ if (typeof window === 'object') return window;
+/******/ }
+/******/ })();
+/******/ })();
+/******/
+/******/ /* webpack/runtime/hasOwnProperty shorthand */
+/******/ (() => {
+/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ })();
+/******/
+/******/ /* webpack/runtime/make namespace object */
+/******/ (() => {
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = (exports) => {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/ })();
+/******/
+/************************************************************************/
+var __webpack_exports__ = {};
+// ESM COMPAT FLAG
+__webpack_require__.r(__webpack_exports__);
+
+// EXPORTS
+__webpack_require__.d(__webpack_exports__, {
+ NewTab: () => (/* binding */ NewTab),
+ renderCache: () => (/* binding */ renderCache),
+ renderWithoutState: () => (/* binding */ renderWithoutState)
+});
+
+;// CONCATENATED MODULE: ./common/Actions.sys.mjs
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
+const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser";
+const UI_CODE = 1;
+const BACKGROUND_PROCESS = 2;
+
+/**
+ * globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process?
+ * Use this in action creators if you need different logic
+ * for ui/background processes.
+ */
+const globalImportContext =
+ typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
+
+// Create an object that avoids accidental differing key/value pairs:
+// {
+// INIT: "INIT",
+// UNINIT: "UNINIT"
+// }
+const actionTypes = {};
+
+for (const type of [
+ "ABOUT_SPONSORED_TOP_SITES",
+ "ADDONS_INFO_REQUEST",
+ "ADDONS_INFO_RESPONSE",
+ "ARCHIVE_FROM_POCKET",
+ "AS_ROUTER_INITIALIZED",
+ "AS_ROUTER_PREF_CHANGED",
+ "AS_ROUTER_TARGETING_UPDATE",
+ "AS_ROUTER_TELEMETRY_USER_EVENT",
+ "BLOCK_URL",
+ "BOOKMARK_URL",
+ "CLEAR_PREF",
+ "COPY_DOWNLOAD_LINK",
+ "DELETE_BOOKMARK_BY_ID",
+ "DELETE_FROM_POCKET",
+ "DELETE_HISTORY_URL",
+ "DIALOG_CANCEL",
+ "DIALOG_OPEN",
+ "DISABLE_SEARCH",
+ "DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE",
+ "DISCOVERY_STREAM_CONFIG_CHANGE",
+ "DISCOVERY_STREAM_CONFIG_RESET",
+ "DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS",
+ "DISCOVERY_STREAM_CONFIG_SETUP",
+ "DISCOVERY_STREAM_CONFIG_SET_VALUE",
+ "DISCOVERY_STREAM_DEV_EXPIRE_CACHE",
+ "DISCOVERY_STREAM_DEV_IDLE_DAILY",
+ "DISCOVERY_STREAM_DEV_SYNC_RS",
+ "DISCOVERY_STREAM_DEV_SYSTEM_TICK",
+ "DISCOVERY_STREAM_EXPERIMENT_DATA",
+ "DISCOVERY_STREAM_FEEDS_UPDATE",
+ "DISCOVERY_STREAM_FEED_UPDATE",
+ "DISCOVERY_STREAM_IMPRESSION_STATS",
+ "DISCOVERY_STREAM_LAYOUT_RESET",
+ "DISCOVERY_STREAM_LAYOUT_UPDATE",
+ "DISCOVERY_STREAM_LINK_BLOCKED",
+ "DISCOVERY_STREAM_LOADED_CONTENT",
+ "DISCOVERY_STREAM_PERSONALIZATION_INIT",
+ "DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED",
+ "DISCOVERY_STREAM_PERSONALIZATION_OVERRIDE",
+ "DISCOVERY_STREAM_PERSONALIZATION_RESET",
+ "DISCOVERY_STREAM_PERSONALIZATION_TOGGLE",
+ "DISCOVERY_STREAM_PERSONALIZATION_UPDATED",
+ "DISCOVERY_STREAM_POCKET_STATE_INIT",
+ "DISCOVERY_STREAM_POCKET_STATE_SET",
+ "DISCOVERY_STREAM_PREFS_SETUP",
+ "DISCOVERY_STREAM_RECENT_SAVES",
+ "DISCOVERY_STREAM_RETRY_FEED",
+ "DISCOVERY_STREAM_SPOCS_CAPS",
+ "DISCOVERY_STREAM_SPOCS_ENDPOINT",
+ "DISCOVERY_STREAM_SPOCS_PLACEMENTS",
+ "DISCOVERY_STREAM_SPOCS_UPDATE",
+ "DISCOVERY_STREAM_SPOC_BLOCKED",
+ "DISCOVERY_STREAM_SPOC_IMPRESSION",
+ "DISCOVERY_STREAM_USER_EVENT",
+ "DOWNLOAD_CHANGED",
+ "FAKE_FOCUS_SEARCH",
+ "FILL_SEARCH_TERM",
+ "HANDOFF_SEARCH_TO_AWESOMEBAR",
+ "HIDE_PERSONALIZE",
+ "HIDE_PRIVACY_INFO",
+ "INIT",
+ "NEW_TAB_INIT",
+ "NEW_TAB_INITIAL_STATE",
+ "NEW_TAB_LOAD",
+ "NEW_TAB_REHYDRATED",
+ "NEW_TAB_STATE_REQUEST",
+ "NEW_TAB_UNLOAD",
+ "OPEN_DOWNLOAD_FILE",
+ "OPEN_LINK",
+ "OPEN_NEW_WINDOW",
+ "OPEN_PRIVATE_WINDOW",
+ "OPEN_WEBEXT_SETTINGS",
+ "PARTNER_LINK_ATTRIBUTION",
+ "PLACES_BOOKMARKS_REMOVED",
+ "PLACES_BOOKMARK_ADDED",
+ "PLACES_HISTORY_CLEARED",
+ "PLACES_LINKS_CHANGED",
+ "PLACES_LINKS_DELETED",
+ "PLACES_LINK_BLOCKED",
+ "PLACES_SAVED_TO_POCKET",
+ "POCKET_CTA",
+ "POCKET_LINK_DELETED_OR_ARCHIVED",
+ "POCKET_LOGGED_IN",
+ "POCKET_WAITING_FOR_SPOC",
+ "PREFS_INITIAL_VALUES",
+ "PREF_CHANGED",
+ "PREVIEW_REQUEST",
+ "PREVIEW_REQUEST_CANCEL",
+ "PREVIEW_RESPONSE",
+ "REMOVE_DOWNLOAD_FILE",
+ "RICH_ICON_MISSING",
+ "SAVE_SESSION_PERF_DATA",
+ "SAVE_TO_POCKET",
+ "SCREENSHOT_UPDATED",
+ "SECTION_DEREGISTER",
+ "SECTION_DISABLE",
+ "SECTION_ENABLE",
+ "SECTION_MOVE",
+ "SECTION_OPTIONS_CHANGED",
+ "SECTION_REGISTER",
+ "SECTION_UPDATE",
+ "SECTION_UPDATE_CARD",
+ "SETTINGS_CLOSE",
+ "SETTINGS_OPEN",
+ "SET_PREF",
+ "SHOW_DOWNLOAD_FILE",
+ "SHOW_FIREFOX_ACCOUNTS",
+ "SHOW_PERSONALIZE",
+ "SHOW_PRIVACY_INFO",
+ "SHOW_SEARCH",
+ "SKIPPED_SIGNIN",
+ "SOV_UPDATED",
+ "SUBMIT_EMAIL",
+ "SUBMIT_SIGNIN",
+ "SYSTEM_TICK",
+ "TELEMETRY_IMPRESSION_STATS",
+ "TELEMETRY_USER_EVENT",
+ "TOP_SITES_CANCEL_EDIT",
+ "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL",
+ "TOP_SITES_EDIT",
+ "TOP_SITES_INSERT",
+ "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL",
+ "TOP_SITES_ORGANIC_IMPRESSION_STATS",
+ "TOP_SITES_PIN",
+ "TOP_SITES_PREFS_UPDATED",
+ "TOP_SITES_SPONSORED_IMPRESSION_STATS",
+ "TOP_SITES_UNPIN",
+ "TOP_SITES_UPDATED",
+ "TOTAL_BOOKMARKS_REQUEST",
+ "TOTAL_BOOKMARKS_RESPONSE",
+ "UNINIT",
+ "UPDATE_PINNED_SEARCH_SHORTCUTS",
+ "UPDATE_SEARCH_SHORTCUTS",
+ "UPDATE_SECTION_PREFS",
+ "WEBEXT_CLICK",
+ "WEBEXT_DISMISS",
+]) {
+ actionTypes[type] = type;
+}
+
+// Helper function for creating routed actions between content and main
+// Not intended to be used by consumers
+function _RouteMessage(action, options) {
+ const meta = action.meta ? { ...action.meta } : {};
+ if (!options || !options.from || !options.to) {
+ throw new Error(
+ "Routed Messages must have options as the second parameter, and must at least include a .from and .to property."
+ );
+ }
+ // For each of these fields, if they are passed as an option,
+ // add them to the action. If they are not defined, remove them.
+ ["from", "to", "toTarget", "fromTarget", "skipMain", "skipLocal"].forEach(
+ o => {
+ if (typeof options[o] !== "undefined") {
+ meta[o] = options[o];
+ } else if (meta[o]) {
+ delete meta[o];
+ }
+ }
+ );
+ return { ...action, meta };
+}
+
+/**
+ * AlsoToMain - Creates a message that will be dispatched locally and also sent to the Main process.
+ *
+ * @param {object} action Any redux action (required)
+ * @param {object} options
+ * @param {bool} skipLocal Used by OnlyToMain to skip the main reducer
+ * @param {string} fromTarget The id of the content port from which the action originated. (optional)
+ * @return {object} An action with added .meta properties
+ */
+function AlsoToMain(action, fromTarget, skipLocal) {
+ return _RouteMessage(action, {
+ from: CONTENT_MESSAGE_TYPE,
+ to: MAIN_MESSAGE_TYPE,
+ fromTarget,
+ skipLocal,
+ });
+}
+
+/**
+ * OnlyToMain - Creates a message that will be sent to the Main process and skip the local reducer.
+ *
+ * @param {object} action Any redux action (required)
+ * @param {object} options
+ * @param {string} fromTarget The id of the content port from which the action originated. (optional)
+ * @return {object} An action with added .meta properties
+ */
+function OnlyToMain(action, fromTarget) {
+ return AlsoToMain(action, fromTarget, true);
+}
+
+/**
+ * BroadcastToContent - Creates a message that will be dispatched to main and sent to ALL content processes.
+ *
+ * @param {object} action Any redux action (required)
+ * @return {object} An action with added .meta properties
+ */
+function BroadcastToContent(action) {
+ return _RouteMessage(action, {
+ from: MAIN_MESSAGE_TYPE,
+ to: CONTENT_MESSAGE_TYPE,
+ });
+}
+
+/**
+ * AlsoToOneContent - Creates a message that will be will be dispatched to the main store
+ * and also sent to a particular Content process.
+ *
+ * @param {object} action Any redux action (required)
+ * @param {string} target The id of a content port
+ * @param {bool} skipMain Used by OnlyToOneContent to skip the main process
+ * @return {object} An action with added .meta properties
+ */
+function AlsoToOneContent(action, target, skipMain) {
+ if (!target) {
+ throw new Error(
+ "You must provide a target ID as the second parameter of AlsoToOneContent. If you want to send to all content processes, use BroadcastToContent"
+ );
+ }
+ return _RouteMessage(action, {
+ from: MAIN_MESSAGE_TYPE,
+ to: CONTENT_MESSAGE_TYPE,
+ toTarget: target,
+ skipMain,
+ });
+}
+
+/**
+ * OnlyToOneContent - Creates a message that will be sent to a particular Content process
+ * and skip the main reducer.
+ *
+ * @param {object} action Any redux action (required)
+ * @param {string} target The id of a content port
+ * @return {object} An action with added .meta properties
+ */
+function OnlyToOneContent(action, target) {
+ return AlsoToOneContent(action, target, true);
+}
+
+/**
+ * AlsoToPreloaded - Creates a message that dispatched to the main reducer and also sent to the preloaded tab.
+ *
+ * @param {object} action Any redux action (required)
+ * @return {object} An action with added .meta properties
+ */
+function AlsoToPreloaded(action) {
+ return _RouteMessage(action, {
+ from: MAIN_MESSAGE_TYPE,
+ to: PRELOAD_MESSAGE_TYPE,
+ });
+}
+
+/**
+ * UserEvent - A telemetry ping indicating a user action. This should only
+ * be sent from the UI during a user session.
+ *
+ * @param {object} data Fields to include in the ping (source, etc.)
+ * @return {object} An AlsoToMain action
+ */
+function UserEvent(data) {
+ return AlsoToMain({
+ type: actionTypes.TELEMETRY_USER_EVENT,
+ data,
+ });
+}
+
+/**
+ * DiscoveryStreamUserEvent - A telemetry ping indicating a user action from Discovery Stream. This should only
+ * be sent from the UI during a user session.
+ *
+ * @param {object} data Fields to include in the ping (source, etc.)
+ * @return {object} An AlsoToMain action
+ */
+function DiscoveryStreamUserEvent(data) {
+ return AlsoToMain({
+ type: actionTypes.DISCOVERY_STREAM_USER_EVENT,
+ data,
+ });
+}
+
+/**
+ * ASRouterUserEvent - A telemetry ping indicating a user action from AS router. This should only
+ * be sent from the UI during a user session.
+ *
+ * @param {object} data Fields to include in the ping (source, etc.)
+ * @return {object} An AlsoToMain action
+ */
+function ASRouterUserEvent(data) {
+ return AlsoToMain({
+ type: actionTypes.AS_ROUTER_TELEMETRY_USER_EVENT,
+ data,
+ });
+}
+
+/**
+ * ImpressionStats - A telemetry ping indicating an impression stats.
+ *
+ * @param {object} data Fields to include in the ping
+ * @param {int} importContext (For testing) Override the import context for testing.
+ * #return {object} An action. For UI code, a AlsoToMain action.
+ */
+function ImpressionStats(data, importContext = globalImportContext) {
+ const action = {
+ type: actionTypes.TELEMETRY_IMPRESSION_STATS,
+ data,
+ };
+ return importContext === UI_CODE ? AlsoToMain(action) : action;
+}
+
+/**
+ * DiscoveryStreamImpressionStats - A telemetry ping indicating an impression stats in Discovery Stream.
+ *
+ * @param {object} data Fields to include in the ping
+ * @param {int} importContext (For testing) Override the import context for testing.
+ * #return {object} An action. For UI code, a AlsoToMain action.
+ */
+function DiscoveryStreamImpressionStats(
+ data,
+ importContext = globalImportContext
+) {
+ const action = {
+ type: actionTypes.DISCOVERY_STREAM_IMPRESSION_STATS,
+ data,
+ };
+ return importContext === UI_CODE ? AlsoToMain(action) : action;
+}
+
+/**
+ * DiscoveryStreamLoadedContent - A telemetry ping indicating a content gets loaded in Discovery Stream.
+ *
+ * @param {object} data Fields to include in the ping
+ * @param {int} importContext (For testing) Override the import context for testing.
+ * #return {object} An action. For UI code, a AlsoToMain action.
+ */
+function DiscoveryStreamLoadedContent(
+ data,
+ importContext = globalImportContext
+) {
+ const action = {
+ type: actionTypes.DISCOVERY_STREAM_LOADED_CONTENT,
+ data,
+ };
+ return importContext === UI_CODE ? AlsoToMain(action) : action;
+}
+
+function SetPref(name, value, importContext = globalImportContext) {
+ const action = { type: actionTypes.SET_PREF, data: { name, value } };
+ return importContext === UI_CODE ? AlsoToMain(action) : action;
+}
+
+function WebExtEvent(type, data, importContext = globalImportContext) {
+ if (!data || !data.source) {
+ throw new Error(
+ 'WebExtEvent actions should include a property "source", the id of the webextension that should receive the event.'
+ );
+ }
+ const action = { type, data };
+ return importContext === UI_CODE ? AlsoToMain(action) : action;
+}
+
+const actionCreators = {
+ BroadcastToContent,
+ UserEvent,
+ DiscoveryStreamUserEvent,
+ ASRouterUserEvent,
+ ImpressionStats,
+ AlsoToOneContent,
+ OnlyToOneContent,
+ AlsoToMain,
+ OnlyToMain,
+ AlsoToPreloaded,
+ SetPref,
+ WebExtEvent,
+ DiscoveryStreamImpressionStats,
+ DiscoveryStreamLoadedContent,
+};
+
+// These are helpers to test for certain kinds of actions
+const actionUtils = {
+ isSendToMain(action) {
+ if (!action.meta) {
+ return false;
+ }
+ return (
+ action.meta.to === MAIN_MESSAGE_TYPE &&
+ action.meta.from === CONTENT_MESSAGE_TYPE
+ );
+ },
+ isBroadcastToContent(action) {
+ if (!action.meta) {
+ return false;
+ }
+ if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) {
+ return true;
+ }
+ return false;
+ },
+ isSendToOneContent(action) {
+ if (!action.meta) {
+ return false;
+ }
+ if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) {
+ return true;
+ }
+ return false;
+ },
+ isSendToPreloaded(action) {
+ if (!action.meta) {
+ return false;
+ }
+ return (
+ action.meta.to === PRELOAD_MESSAGE_TYPE &&
+ action.meta.from === MAIN_MESSAGE_TYPE
+ );
+ },
+ isFromMain(action) {
+ if (!action.meta) {
+ return false;
+ }
+ return (
+ action.meta.from === MAIN_MESSAGE_TYPE &&
+ action.meta.to === CONTENT_MESSAGE_TYPE
+ );
+ },
+ getPortIdOfSender(action) {
+ return (action.meta && action.meta.fromTarget) || null;
+ },
+ _RouteMessage,
+};
+
+;// CONCATENATED MODULE: external "ReactRedux"
+const external_ReactRedux_namespaceObject = ReactRedux;
+;// CONCATENATED MODULE: external "React"
+const external_React_namespaceObject = React;
+var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_namespaceObject);
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class SimpleHashRouter extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onHashChange = this.onHashChange.bind(this);
+ this.state = {
+ hash: __webpack_require__.g.location.hash
+ };
+ }
+ onHashChange() {
+ this.setState({
+ hash: __webpack_require__.g.location.hash
+ });
+ }
+ componentWillMount() {
+ __webpack_require__.g.addEventListener("hashchange", this.onHashChange);
+ }
+ componentWillUnmount() {
+ __webpack_require__.g.removeEventListener("hashchange", this.onHashChange);
+ }
+ render() {
+ const [, ...routes] = this.state.hash.split("-");
+ return /*#__PURE__*/external_React_default().cloneElement(this.props.children, {
+ location: {
+ hash: this.state.hash,
+ routes
+ }
+ });
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx
+function _extends() { _extends = Object.assign ? Object.assign.bind() : 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; }; return _extends.apply(this, arguments); }
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+const Row = props => /*#__PURE__*/external_React_default().createElement("tr", _extends({
+ className: "message-item"
+}, props), props.children);
+function relativeTime(timestamp) {
+ if (!timestamp) {
+ return "";
+ }
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
+ const minutes = Math.floor((Date.now() - timestamp) / 60000);
+ if (seconds < 2) {
+ return "just now";
+ } else if (seconds < 60) {
+ return `${seconds} seconds ago`;
+ } else if (minutes === 1) {
+ return "1 minute ago";
+ } else if (minutes < 600) {
+ return `${minutes} minutes ago`;
+ }
+ return new Date(timestamp).toLocaleString();
+}
+class ToggleStoryButton extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.handleClick = this.handleClick.bind(this);
+ }
+ handleClick() {
+ this.props.onClick(this.props.story);
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("button", {
+ onClick: this.handleClick
+ }, "collapse/open");
+ }
+}
+class TogglePrefCheckbox extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onChange = this.onChange.bind(this);
+ }
+ onChange(event) {
+ this.props.onChange(this.props.pref, event.target.checked);
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("input", {
+ type: "checkbox",
+ checked: this.props.checked,
+ onChange: this.onChange,
+ disabled: this.props.disabled
+ }), " ", this.props.pref, " ");
+ }
+}
+class Personalization extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.togglePersonalization = this.togglePersonalization.bind(this);
+ }
+ togglePersonalization() {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.DISCOVERY_STREAM_PERSONALIZATION_TOGGLE
+ }));
+ }
+ render() {
+ const {
+ lastUpdated,
+ initialized
+ } = this.props.state.Personalization;
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
+ colSpan: "2"
+ }, /*#__PURE__*/external_React_default().createElement(TogglePrefCheckbox, {
+ checked: this.props.personalized,
+ pref: "personalized",
+ onChange: this.togglePersonalization
+ }))), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "min"
+ }, "Personalization Last Updated"), /*#__PURE__*/external_React_default().createElement("td", null, relativeTime(lastUpdated) || "(no data)")), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "min"
+ }, "Personalization Initialized"), /*#__PURE__*/external_React_default().createElement("td", null, initialized ? "true" : "false")))));
+ }
+}
+class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.restorePrefDefaults = this.restorePrefDefaults.bind(this);
+ this.setConfigValue = this.setConfigValue.bind(this);
+ this.expireCache = this.expireCache.bind(this);
+ this.refreshCache = this.refreshCache.bind(this);
+ this.idleDaily = this.idleDaily.bind(this);
+ this.systemTick = this.systemTick.bind(this);
+ this.syncRemoteSettings = this.syncRemoteSettings.bind(this);
+ this.onStoryToggle = this.onStoryToggle.bind(this);
+ this.state = {
+ toggledStories: {}
+ };
+ }
+ setConfigValue(name, value) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.DISCOVERY_STREAM_CONFIG_SET_VALUE,
+ data: {
+ name,
+ value
+ }
+ }));
+ }
+ restorePrefDefaults(event) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS
+ }));
+ }
+ refreshCache() {
+ const {
+ config
+ } = this.props.state.DiscoveryStream;
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.DISCOVERY_STREAM_CONFIG_CHANGE,
+ data: config
+ }));
+ }
+ dispatchSimpleAction(type) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type
+ }));
+ }
+ systemTick() {
+ this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_SYSTEM_TICK);
+ }
+ expireCache() {
+ this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_EXPIRE_CACHE);
+ }
+ idleDaily() {
+ this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_IDLE_DAILY);
+ }
+ syncRemoteSettings() {
+ this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_SYNC_RS);
+ }
+ renderComponent(width, component) {
+ return /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "min"
+ }, "Type"), /*#__PURE__*/external_React_default().createElement("td", null, component.type)), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "min"
+ }, "Width"), /*#__PURE__*/external_React_default().createElement("td", null, width)), component.feed && this.renderFeed(component.feed)));
+ }
+ renderFeedData(url) {
+ const {
+ feeds
+ } = this.props.state.DiscoveryStream;
+ const feed = feeds.data[url].data;
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("h4", null, "Feed url: ", url), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, feed.recommendations?.map(story => this.renderStoryData(story)))));
+ }
+ renderFeedsData() {
+ const {
+ feeds
+ } = this.props.state.DiscoveryStream;
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, Object.keys(feeds.data).map(url => this.renderFeedData(url)));
+ }
+ renderSpocs() {
+ const {
+ spocs
+ } = this.props.state.DiscoveryStream;
+ let spocsData = [];
+ if (spocs.data && spocs.data.spocs && spocs.data.spocs.items) {
+ spocsData = spocs.data.spocs.items || [];
+ }
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "min"
+ }, "spocs_endpoint"), /*#__PURE__*/external_React_default().createElement("td", null, spocs.spocs_endpoint)), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "min"
+ }, "Data last fetched"), /*#__PURE__*/external_React_default().createElement("td", null, relativeTime(spocs.lastUpdated))))), /*#__PURE__*/external_React_default().createElement("h4", null, "Spoc data"), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, spocsData.map(spoc => this.renderStoryData(spoc)))), /*#__PURE__*/external_React_default().createElement("h4", null, "Spoc frequency caps"), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, spocs.frequency_caps.map(spoc => this.renderStoryData(spoc)))));
+ }
+ onStoryToggle(story) {
+ const {
+ toggledStories
+ } = this.state;
+ this.setState({
+ toggledStories: {
+ ...toggledStories,
+ [story.id]: !toggledStories[story.id]
+ }
+ });
+ }
+ renderStoryData(story) {
+ let storyData = "";
+ if (this.state.toggledStories[story.id]) {
+ storyData = JSON.stringify(story, null, 2);
+ }
+ return /*#__PURE__*/external_React_default().createElement("tr", {
+ className: "message-item",
+ key: story.id
+ }, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "message-id"
+ }, /*#__PURE__*/external_React_default().createElement("span", null, story.id, " ", /*#__PURE__*/external_React_default().createElement("br", null)), /*#__PURE__*/external_React_default().createElement(ToggleStoryButton, {
+ story: story,
+ onClick: this.onStoryToggle
+ })), /*#__PURE__*/external_React_default().createElement("td", {
+ className: "message-summary"
+ }, /*#__PURE__*/external_React_default().createElement("pre", null, storyData)));
+ }
+ renderFeed(feed) {
+ const {
+ feeds
+ } = this.props.state.DiscoveryStream;
+ if (!feed.url) {
+ return null;
+ }
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "min"
+ }, "Feed url"), /*#__PURE__*/external_React_default().createElement("td", null, feed.url)), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "min"
+ }, "Data last fetched"), /*#__PURE__*/external_React_default().createElement("td", null, relativeTime(feeds.data[feed.url] ? feeds.data[feed.url].lastUpdated : null) || "(no data)")));
+ }
+ render() {
+ const prefToggles = "enabled collapsible".split(" ");
+ const {
+ config,
+ layout
+ } = this.props.state.DiscoveryStream;
+ const personalized = this.props.otherPrefs["discoverystream.personalization.enabled"];
+ return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("button", {
+ className: "button",
+ onClick: this.restorePrefDefaults
+ }, "Restore Pref Defaults"), " ", /*#__PURE__*/external_React_default().createElement("button", {
+ className: "button",
+ onClick: this.refreshCache
+ }, "Refresh Cache"), /*#__PURE__*/external_React_default().createElement("br", null), /*#__PURE__*/external_React_default().createElement("button", {
+ className: "button",
+ onClick: this.expireCache
+ }, "Expire Cache"), " ", /*#__PURE__*/external_React_default().createElement("button", {
+ className: "button",
+ onClick: this.systemTick
+ }, "Trigger System Tick"), " ", /*#__PURE__*/external_React_default().createElement("button", {
+ className: "button",
+ onClick: this.idleDaily
+ }, "Trigger Idle Daily"), /*#__PURE__*/external_React_default().createElement("br", null), /*#__PURE__*/external_React_default().createElement("button", {
+ className: "button",
+ onClick: this.syncRemoteSettings
+ }, "Sync Remote Settings"), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, prefToggles.map(pref => /*#__PURE__*/external_React_default().createElement(Row, {
+ key: pref
+ }, /*#__PURE__*/external_React_default().createElement("td", null, /*#__PURE__*/external_React_default().createElement(TogglePrefCheckbox, {
+ checked: config[pref],
+ pref: pref,
+ onChange: this.setConfigValue
+ })))))), /*#__PURE__*/external_React_default().createElement("h3", null, "Layout"), layout.map((row, rowIndex) => /*#__PURE__*/external_React_default().createElement("div", {
+ key: `row-${rowIndex}`
+ }, row.components.map((component, componentIndex) => /*#__PURE__*/external_React_default().createElement("div", {
+ key: `component-${componentIndex}`,
+ className: "ds-component"
+ }, this.renderComponent(row.width, component))))), /*#__PURE__*/external_React_default().createElement("h3", null, "Personalization"), /*#__PURE__*/external_React_default().createElement(Personalization, {
+ personalized: personalized,
+ dispatch: this.props.dispatch,
+ state: {
+ Personalization: this.props.state.Personalization
+ }
+ }), /*#__PURE__*/external_React_default().createElement("h3", null, "Spocs"), this.renderSpocs(), /*#__PURE__*/external_React_default().createElement("h3", null, "Feeds Data"), this.renderFeedsData());
+ }
+}
+class DiscoveryStreamAdminInner extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.setState = this.setState.bind(this);
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: `discoverystream-admin ${this.props.collapsed ? "collapsed" : "expanded"}`
+ }, /*#__PURE__*/external_React_default().createElement("main", {
+ className: "main-panel"
+ }, /*#__PURE__*/external_React_default().createElement("h1", null, "Discovery Stream Admin"), /*#__PURE__*/external_React_default().createElement("p", {
+ className: "helpLink"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "icon icon-small-spacer icon-info"
+ }), " ", /*#__PURE__*/external_React_default().createElement("span", null, "Need to access the ASRouter Admin dev tools?", " ", /*#__PURE__*/external_React_default().createElement("a", {
+ target: "blank",
+ href: "about:asrouter"
+ }, "Click here"))), /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamAdminUI, {
+ state: {
+ DiscoveryStream: this.props.DiscoveryStream,
+ Personalization: this.props.Personalization
+ },
+ otherPrefs: this.props.Prefs.values,
+ dispatch: this.props.dispatch
+ }))));
+ }
+}
+class CollapseToggle extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onCollapseToggle = this.onCollapseToggle.bind(this);
+ this.state = {
+ collapsed: false
+ };
+ }
+ get renderAdmin() {
+ const {
+ props
+ } = this;
+ return props.location.hash && props.location.hash.startsWith("#devtools");
+ }
+ onCollapseToggle(e) {
+ e.preventDefault();
+ this.setState(state => ({
+ collapsed: !state.collapsed
+ }));
+ }
+ setBodyClass() {
+ if (this.renderAdmin && !this.state.collapsed) {
+ __webpack_require__.g.document.body.classList.add("no-scroll");
+ } else {
+ __webpack_require__.g.document.body.classList.remove("no-scroll");
+ }
+ }
+ componentDidMount() {
+ this.setBodyClass();
+ }
+ componentDidUpdate() {
+ this.setBodyClass();
+ }
+ componentWillUnmount() {
+ __webpack_require__.g.document.body.classList.remove("no-scroll");
+ }
+ render() {
+ const {
+ props
+ } = this;
+ const {
+ renderAdmin
+ } = this;
+ const isCollapsed = this.state.collapsed || !renderAdmin;
+ const label = `${isCollapsed ? "Expand" : "Collapse"} devtools`;
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("a", {
+ href: "#devtools",
+ title: label,
+ "aria-label": label,
+ className: `discoverystream-admin-toggle ${isCollapsed ? "collapsed" : "expanded"}`,
+ onClick: this.renderAdmin ? this.onCollapseToggle : null
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "icon icon-devtools"
+ })), renderAdmin ? /*#__PURE__*/external_React_default().createElement(DiscoveryStreamAdminInner, _extends({}, props, {
+ collapsed: this.state.collapsed
+ })) : null);
+ }
+}
+const _DiscoveryStreamAdmin = props => /*#__PURE__*/external_React_default().createElement(SimpleHashRouter, null, /*#__PURE__*/external_React_default().createElement(CollapseToggle, props));
+const DiscoveryStreamAdmin = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Sections: state.Sections,
+ DiscoveryStream: state.DiscoveryStream,
+ Personalization: state.Personalization,
+ Prefs: state.Prefs
+}))(_DiscoveryStreamAdmin);
+;// CONCATENATED MODULE: ./content-src/components/ConfirmDialog/ConfirmDialog.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+/**
+ * ConfirmDialog component.
+ * One primary action button, one cancel button.
+ *
+ * Content displayed is controlled by `data` prop the component receives.
+ * Example:
+ * data: {
+ * // Any sort of data needed to be passed around by actions.
+ * payload: site.url,
+ * // Primary button AlsoToMain action.
+ * action: "DELETE_HISTORY_URL",
+ * // Primary button USerEvent action.
+ * userEvent: "DELETE",
+ * // Array of locale ids to display.
+ * message_body: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"],
+ * // Text for primary button.
+ * confirm_button_string_id: "menu_action_delete"
+ * },
+ */
+class _ConfirmDialog extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this._handleCancelBtn = this._handleCancelBtn.bind(this);
+ this._handleConfirmBtn = this._handleConfirmBtn.bind(this);
+ }
+ _handleCancelBtn() {
+ this.props.dispatch({
+ type: actionTypes.DIALOG_CANCEL
+ });
+ this.props.dispatch(actionCreators.UserEvent({
+ event: actionTypes.DIALOG_CANCEL,
+ source: this.props.data.eventSource
+ }));
+ }
+ _handleConfirmBtn() {
+ this.props.data.onConfirm.forEach(this.props.dispatch);
+ }
+ _renderModalMessage() {
+ const message_body = this.props.data.body_string_id;
+ if (!message_body) {
+ return null;
+ }
+ return /*#__PURE__*/external_React_default().createElement("span", null, message_body.map(msg => /*#__PURE__*/external_React_default().createElement("p", {
+ key: msg,
+ "data-l10n-id": msg
+ })));
+ }
+ render() {
+ if (!this.props.visible) {
+ return null;
+ }
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "confirmation-dialog"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "modal-overlay",
+ onClick: this._handleCancelBtn,
+ role: "presentation"
+ }), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "modal"
+ }, /*#__PURE__*/external_React_default().createElement("section", {
+ className: "modal-message"
+ }, this.props.data.icon && /*#__PURE__*/external_React_default().createElement("span", {
+ className: `icon icon-spacer icon-${this.props.data.icon}`
+ }), this._renderModalMessage()), /*#__PURE__*/external_React_default().createElement("section", {
+ className: "actions"
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ onClick: this._handleCancelBtn,
+ "data-l10n-id": this.props.data.cancel_button_string_id
+ }), /*#__PURE__*/external_React_default().createElement("button", {
+ className: "done",
+ onClick: this._handleConfirmBtn,
+ "data-l10n-id": this.props.data.confirm_button_string_id
+ }))));
+ }
+}
+const ConfirmDialog = (0,external_ReactRedux_namespaceObject.connect)(state => state.Dialog)(_ConfirmDialog);
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+const PLACEHOLDER_IMAGE_DATA_ARRAY = [{
+ rotation: "0deg",
+ offsetx: "20px",
+ offsety: "8px",
+ scale: "45%"
+}, {
+ rotation: "54deg",
+ offsetx: "-26px",
+ offsety: "62px",
+ scale: "55%"
+}, {
+ rotation: "-30deg",
+ offsetx: "78px",
+ offsety: "30px",
+ scale: "68%"
+}, {
+ rotation: "-22deg",
+ offsetx: "0",
+ offsety: "92px",
+ scale: "60%"
+}, {
+ rotation: "-65deg",
+ offsetx: "66px",
+ offsety: "28px",
+ scale: "60%"
+}, {
+ rotation: "22deg",
+ offsetx: "-35px",
+ offsety: "62px",
+ scale: "52%"
+}, {
+ rotation: "-25deg",
+ offsetx: "86px",
+ offsety: "-15px",
+ scale: "68%"
+}];
+const PLACEHOLDER_IMAGE_COLORS_ARRAY = "#0090ED #FF4F5F #2AC3A2 #FF7139 #A172FF #FFA437 #FF2A8A".split(" ");
+function generateIndex({
+ keyCode,
+ max
+}) {
+ if (!keyCode) {
+ // Just grab a random index if we cannot generate an index from a key.
+ return Math.floor(Math.random() * max);
+ }
+ const hashStr = str => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ let charCode = str.charCodeAt(i);
+ hash += charCode;
+ }
+ return hash;
+ };
+ const hash = hashStr(keyCode);
+ return hash % max;
+}
+function PlaceholderImage({
+ urlKey,
+ titleKey
+}) {
+ const dataIndex = generateIndex({
+ keyCode: urlKey,
+ max: PLACEHOLDER_IMAGE_DATA_ARRAY.length
+ });
+ const colorIndex = generateIndex({
+ keyCode: titleKey,
+ max: PLACEHOLDER_IMAGE_COLORS_ARRAY.length
+ });
+ const {
+ rotation,
+ offsetx,
+ offsety,
+ scale
+ } = PLACEHOLDER_IMAGE_DATA_ARRAY[dataIndex];
+ const color = PLACEHOLDER_IMAGE_COLORS_ARRAY[colorIndex];
+ const style = {
+ "--placeholderBackgroundColor": color,
+ "--placeholderBackgroundRotation": rotation,
+ "--placeholderBackgroundOffsetx": offsetx,
+ "--placeholderBackgroundOffsety": offsety,
+ "--placeholderBackgroundScale": scale
+ };
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ style: style,
+ className: "placeholder-image"
+ });
+}
+class DSImage extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onOptimizedImageError = this.onOptimizedImageError.bind(this);
+ this.onNonOptimizedImageError = this.onNonOptimizedImageError.bind(this);
+ this.onLoad = this.onLoad.bind(this);
+ this.state = {
+ isLoaded: false,
+ optimizedImageFailed: false,
+ useTransition: false
+ };
+ }
+ onIdleCallback() {
+ if (!this.state.isLoaded) {
+ this.setState({
+ useTransition: true
+ });
+ }
+ }
+ reformatImageURL(url, width, height) {
+ // Change the image URL to request a size tailored for the parent container width
+ // Also: force JPEG, quality 60, no upscaling, no EXIF data
+ // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html
+ return `https://img-getpocket.cdn.mozilla.net/${width}x${height}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent(url)}`;
+ }
+ componentDidMount() {
+ this.idleCallbackId = this.props.windowObj.requestIdleCallback(this.onIdleCallback.bind(this));
+ }
+ componentWillUnmount() {
+ if (this.idleCallbackId) {
+ this.props.windowObj.cancelIdleCallback(this.idleCallbackId);
+ }
+ }
+ render() {
+ let classNames = `ds-image
+ ${this.props.extraClassNames ? ` ${this.props.extraClassNames}` : ``}
+ ${this.state && this.state.useTransition ? ` use-transition` : ``}
+ ${this.state && this.state.isLoaded ? ` loaded` : ``}
+ `;
+ let img;
+ if (this.state) {
+ if (this.props.optimize && this.props.rawSource && !this.state.optimizedImageFailed) {
+ let baseSource = this.props.rawSource;
+ let sizeRules = [];
+ let srcSetRules = [];
+ for (let rule of this.props.sizes) {
+ let {
+ mediaMatcher,
+ width,
+ height
+ } = rule;
+ let sizeRule = `${mediaMatcher} ${width}px`;
+ sizeRules.push(sizeRule);
+ let srcSetRule = `${this.reformatImageURL(baseSource, width, height)} ${width}w`;
+ let srcSetRule2x = `${this.reformatImageURL(baseSource, width * 2, height * 2)} ${width * 2}w`;
+ srcSetRules.push(srcSetRule);
+ srcSetRules.push(srcSetRule2x);
+ }
+ if (this.props.sizes.length) {
+ // We have to supply a fallback in the very unlikely event that none of
+ // the media queries match. The smallest dimension was chosen arbitrarily.
+ sizeRules.push(`${this.props.sizes[this.props.sizes.length - 1].width}px`);
+ }
+ img = /*#__PURE__*/external_React_default().createElement("img", {
+ loading: "lazy",
+ alt: this.props.alt_text,
+ crossOrigin: "anonymous",
+ onLoad: this.onLoad,
+ onError: this.onOptimizedImageError,
+ sizes: sizeRules.join(","),
+ src: baseSource,
+ srcSet: srcSetRules.join(",")
+ });
+ } else if (this.props.source && !this.state.nonOptimizedImageFailed) {
+ img = /*#__PURE__*/external_React_default().createElement("img", {
+ loading: "lazy",
+ alt: this.props.alt_text,
+ crossOrigin: "anonymous",
+ onLoad: this.onLoad,
+ onError: this.onNonOptimizedImageError,
+ src: this.props.source
+ });
+ } else {
+ // We consider a failed to load img or source without an image as loaded.
+ classNames = `${classNames} loaded`;
+ // Remove the img element if we have no source. Render a placeholder instead.
+ // This only happens for recent saves without a source.
+ if (this.props.isRecentSave && !this.props.rawSource && !this.props.source) {
+ img = /*#__PURE__*/external_React_default().createElement(PlaceholderImage, {
+ urlKey: this.props.url,
+ titleKey: this.props.title
+ });
+ } else {
+ img = /*#__PURE__*/external_React_default().createElement("div", {
+ className: "broken-image"
+ });
+ }
+ }
+ }
+ return /*#__PURE__*/external_React_default().createElement("picture", {
+ className: classNames
+ }, img);
+ }
+ onOptimizedImageError() {
+ // This will trigger a re-render and the unoptimized 450px image will be used as a fallback
+ this.setState({
+ optimizedImageFailed: true
+ });
+ }
+ onNonOptimizedImageError() {
+ this.setState({
+ nonOptimizedImageFailed: true
+ });
+ }
+ onLoad() {
+ this.setState({
+ isLoaded: true
+ });
+ }
+}
+DSImage.defaultProps = {
+ source: null,
+ // The current source style from Pocket API (always 450px)
+ rawSource: null,
+ // Unadulterated image URL to filter through Thumbor
+ extraClassNames: null,
+ // Additional classnames to append to component
+ optimize: true,
+ // Measure parent container to request exact sizes
+ alt_text: null,
+ windowObj: window,
+ // Added to support unit tests
+ sizes: []
+};
+;// CONCATENATED MODULE: ./content-src/components/ContextMenu/ContextMenu.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+class ContextMenu extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.hideContext = this.hideContext.bind(this);
+ this.onShow = this.onShow.bind(this);
+ this.onClick = this.onClick.bind(this);
+ }
+ hideContext() {
+ this.props.onUpdate(false);
+ }
+ onShow() {
+ if (this.props.onShow) {
+ this.props.onShow();
+ }
+ }
+ componentDidMount() {
+ this.onShow();
+ setTimeout(() => {
+ __webpack_require__.g.addEventListener("click", this.hideContext);
+ }, 0);
+ }
+ componentWillUnmount() {
+ __webpack_require__.g.removeEventListener("click", this.hideContext);
+ }
+ onClick(event) {
+ // Eat all clicks on the context menu so they don't bubble up to window.
+ // This prevents the context menu from closing when clicking disabled items
+ // or the separators.
+ event.stopPropagation();
+ }
+ render() {
+ // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper.
+ return (
+ /*#__PURE__*/
+ // eslint-disable-next-line jsx-a11y/interactive-supports-focus
+ external_React_default().createElement("span", {
+ className: "context-menu"
+ }, /*#__PURE__*/external_React_default().createElement("ul", {
+ role: "menu",
+ onClick: this.onClick,
+ onKeyDown: this.onClick,
+ className: "context-menu-list"
+ }, this.props.options.map((option, i) => option.type === "separator" ? /*#__PURE__*/external_React_default().createElement("li", {
+ key: i,
+ className: "separator",
+ role: "separator"
+ }) : option.type !== "empty" && /*#__PURE__*/external_React_default().createElement(ContextMenuItem, {
+ key: i,
+ option: option,
+ hideContext: this.hideContext,
+ keyboardAccess: this.props.keyboardAccess
+ }))))
+ );
+ }
+}
+class _ContextMenuItem extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onClick = this.onClick.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onKeyUp = this.onKeyUp.bind(this);
+ this.focusFirst = this.focusFirst.bind(this);
+ }
+ onClick(event) {
+ this.props.hideContext();
+ this.props.option.onClick(event);
+ }
+
+ // Focus the first menu item if the menu was accessed via the keyboard.
+ focusFirst(button) {
+ if (this.props.keyboardAccess && button) {
+ button.focus();
+ }
+ }
+
+ // This selects the correct node based on the key pressed
+ focusSibling(target, key) {
+ const parent = target.parentNode;
+ const closestSiblingSelector = key === "ArrowUp" ? "previousSibling" : "nextSibling";
+ if (!parent[closestSiblingSelector]) {
+ return;
+ }
+ if (parent[closestSiblingSelector].firstElementChild) {
+ parent[closestSiblingSelector].firstElementChild.focus();
+ } else {
+ parent[closestSiblingSelector][closestSiblingSelector].firstElementChild.focus();
+ }
+ }
+ onKeyDown(event) {
+ const {
+ option
+ } = this.props;
+ switch (event.key) {
+ case "Tab":
+ // tab goes down in context menu, shift + tab goes up in context menu
+ // if we're on the last item, one more tab will close the context menu
+ // similarly, if we're on the first item, one more shift + tab will close it
+ if (event.shiftKey && option.first || !event.shiftKey && option.last) {
+ this.props.hideContext();
+ }
+ break;
+ case "ArrowUp":
+ case "ArrowDown":
+ event.preventDefault();
+ this.focusSibling(event.target, event.key);
+ break;
+ case "Enter":
+ case " ":
+ event.preventDefault();
+ this.props.hideContext();
+ option.onClick();
+ break;
+ case "Escape":
+ this.props.hideContext();
+ break;
+ }
+ }
+
+ // Prevents the default behavior of spacebar
+ // scrolling the page & auto-triggering buttons.
+ onKeyUp(event) {
+ if (event.key === " ") {
+ event.preventDefault();
+ }
+ }
+ render() {
+ const {
+ option
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement("li", {
+ role: "presentation",
+ className: "context-menu-item"
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ className: option.disabled ? "disabled" : "",
+ role: "menuitem",
+ onClick: this.onClick,
+ onKeyDown: this.onKeyDown,
+ onKeyUp: this.onKeyUp,
+ ref: option.first ? this.focusFirst : null,
+ "aria-haspopup": option.id === "newtab-menu-edit-topsites" ? "dialog" : null
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": option.string_id || option.id
+ })));
+ }
+}
+const ContextMenuItem = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Prefs: state.Prefs
+}))(_ContextMenuItem);
+;// CONCATENATED MODULE: ./content-src/lib/link-menu-options.js
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+const _OpenInPrivateWindow = site => ({
+ id: "newtab-menu-open-new-private-window",
+ icon: "new-window-private",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.OPEN_PRIVATE_WINDOW,
+ data: {
+ url: site.url,
+ referrer: site.referrer
+ }
+ }),
+ userEvent: "OPEN_PRIVATE_WINDOW"
+});
+
+/**
+ * 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.
+ */
+const LinkMenuOptions = {
+ Separator: () => ({
+ type: "separator"
+ }),
+ EmptyItem: () => ({
+ type: "empty"
+ }),
+ ShowPrivacyInfo: site => ({
+ id: "newtab-menu-show-privacy-info",
+ icon: "info",
+ action: {
+ type: actionTypes.SHOW_PRIVACY_INFO
+ },
+ userEvent: "SHOW_PRIVACY_INFO"
+ }),
+ AboutSponsored: site => ({
+ id: "newtab-menu-show-privacy-info",
+ icon: "info",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.ABOUT_SPONSORED_TOP_SITES,
+ data: {
+ advertiser_name: (site.label || site.hostname).toLocaleLowerCase(),
+ position: site.sponsored_position,
+ tile_id: site.sponsored_tile_id
+ }
+ }),
+ userEvent: "TOPSITE_SPONSOR_INFO"
+ }),
+ RemoveBookmark: site => ({
+ id: "newtab-menu-remove-bookmark",
+ icon: "bookmark-added",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.DELETE_BOOKMARK_BY_ID,
+ data: site.bookmarkGuid
+ }),
+ userEvent: "BOOKMARK_DELETE"
+ }),
+ AddBookmark: site => ({
+ id: "newtab-menu-bookmark",
+ icon: "bookmark-hollow",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.BOOKMARK_URL,
+ data: {
+ url: site.url,
+ title: site.title,
+ type: site.type
+ }
+ }),
+ userEvent: "BOOKMARK_ADD"
+ }),
+ OpenInNewWindow: site => ({
+ id: "newtab-menu-open-new-window",
+ icon: "new-window",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.OPEN_NEW_WINDOW,
+ data: {
+ referrer: site.referrer,
+ typedBonus: site.typedBonus,
+ url: site.url,
+ sponsored_tile_id: site.sponsored_tile_id
+ }
+ }),
+ userEvent: "OPEN_NEW_WINDOW"
+ }),
+ // This blocks the url for regular stories,
+ // but also sends a message to DiscoveryStream with flight_id.
+ // If DiscoveryStream sees this message for a flight_id
+ // it also blocks it on the flight_id.
+ BlockUrl: (site, index, eventSource) => {
+ return LinkMenuOptions.BlockUrls([site], index, eventSource);
+ },
+ // Same as BlockUrl, cept can work on an array of sites.
+ BlockUrls: (tiles, pos, eventSource) => ({
+ id: "newtab-menu-dismiss",
+ icon: "dismiss",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.BLOCK_URL,
+ data: tiles.map(site => ({
+ url: site.original_url || site.open_url || site.url,
+ // pocket_id is only for pocket stories being in highlights, and then dismissed.
+ pocket_id: site.pocket_id,
+ // used by PlacesFeed and TopSitesFeed for sponsored top sites blocking.
+ isSponsoredTopSite: site.sponsored_position,
+ ...(site.flight_id ? {
+ flight_id: site.flight_id
+ } : {}),
+ // If not sponsored, hostname could be anything (Cat3 Data!).
+ // So only put in advertiser_name for sponsored topsites.
+ ...(site.sponsored_position ? {
+ advertiser_name: (site.label || site.hostname)?.toLocaleLowerCase()
+ } : {}),
+ position: pos,
+ ...(site.sponsored_tile_id ? {
+ tile_id: site.sponsored_tile_id
+ } : {}),
+ is_pocket_card: site.type === "CardGrid"
+ }))
+ }),
+ impression: actionCreators.ImpressionStats({
+ source: eventSource,
+ block: 0,
+ tiles: tiles.map((site, index) => ({
+ id: site.guid,
+ pos: pos + index,
+ ...(site.shim && site.shim.delete ? {
+ shim: site.shim.delete
+ } : {})
+ }))
+ }),
+ userEvent: "BLOCK"
+ }),
+ // This is an option for web extentions which will result in remove items from
+ // memory and notify the web extenion, rather than using the built-in block list.
+ WebExtDismiss: (site, index, eventSource) => ({
+ id: "menu_action_webext_dismiss",
+ string_id: "newtab-menu-dismiss",
+ icon: "dismiss",
+ action: actionCreators.WebExtEvent(actionTypes.WEBEXT_DISMISS, {
+ source: eventSource,
+ url: site.url,
+ action_position: index
+ })
+ }),
+ DeleteUrl: (site, index, eventSource, isEnabled, siteInfo) => ({
+ id: "newtab-menu-delete-history",
+ icon: "delete",
+ action: {
+ type: actionTypes.DIALOG_OPEN,
+ data: {
+ onConfirm: [actionCreators.AlsoToMain({
+ type: actionTypes.DELETE_HISTORY_URL,
+ data: {
+ url: site.url,
+ pocket_id: site.pocket_id,
+ forceBlock: site.bookmarkGuid
+ }
+ }), actionCreators.UserEvent(Object.assign({
+ event: "DELETE",
+ source: eventSource,
+ action_position: index
+ }, siteInfo))],
+ eventSource,
+ body_string_id: ["newtab-confirm-delete-history-p1", "newtab-confirm-delete-history-p2"],
+ confirm_button_string_id: "newtab-topsites-delete-history-button",
+ cancel_button_string_id: "newtab-topsites-cancel-button",
+ icon: "modal-delete"
+ }
+ },
+ userEvent: "DIALOG_OPEN"
+ }),
+ ShowFile: site => ({
+ id: "newtab-menu-show-file",
+ icon: "search",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.SHOW_DOWNLOAD_FILE,
+ data: {
+ url: site.url
+ }
+ })
+ }),
+ OpenFile: site => ({
+ id: "newtab-menu-open-file",
+ icon: "open-file",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.OPEN_DOWNLOAD_FILE,
+ data: {
+ url: site.url
+ }
+ })
+ }),
+ CopyDownloadLink: site => ({
+ id: "newtab-menu-copy-download-link",
+ icon: "copy",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.COPY_DOWNLOAD_LINK,
+ data: {
+ url: site.url
+ }
+ })
+ }),
+ GoToDownloadPage: site => ({
+ id: "newtab-menu-go-to-download-page",
+ icon: "download",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.OPEN_LINK,
+ data: {
+ url: site.referrer
+ }
+ }),
+ disabled: !site.referrer
+ }),
+ RemoveDownload: site => ({
+ id: "newtab-menu-remove-download",
+ icon: "delete",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.REMOVE_DOWNLOAD_FILE,
+ data: {
+ url: site.url
+ }
+ })
+ }),
+ PinTopSite: (site, index) => ({
+ id: "newtab-menu-pin",
+ icon: "pin",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.TOP_SITES_PIN,
+ data: {
+ site,
+ index
+ }
+ }),
+ userEvent: "PIN"
+ }),
+ UnpinTopSite: site => ({
+ id: "newtab-menu-unpin",
+ icon: "unpin",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.TOP_SITES_UNPIN,
+ data: {
+ site: {
+ url: site.url
+ }
+ }
+ }),
+ userEvent: "UNPIN"
+ }),
+ SaveToPocket: (site, index, eventSource = "CARDGRID") => ({
+ id: "newtab-menu-save-to-pocket",
+ icon: "pocket-save",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.SAVE_TO_POCKET,
+ data: {
+ site: {
+ url: site.url,
+ title: site.title
+ }
+ }
+ }),
+ impression: actionCreators.ImpressionStats({
+ source: eventSource,
+ pocket: 0,
+ tiles: [{
+ id: site.guid,
+ pos: index,
+ ...(site.shim && site.shim.save ? {
+ shim: site.shim.save
+ } : {})
+ }]
+ }),
+ userEvent: "SAVE_TO_POCKET"
+ }),
+ DeleteFromPocket: site => ({
+ id: "newtab-menu-delete-pocket",
+ icon: "pocket-delete",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.DELETE_FROM_POCKET,
+ data: {
+ pocket_id: site.pocket_id
+ }
+ }),
+ userEvent: "DELETE_FROM_POCKET"
+ }),
+ ArchiveFromPocket: site => ({
+ id: "newtab-menu-archive-pocket",
+ icon: "pocket-archive",
+ action: actionCreators.AlsoToMain({
+ type: actionTypes.ARCHIVE_FROM_POCKET,
+ data: {
+ pocket_id: site.pocket_id
+ }
+ }),
+ userEvent: "ARCHIVE_FROM_POCKET"
+ }),
+ EditTopSite: (site, index) => ({
+ id: "newtab-menu-edit-topsites",
+ icon: "edit",
+ action: {
+ type: actionTypes.TOP_SITES_EDIT,
+ data: {
+ index
+ }
+ }
+ }),
+ CheckBookmark: site => site.bookmarkGuid ? LinkMenuOptions.RemoveBookmark(site) : LinkMenuOptions.AddBookmark(site),
+ CheckPinTopSite: (site, index) => site.isPinned ? LinkMenuOptions.UnpinTopSite(site) : LinkMenuOptions.PinTopSite(site, index),
+ CheckSavedToPocket: (site, index, source) => site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.SaveToPocket(site, index, source),
+ CheckBookmarkOrArchive: site => site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.CheckBookmark(site),
+ CheckArchiveFromPocket: site => site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.EmptyItem(),
+ CheckDeleteFromPocket: site => site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.EmptyItem(),
+ OpenInPrivateWindow: (site, index, eventSource, isEnabled) => isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem()
+};
+;// CONCATENATED MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"];
+class _LinkMenu extends (external_React_default()).PureComponent {
+ getOptions() {
+ const {
+ props
+ } = this;
+ const {
+ site,
+ index,
+ source,
+ isPrivateBrowsingEnabled,
+ siteInfo,
+ platform,
+ userEvent = actionCreators.UserEvent
+ } = props;
+
+ // Handle special case of default site
+ const propOptions = site.isDefault && !site.searchTopSite && !site.sponsored_position ? DEFAULT_SITE_MENU_OPTIONS : props.options;
+ const options = propOptions.map(o => LinkMenuOptions[o](site, index, source, isPrivateBrowsingEnabled, siteInfo, platform)).map(option => {
+ const {
+ action,
+ impression,
+ id,
+ type,
+ userEvent: eventName
+ } = option;
+ if (!type && id) {
+ option.onClick = (event = {}) => {
+ const {
+ ctrlKey,
+ metaKey,
+ shiftKey,
+ button
+ } = event;
+ // Only send along event info if there's something non-default to send
+ if (ctrlKey || metaKey || shiftKey || button === 1) {
+ action.data = Object.assign({
+ event: {
+ ctrlKey,
+ metaKey,
+ shiftKey,
+ button
+ }
+ }, action.data);
+ }
+ props.dispatch(action);
+ if (eventName) {
+ const userEventData = Object.assign({
+ event: eventName,
+ source,
+ action_position: index,
+ value: {
+ card_type: site.flight_id ? "spoc" : "organic"
+ }
+ }, siteInfo);
+ props.dispatch(userEvent(userEventData));
+ }
+ if (impression && props.shouldSendImpressionStats) {
+ props.dispatch(impression);
+ }
+ };
+ }
+ return option;
+ });
+
+ // This is for accessibility to support making each item tabbable.
+ // We want to know which item is the first and which item
+ // is the last, so we can close the context menu accordingly.
+ options[0].first = true;
+ options[options.length - 1].last = true;
+ return options;
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement(ContextMenu, {
+ onUpdate: this.props.onUpdate,
+ onShow: this.props.onShow,
+ options: this.getOptions(),
+ keyboardAccess: this.props.keyboardAccess
+ });
+ }
+}
+const getState = state => ({
+ isPrivateBrowsingEnabled: state.Prefs.values.isPrivateBrowsingEnabled,
+ platform: state.Prefs.values.platform
+});
+const LinkMenu = (0,external_ReactRedux_namespaceObject.connect)(getState)(_LinkMenu);
+;// CONCATENATED MODULE: ./content-src/components/ContextMenu/ContextMenuButton.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class ContextMenuButton extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ showContextMenu: false,
+ contextMenuKeyboard: false
+ };
+ this.onClick = this.onClick.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onUpdate = this.onUpdate.bind(this);
+ }
+ openContextMenu(isKeyBoard, event) {
+ if (this.props.onUpdate) {
+ this.props.onUpdate(true);
+ }
+ this.setState({
+ showContextMenu: true,
+ contextMenuKeyboard: isKeyBoard
+ });
+ }
+ onClick(event) {
+ event.preventDefault();
+ this.openContextMenu(false, event);
+ }
+ onKeyDown(event) {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ this.openContextMenu(true, event);
+ }
+ }
+ onUpdate(showContextMenu) {
+ if (this.props.onUpdate) {
+ this.props.onUpdate(showContextMenu);
+ }
+ this.setState({
+ showContextMenu
+ });
+ }
+ render() {
+ const {
+ tooltipArgs,
+ tooltip,
+ children,
+ refFunction
+ } = this.props;
+ const {
+ showContextMenu,
+ contextMenuKeyboard
+ } = this.state;
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("button", {
+ "aria-haspopup": "true",
+ "data-l10n-id": tooltip,
+ "data-l10n-args": tooltipArgs ? JSON.stringify(tooltipArgs) : null,
+ className: "context-menu-button icon",
+ onKeyDown: this.onKeyDown,
+ onClick: this.onClick,
+ ref: refFunction
+ }), showContextMenu ? /*#__PURE__*/external_React_default().cloneElement(children, {
+ keyboardAccess: contextMenuKeyboard,
+ onUpdate: this.onUpdate
+ }) : null);
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+class DSLinkMenu extends (external_React_default()).PureComponent {
+ render() {
+ const {
+ index,
+ dispatch
+ } = this.props;
+ let pocketMenuOptions = [];
+ let TOP_STORIES_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow"];
+ if (!this.props.isRecentSave) {
+ if (this.props.pocket_button_enabled) {
+ pocketMenuOptions = this.props.saveToPocketCard ? ["CheckDeleteFromPocket"] : ["CheckSavedToPocket"];
+ }
+ TOP_STORIES_CONTEXT_MENU_OPTIONS = ["CheckBookmark", "CheckArchiveFromPocket", ...pocketMenuOptions, "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", ...(this.props.showPrivacyInfo ? ["ShowPrivacyInfo"] : [])];
+ }
+ const type = this.props.type || "DISCOVERY_STREAM";
+ const title = this.props.title || this.props.source;
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "context-menu-position-container"
+ }, /*#__PURE__*/external_React_default().createElement(ContextMenuButton, {
+ tooltip: "newtab-menu-content-tooltip",
+ tooltipArgs: {
+ title
+ },
+ onUpdate: this.props.onMenuUpdate
+ }, /*#__PURE__*/external_React_default().createElement(LinkMenu, {
+ dispatch: dispatch,
+ index: index,
+ source: type.toUpperCase(),
+ onShow: this.props.onMenuShow,
+ options: TOP_STORIES_CONTEXT_MENU_OPTIONS,
+ shouldSendImpressionStats: true,
+ userEvent: actionCreators.DiscoveryStreamUserEvent,
+ site: {
+ referrer: "https://getpocket.com/recommendations",
+ title: this.props.title,
+ type: this.props.type,
+ url: this.props.url,
+ guid: this.props.id,
+ pocket_id: this.props.pocket_id,
+ shim: this.props.shim,
+ bookmarkGuid: this.props.bookmarkGuid,
+ flight_id: this.props.flightId
+ }
+ })));
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSitesConstants.js
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const TOP_SITES_SOURCE = "TOP_SITES";
+const TOP_SITES_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"];
+const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "ShowPrivacyInfo"];
+const TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "AboutSponsored"];
+// the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite
+const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "BlockUrl"];
+// minimum size necessary to show a rich icon instead of a screenshot
+const MIN_RICH_FAVICON_SIZE = 96;
+// minimum size necessary to show any icon
+const MIN_SMALL_FAVICON_SIZE = 16;
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+const VISIBLE = "visible";
+const VISIBILITY_CHANGE_EVENT = "visibilitychange";
+
+// Per analytical requirement, we set the minimal intersection ratio to
+// 0.5, and an impression is identified when the wrapped item has at least
+// 50% visibility.
+//
+// This constant is exported for unit test
+const INTERSECTION_RATIO = 0.5;
+
+/**
+ * Impression wrapper for Discovery Stream related React components.
+ *
+ * It makses use of the Intersection Observer API to detect the visibility,
+ * and relies on page visibility to ensure the impression is reported
+ * only when the component is visible on the page.
+ *
+ * Note:
+ * * This wrapper used to be used either at the individual card level,
+ * or by the card container components.
+ * It is now only used for individual card level.
+ * * Each impression will be sent only once as soon as the desired
+ * visibility is detected
+ * * Batching is not yet implemented, hence it might send multiple
+ * impression pings separately
+ */
+class ImpressionStats_ImpressionStats extends (external_React_default()).PureComponent {
+ // This checks if the given cards are the same as those in the last impression ping.
+ // If so, it should not send the same impression ping again.
+ _needsImpressionStats(cards) {
+ if (!this.impressionCardGuids || this.impressionCardGuids.length !== cards.length) {
+ return true;
+ }
+ for (let i = 0; i < cards.length; i++) {
+ if (cards[i].id !== this.impressionCardGuids[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+ _dispatchImpressionStats() {
+ const {
+ props
+ } = this;
+ const cards = props.rows;
+ if (this.props.flightId) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.DISCOVERY_STREAM_SPOC_IMPRESSION,
+ data: {
+ flightId: this.props.flightId
+ }
+ }));
+
+ // Record sponsored topsites impressions if the source is `TOP_SITES_SOURCE`.
+ if (this.props.source === TOP_SITES_SOURCE) {
+ for (const card of cards) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.TOP_SITES_SPONSORED_IMPRESSION_STATS,
+ data: {
+ type: "impression",
+ tile_id: card.id,
+ source: "newtab",
+ advertiser: card.advertiser,
+ // Keep the 0-based position, can be adjusted by the telemetry
+ // sender if necessary.
+ position: card.pos
+ }
+ }));
+ }
+ }
+ }
+ if (this._needsImpressionStats(cards)) {
+ props.dispatch(actionCreators.DiscoveryStreamImpressionStats({
+ source: props.source.toUpperCase(),
+ window_inner_width: window.innerWidth,
+ window_inner_height: window.innerHeight,
+ tiles: cards.map(link => ({
+ id: link.id,
+ pos: link.pos,
+ type: this.props.flightId ? "spoc" : "organic",
+ ...(link.shim ? {
+ shim: link.shim
+ } : {}),
+ recommendation_id: link.recommendation_id
+ }))
+ }));
+ this.impressionCardGuids = cards.map(link => link.id);
+ }
+ }
+
+ // This checks if the given cards are the same as those in the last loaded content ping.
+ // If so, it should not send the same loaded content ping again.
+ _needsLoadedContent(cards) {
+ if (!this.loadedContentGuids || this.loadedContentGuids.length !== cards.length) {
+ return true;
+ }
+ for (let i = 0; i < cards.length; i++) {
+ if (cards[i].id !== this.loadedContentGuids[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+ _dispatchLoadedContent() {
+ const {
+ props
+ } = this;
+ const cards = props.rows;
+ if (this._needsLoadedContent(cards)) {
+ props.dispatch(actionCreators.DiscoveryStreamLoadedContent({
+ source: props.source.toUpperCase(),
+ tiles: cards.map(link => ({
+ id: link.id,
+ pos: link.pos
+ }))
+ }));
+ this.loadedContentGuids = cards.map(link => link.id);
+ }
+ }
+ setImpressionObserverOrAddListener() {
+ const {
+ props
+ } = this;
+ if (!props.dispatch) {
+ return;
+ }
+ if (props.document.visibilityState === VISIBLE) {
+ // Send the loaded content ping once the page is visible.
+ this._dispatchLoadedContent();
+ this.setImpressionObserver();
+ } else {
+ // We should only ever send the latest impression stats ping, so remove any
+ // older listeners.
+ if (this._onVisibilityChange) {
+ props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ this._onVisibilityChange = () => {
+ if (props.document.visibilityState === VISIBLE) {
+ // Send the loaded content ping once the page is visible.
+ this._dispatchLoadedContent();
+ this.setImpressionObserver();
+ props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ };
+ props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+
+ /**
+ * Set an impression observer for the wrapped component. It makes use of
+ * the Intersection Observer API to detect if the wrapped component is
+ * visible with a desired ratio, and only sends impression if that's the case.
+ *
+ * See more details about Intersection Observer API at:
+ * https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
+ */
+ setImpressionObserver() {
+ const {
+ props
+ } = this;
+ if (!props.rows.length) {
+ return;
+ }
+ this._handleIntersect = entries => {
+ if (entries.some(entry => entry.isIntersecting && entry.intersectionRatio >= INTERSECTION_RATIO)) {
+ this._dispatchImpressionStats();
+ this.impressionObserver.unobserve(this.refs.impression);
+ }
+ };
+ const options = {
+ threshold: INTERSECTION_RATIO
+ };
+ this.impressionObserver = new props.IntersectionObserver(this._handleIntersect, options);
+ this.impressionObserver.observe(this.refs.impression);
+ }
+ componentDidMount() {
+ if (this.props.rows.length) {
+ this.setImpressionObserverOrAddListener();
+ }
+ }
+ componentWillUnmount() {
+ if (this._handleIntersect && this.impressionObserver) {
+ this.impressionObserver.unobserve(this.refs.impression);
+ }
+ if (this._onVisibilityChange) {
+ this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ ref: "impression",
+ className: "impression-observer"
+ }, this.props.children);
+ }
+}
+ImpressionStats_ImpressionStats.defaultProps = {
+ IntersectionObserver: __webpack_require__.g.IntersectionObserver,
+ document: __webpack_require__.g.document,
+ rows: [],
+ source: ""
+};
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+class SafeAnchor extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onClick = this.onClick.bind(this);
+ }
+ onClick(event) {
+ // Use dispatch instead of normal link click behavior to include referrer
+ if (this.props.dispatch) {
+ event.preventDefault();
+ const {
+ altKey,
+ button,
+ ctrlKey,
+ metaKey,
+ shiftKey
+ } = event;
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.OPEN_LINK,
+ data: {
+ event: {
+ altKey,
+ button,
+ ctrlKey,
+ metaKey,
+ shiftKey
+ },
+ referrer: "https://getpocket.com/recommendations",
+ // Use the anchor's url, which could have been cleaned up
+ url: event.currentTarget.href
+ }
+ }));
+ }
+
+ // Propagate event if there's a handler
+ if (this.props.onLinkClick) {
+ this.props.onLinkClick(event);
+ }
+ }
+ safeURI(url) {
+ let protocol = null;
+ try {
+ protocol = new URL(url).protocol;
+ } catch (e) {
+ return "";
+ }
+ const isAllowed = ["http:", "https:"].includes(protocol);
+ if (!isAllowed) {
+ console.warn(`${url} is not allowed for anchor targets.`); // eslint-disable-line no-console
+ return "";
+ }
+ return url;
+ }
+ render() {
+ const {
+ url,
+ className
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement("a", {
+ href: this.safeURI(url),
+ className: className,
+ onClick: this.onClick
+ }, this.props.children);
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/Card/types.js
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const cardContextTypes = {
+ history: {
+ fluentID: "newtab-label-visited",
+ icon: "history-item"
+ },
+ removedBookmark: {
+ fluentID: "newtab-label-removed-bookmark",
+ icon: "bookmark-removed"
+ },
+ bookmark: {
+ fluentID: "newtab-label-bookmarked",
+ icon: "bookmark-added"
+ },
+ trending: {
+ fluentID: "newtab-label-recommended",
+ icon: "trending"
+ },
+ pocket: {
+ fluentID: "newtab-label-saved",
+ icon: "pocket"
+ },
+ download: {
+ fluentID: "newtab-label-download",
+ icon: "download"
+ }
+};
+;// CONCATENATED MODULE: external "ReactTransitionGroup"
+const external_ReactTransitionGroup_namespaceObject = ReactTransitionGroup;
+;// CONCATENATED MODULE: ./content-src/components/FluentOrText/FluentOrText.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+/**
+ * Set text on a child element/component depending on if the message is already
+ * translated plain text or a fluent id with optional args.
+ */
+class FluentOrText extends (external_React_default()).PureComponent {
+ render() {
+ // Ensure we have a single child to attach attributes
+ const {
+ children,
+ message
+ } = this.props;
+ const child = children ? external_React_default().Children.only(children) : /*#__PURE__*/external_React_default().createElement("span", null);
+
+ // For a string message, just use it as the child's text
+ let grandChildren = message;
+ let extraProps;
+
+ // Convert a message object to set desired fluent-dom attributes
+ if (typeof message === "object") {
+ const args = message.args || message.values;
+ extraProps = {
+ "data-l10n-args": args && JSON.stringify(args),
+ "data-l10n-id": message.id || message.string_id
+ };
+
+ // Use original children potentially with data-l10n-name attributes
+ grandChildren = child.props.children;
+ }
+
+ // Add the message to the child via fluent attributes or text node
+ return /*#__PURE__*/external_React_default().cloneElement(child, extraProps, grandChildren);
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+// Animation time is mirrored in DSContextFooter.scss
+const ANIMATION_DURATION = 3000;
+const DSMessageLabel = props => {
+ const {
+ context,
+ context_type
+ } = props;
+ const {
+ icon,
+ fluentID
+ } = cardContextTypes[context_type] || {};
+ if (!context && context_type) {
+ return /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.TransitionGroup, {
+ component: null
+ }, /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.CSSTransition, {
+ key: fluentID,
+ timeout: ANIMATION_DURATION,
+ classNames: "story-animate"
+ }, /*#__PURE__*/external_React_default().createElement(StatusMessage, {
+ icon: icon,
+ fluentID: fluentID
+ })));
+ }
+ return null;
+};
+const StatusMessage = ({
+ icon,
+ fluentID
+}) => /*#__PURE__*/external_React_default().createElement("div", {
+ className: "status-message"
+}, /*#__PURE__*/external_React_default().createElement("span", {
+ "aria-haspopup": "true",
+ className: `story-badge-icon icon icon-${icon}`
+}), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "story-context-label",
+ "data-l10n-id": fluentID
+}));
+const SponsorLabel = ({
+ sponsored_by_override,
+ sponsor,
+ context,
+ newSponsoredLabel
+}) => {
+ const classList = `story-sponsored-label ${newSponsoredLabel || ""} clamp`;
+ // If override is not false or an empty string.
+ if (sponsored_by_override) {
+ return /*#__PURE__*/external_React_default().createElement("p", {
+ className: classList
+ }, sponsored_by_override);
+ } else if (sponsored_by_override === "") {
+ // We specifically want to display nothing if the server returns an empty string.
+ // So the server can turn off the label.
+ // This is to support the use cases where the sponsored context is displayed elsewhere.
+ return null;
+ } else if (sponsor) {
+ return /*#__PURE__*/external_React_default().createElement("p", {
+ className: classList
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: {
+ id: `newtab-label-sponsored-by`,
+ values: {
+ sponsor
+ }
+ }
+ }));
+ } else if (context) {
+ return /*#__PURE__*/external_React_default().createElement("p", {
+ className: classList
+ }, context);
+ }
+ return null;
+};
+class DSContextFooter extends (external_React_default()).PureComponent {
+ render() {
+ const {
+ context,
+ context_type,
+ sponsor,
+ sponsored_by_override,
+ cta_button_variant,
+ source
+ } = this.props;
+ const sponsorLabel = SponsorLabel({
+ sponsored_by_override,
+ sponsor,
+ context
+ });
+ const dsMessageLabel = DSMessageLabel({
+ context,
+ context_type
+ });
+ if (cta_button_variant === "variant-a") {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "story-footer"
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ "aria-hidden": "true",
+ className: "story-cta-button"
+ }, "Shop Now"), sponsorLabel);
+ }
+ if (cta_button_variant === "variant-b") {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "story-footer"
+ }, sponsorLabel, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "source clamp cta-footer-source"
+ }, source));
+ }
+ if (sponsorLabel || dsMessageLabel) {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "story-footer"
+ }, sponsorLabel, dsMessageLabel);
+ }
+ return null;
+ }
+}
+const DSMessageFooter = props => {
+ const {
+ context,
+ context_type,
+ saveToPocketCard
+ } = props;
+ const dsMessageLabel = DSMessageLabel({
+ context,
+ context_type
+ });
+
+ // This case is specific and already displayed to the user elsewhere.
+ if (!dsMessageLabel || saveToPocketCard && context_type === "pocket") {
+ return null;
+ }
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "story-footer"
+ }, dsMessageLabel);
+};
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+
+
+const READING_WPM = 220;
+
+/**
+ * READ TIME FROM WORD COUNT
+ * @param {int} wordCount number of words in an article
+ * @returns {int} number of words per minute in minutes
+ */
+function readTimeFromWordCount(wordCount) {
+ if (!wordCount) {
+ return false;
+ }
+ return Math.ceil(parseInt(wordCount, 10) / READING_WPM);
+}
+const DSSource = ({
+ source,
+ timeToRead,
+ newSponsoredLabel,
+ context,
+ sponsor,
+ sponsored_by_override
+}) => {
+ // First try to display sponsored label or time to read here.
+ if (newSponsoredLabel) {
+ // If we can display something for spocs, do so.
+ if (sponsored_by_override || sponsor || context) {
+ return /*#__PURE__*/external_React_default().createElement(SponsorLabel, {
+ context: context,
+ sponsor: sponsor,
+ sponsored_by_override: sponsored_by_override,
+ newSponsoredLabel: "new-sponsored-label"
+ });
+ }
+ }
+
+ // If we are not a spoc, and can display a time to read value.
+ if (source && timeToRead) {
+ return /*#__PURE__*/external_React_default().createElement("p", {
+ className: "source clamp time-to-read"
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: {
+ id: `newtab-label-source-read-time`,
+ values: {
+ source,
+ timeToRead
+ }
+ }
+ }));
+ }
+
+ // Otherwise display a default source.
+ return /*#__PURE__*/external_React_default().createElement("p", {
+ className: "source clamp"
+ }, source);
+};
+const DefaultMeta = ({
+ source,
+ title,
+ excerpt,
+ timeToRead,
+ newSponsoredLabel,
+ context,
+ context_type,
+ sponsor,
+ sponsored_by_override,
+ saveToPocketCard,
+ isRecentSave,
+ ctaButtonVariant
+}) => /*#__PURE__*/external_React_default().createElement("div", {
+ className: "meta"
+}, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "info-wrap"
+}, ctaButtonVariant !== "variant-b" && /*#__PURE__*/external_React_default().createElement(DSSource, {
+ source: source,
+ timeToRead: timeToRead,
+ newSponsoredLabel: newSponsoredLabel,
+ context: context,
+ sponsor: sponsor,
+ sponsored_by_override: sponsored_by_override
+}), /*#__PURE__*/external_React_default().createElement("header", {
+ title: title,
+ className: "title clamp"
+}, title), excerpt && /*#__PURE__*/external_React_default().createElement("p", {
+ className: "excerpt clamp"
+}, excerpt)), !newSponsoredLabel && /*#__PURE__*/external_React_default().createElement(DSContextFooter, {
+ context_type: context_type,
+ context: context,
+ sponsor: sponsor,
+ sponsored_by_override: sponsored_by_override,
+ cta_button_variant: ctaButtonVariant,
+ source: source
+}), newSponsoredLabel && /*#__PURE__*/external_React_default().createElement(DSMessageFooter, {
+ context_type: context_type,
+ context: null,
+ saveToPocketCard: saveToPocketCard
+}));
+class _DSCard extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onLinkClick = this.onLinkClick.bind(this);
+ this.onSaveClick = this.onSaveClick.bind(this);
+ this.onMenuUpdate = this.onMenuUpdate.bind(this);
+ this.onMenuShow = this.onMenuShow.bind(this);
+ this.setContextMenuButtonHostRef = element => {
+ this.contextMenuButtonHostElement = element;
+ };
+ this.setPlaceholderRef = element => {
+ this.placeholderElement = element;
+ };
+ this.state = {
+ isSeen: false
+ };
+
+ // If this is for the about:home startup cache, then we always want
+ // to render the DSCard, regardless of whether or not its been seen.
+ if (props.App.isForStartupCache) {
+ this.state.isSeen = true;
+ }
+
+ // We want to choose the optimal thumbnail for the underlying DSImage, but
+ // want to do it in a performant way. The breakpoints used in the
+ // CSS of the page are, unfortuntely, not easy to retrieve without
+ // causing a style flush. To avoid that, we hardcode them here.
+ //
+ // The values chosen here were the dimensions of the card thumbnails as
+ // computed by getBoundingClientRect() for each type of viewport width
+ // across both high-density and normal-density displays.
+ this.dsImageSizes = [{
+ mediaMatcher: "(min-width: 1122px)",
+ width: 296,
+ height: 148
+ }, {
+ mediaMatcher: "(min-width: 866px)",
+ width: 218,
+ height: 109
+ }, {
+ mediaMatcher: "(max-width: 610px)",
+ width: 202,
+ height: 101
+ }];
+ }
+ onLinkClick(event) {
+ if (this.props.dispatch) {
+ this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: this.props.type.toUpperCase(),
+ action_position: this.props.pos,
+ value: {
+ card_type: this.props.flightId ? "spoc" : "organic",
+ recommendation_id: this.props.recommendation_id,
+ tile_id: this.props.id,
+ ...(this.props.shim && this.props.shim.click ? {
+ shim: this.props.shim.click
+ } : {})
+ }
+ }));
+ this.props.dispatch(actionCreators.ImpressionStats({
+ source: this.props.type.toUpperCase(),
+ click: 0,
+ window_inner_width: this.props.windowObj.innerWidth,
+ window_inner_height: this.props.windowObj.innerHeight,
+ tiles: [{
+ id: this.props.id,
+ pos: this.props.pos,
+ ...(this.props.shim && this.props.shim.click ? {
+ shim: this.props.shim.click
+ } : {}),
+ type: this.props.flightId ? "spoc" : "organic",
+ recommendation_id: this.props.recommendation_id
+ }]
+ }));
+ }
+ }
+ onSaveClick(event) {
+ if (this.props.dispatch) {
+ this.props.dispatch(actionCreators.AlsoToMain({
+ type: actionTypes.SAVE_TO_POCKET,
+ data: {
+ site: {
+ url: this.props.url,
+ title: this.props.title
+ }
+ }
+ }));
+ this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "SAVE_TO_POCKET",
+ source: "CARDGRID_HOVER",
+ action_position: this.props.pos,
+ value: {
+ card_type: this.props.flightId ? "spoc" : "organic",
+ recommendation_id: this.props.recommendation_id,
+ tile_id: this.props.id,
+ ...(this.props.shim && this.props.shim.save ? {
+ shim: this.props.shim.save
+ } : {})
+ }
+ }));
+ this.props.dispatch(actionCreators.ImpressionStats({
+ source: "CARDGRID_HOVER",
+ pocket: 0,
+ tiles: [{
+ id: this.props.id,
+ pos: this.props.pos,
+ ...(this.props.shim && this.props.shim.save ? {
+ shim: this.props.shim.save
+ } : {}),
+ recommendation_id: this.props.recommendation_id
+ }]
+ }));
+ }
+ }
+ onMenuUpdate(showContextMenu) {
+ if (!showContextMenu) {
+ const dsLinkMenuHostDiv = this.contextMenuButtonHostElement;
+ if (dsLinkMenuHostDiv) {
+ dsLinkMenuHostDiv.classList.remove("active", "last-item");
+ }
+ }
+ }
+ async onMenuShow() {
+ const dsLinkMenuHostDiv = this.contextMenuButtonHostElement;
+ if (dsLinkMenuHostDiv) {
+ // Force translation so we can be sure it's ready before measuring.
+ await this.props.windowObj.document.l10n.translateFragment(dsLinkMenuHostDiv);
+ if (this.props.windowObj.scrollMaxX > 0) {
+ dsLinkMenuHostDiv.classList.add("last-item");
+ }
+ dsLinkMenuHostDiv.classList.add("active");
+ }
+ }
+ onSeen(entries) {
+ if (this.state) {
+ const entry = entries.find(e => e.isIntersecting);
+ if (entry) {
+ if (this.placeholderElement) {
+ this.observer.unobserve(this.placeholderElement);
+ }
+
+ // Stop observing since element has been seen
+ this.setState({
+ isSeen: true
+ });
+ }
+ }
+ }
+ onIdleCallback() {
+ if (!this.state.isSeen) {
+ if (this.observer && this.placeholderElement) {
+ this.observer.unobserve(this.placeholderElement);
+ }
+ this.setState({
+ isSeen: true
+ });
+ }
+ }
+ componentDidMount() {
+ this.idleCallbackId = this.props.windowObj.requestIdleCallback(this.onIdleCallback.bind(this));
+ if (this.placeholderElement) {
+ this.observer = new IntersectionObserver(this.onSeen.bind(this));
+ this.observer.observe(this.placeholderElement);
+ }
+ }
+ componentWillUnmount() {
+ // Remove observer on unmount
+ if (this.observer && this.placeholderElement) {
+ this.observer.unobserve(this.placeholderElement);
+ }
+ if (this.idleCallbackId) {
+ this.props.windowObj.cancelIdleCallback(this.idleCallbackId);
+ }
+ }
+ render() {
+ if (this.props.placeholder || !this.state.isSeen) {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-card placeholder",
+ ref: this.setPlaceholderRef
+ });
+ }
+ const {
+ isRecentSave,
+ DiscoveryStream,
+ saveToPocketCard
+ } = this.props;
+ let source = this.props.source || this.props.publisher;
+ if (!source) {
+ try {
+ source = new URL(this.props.url).hostname;
+ } catch (e) {}
+ }
+ const {
+ pocketButtonEnabled,
+ hideDescriptions,
+ compactImages,
+ imageGradient,
+ newSponsoredLabel,
+ titleLines = 3,
+ descLines = 3,
+ readTime: displayReadTime
+ } = DiscoveryStream;
+ const excerpt = !hideDescriptions ? this.props.excerpt : "";
+ let timeToRead;
+ if (displayReadTime) {
+ timeToRead = this.props.time_to_read || readTimeFromWordCount(this.props.word_count);
+ }
+ const ctaButtonEnabled = this.props.ctaButtonSponsors?.includes(this.props.sponsor?.toLowerCase());
+ let ctaButtonVariant = "";
+ if (ctaButtonEnabled) {
+ ctaButtonVariant = this.props.ctaButtonVariant;
+ }
+ let ctaButtonVariantClassName = ctaButtonVariant;
+ const ctaButtonClassName = ctaButtonEnabled ? `ds-card-cta-button` : ``;
+ const compactImagesClassName = compactImages ? `ds-card-compact-image` : ``;
+ const imageGradientClassName = imageGradient ? `ds-card-image-gradient` : ``;
+ const titleLinesName = `ds-card-title-lines-${titleLines}`;
+ const descLinesClassName = `ds-card-desc-lines-${descLines}`;
+ let stpButton = () => {
+ return /*#__PURE__*/external_React_default().createElement("button", {
+ className: "card-stp-button",
+ onClick: this.onSaveClick
+ }, this.props.context_type === "pocket" ? /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "story-badge-icon icon icon-pocket"
+ }), /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": "newtab-pocket-saved"
+ })) : /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "story-badge-icon icon icon-pocket-save"
+ }), /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": "newtab-pocket-save"
+ })));
+ };
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: `ds-card ${compactImagesClassName} ${imageGradientClassName} ${titleLinesName} ${descLinesClassName} ${ctaButtonClassName} ${ctaButtonVariantClassName}`,
+ ref: this.setContextMenuButtonHostRef
+ }, /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ className: "ds-card-link",
+ dispatch: this.props.dispatch,
+ onLinkClick: !this.props.placeholder ? this.onLinkClick : undefined,
+ url: this.props.url
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "img-wrapper"
+ }, /*#__PURE__*/external_React_default().createElement(DSImage, {
+ extraClassNames: "img",
+ source: this.props.image_src,
+ rawSource: this.props.raw_image_src,
+ sizes: this.dsImageSizes,
+ url: this.props.url,
+ title: this.props.title,
+ isRecentSave: isRecentSave
+ })), ctaButtonVariant === "variant-b" && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "cta-header"
+ }, "Shop Now"), /*#__PURE__*/external_React_default().createElement(DefaultMeta, {
+ source: source,
+ title: this.props.title,
+ excerpt: excerpt,
+ newSponsoredLabel: newSponsoredLabel,
+ timeToRead: timeToRead,
+ context: this.props.context,
+ context_type: this.props.context_type,
+ sponsor: this.props.sponsor,
+ sponsored_by_override: this.props.sponsored_by_override,
+ saveToPocketCard: saveToPocketCard,
+ ctaButtonVariant: ctaButtonVariant
+ }), /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, {
+ flightId: this.props.flightId,
+ rows: [{
+ id: this.props.id,
+ pos: this.props.pos,
+ ...(this.props.shim && this.props.shim.impression ? {
+ shim: this.props.shim.impression
+ } : {}),
+ recommendation_id: this.props.recommendation_id
+ }],
+ dispatch: this.props.dispatch,
+ source: this.props.type
+ })), saveToPocketCard && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card-stp-button-hover-background"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card-stp-button-position-wrapper"
+ }, !this.props.flightId && stpButton(), /*#__PURE__*/external_React_default().createElement(DSLinkMenu, {
+ id: this.props.id,
+ index: this.props.pos,
+ dispatch: this.props.dispatch,
+ url: this.props.url,
+ title: this.props.title,
+ source: source,
+ type: this.props.type,
+ pocket_id: this.props.pocket_id,
+ shim: this.props.shim,
+ bookmarkGuid: this.props.bookmarkGuid,
+ flightId: !this.props.is_collection ? this.props.flightId : undefined,
+ showPrivacyInfo: !!this.props.flightId,
+ onMenuUpdate: this.onMenuUpdate,
+ onMenuShow: this.onMenuShow,
+ saveToPocketCard: saveToPocketCard,
+ pocket_button_enabled: pocketButtonEnabled,
+ isRecentSave: isRecentSave
+ }))), !saveToPocketCard && /*#__PURE__*/external_React_default().createElement(DSLinkMenu, {
+ id: this.props.id,
+ index: this.props.pos,
+ dispatch: this.props.dispatch,
+ url: this.props.url,
+ title: this.props.title,
+ source: source,
+ type: this.props.type,
+ pocket_id: this.props.pocket_id,
+ shim: this.props.shim,
+ bookmarkGuid: this.props.bookmarkGuid,
+ flightId: !this.props.is_collection ? this.props.flightId : undefined,
+ showPrivacyInfo: !!this.props.flightId,
+ hostRef: this.contextMenuButtonHostRef,
+ onMenuUpdate: this.onMenuUpdate,
+ onMenuShow: this.onMenuShow,
+ pocket_button_enabled: pocketButtonEnabled,
+ isRecentSave: isRecentSave
+ }));
+ }
+}
+_DSCard.defaultProps = {
+ windowObj: window // Added to support unit tests
+};
+const DSCard = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ App: state.App,
+ DiscoveryStream: state.DiscoveryStream
+}))(_DSCard);
+const PlaceholderDSCard = props => /*#__PURE__*/external_React_default().createElement(DSCard, {
+ placeholder: true
+});
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+class DSEmptyState extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onReset = this.onReset.bind(this);
+ this.state = {};
+ }
+ componentWillUnmount() {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ }
+ onReset() {
+ if (this.props.dispatch && this.props.feed) {
+ const {
+ feed
+ } = this.props;
+ const {
+ url
+ } = feed;
+ this.props.dispatch({
+ type: actionTypes.DISCOVERY_STREAM_FEED_UPDATE,
+ data: {
+ feed: {
+ ...feed,
+ data: {
+ ...feed.data,
+ status: "waiting"
+ }
+ },
+ url
+ }
+ });
+ this.setState({
+ waiting: true
+ });
+ this.timeout = setTimeout(() => {
+ this.timeout = null;
+ this.setState({
+ waiting: false
+ });
+ }, 300);
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.DISCOVERY_STREAM_RETRY_FEED,
+ data: {
+ feed
+ }
+ }));
+ }
+ }
+ renderButton() {
+ if (this.props.status === "waiting" || this.state.waiting) {
+ return /*#__PURE__*/external_React_default().createElement("button", {
+ className: "try-again-button waiting",
+ "data-l10n-id": "newtab-discovery-empty-section-topstories-loading"
+ });
+ }
+ return /*#__PURE__*/external_React_default().createElement("button", {
+ className: "try-again-button",
+ onClick: this.onReset,
+ "data-l10n-id": "newtab-discovery-empty-section-topstories-try-again-button"
+ });
+ }
+ renderState() {
+ if (this.props.status === "waiting" || this.props.status === "failed") {
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("h2", {
+ "data-l10n-id": "newtab-discovery-empty-section-topstories-timed-out"
+ }), this.renderButton());
+ }
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("h2", {
+ "data-l10n-id": "newtab-discovery-empty-section-topstories-header"
+ }), /*#__PURE__*/external_React_default().createElement("p", {
+ "data-l10n-id": "newtab-discovery-empty-section-topstories-content"
+ }));
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "section-empty-state"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "empty-state-message"
+ }, this.renderState()));
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class DSDismiss extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onDismissClick = this.onDismissClick.bind(this);
+ this.onHover = this.onHover.bind(this);
+ this.offHover = this.offHover.bind(this);
+ this.state = {
+ hovering: false
+ };
+ }
+ onDismissClick() {
+ if (this.props.onDismissClick) {
+ this.props.onDismissClick();
+ }
+ }
+ onHover() {
+ this.setState({
+ hovering: true
+ });
+ }
+ offHover() {
+ this.setState({
+ hovering: false
+ });
+ }
+ render() {
+ let className = `ds-dismiss
+ ${this.state.hovering ? ` hovering` : ``}
+ ${this.props.extraClasses ? ` ${this.props.extraClasses}` : ``}`;
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: className
+ }, this.props.children, /*#__PURE__*/external_React_default().createElement("button", {
+ className: "ds-dismiss-button",
+ "data-l10n-id": "newtab-dismiss-button-tooltip",
+ onClick: this.onDismissClick,
+ onMouseEnter: this.onHover,
+ onMouseLeave: this.offHover
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "icon icon-dismiss"
+ })));
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+function _TopicsWidget(props) {
+ const {
+ id,
+ source,
+ position,
+ DiscoveryStream,
+ dispatch
+ } = props;
+ const {
+ utmCampaign,
+ utmContent,
+ utmSource
+ } = DiscoveryStream.experimentData;
+ let queryParams = `?utm_source=${utmSource}`;
+ if (utmCampaign && utmContent) {
+ queryParams += `&utm_content=${utmContent}&utm_campaign=${utmCampaign}`;
+ }
+ const topics = [{
+ label: "Technology",
+ name: "technology"
+ }, {
+ label: "Science",
+ name: "science"
+ }, {
+ label: "Self-Improvement",
+ name: "self-improvement"
+ }, {
+ label: "Travel",
+ name: "travel"
+ }, {
+ label: "Career",
+ name: "career"
+ }, {
+ label: "Entertainment",
+ name: "entertainment"
+ }, {
+ label: "Food",
+ name: "food"
+ }, {
+ label: "Health",
+ name: "health"
+ }, {
+ label: "Must-Reads",
+ name: "must-reads",
+ url: `https://getpocket.com/collections${queryParams}`
+ }];
+ function onLinkClick(topic, positionInCard) {
+ if (dispatch) {
+ dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source,
+ action_position: position,
+ value: {
+ card_type: "topics_widget",
+ topic,
+ ...(positionInCard || positionInCard === 0 ? {
+ position_in_card: positionInCard
+ } : {})
+ }
+ }));
+ dispatch(actionCreators.ImpressionStats({
+ source,
+ click: 0,
+ window_inner_width: props.windowObj.innerWidth,
+ window_inner_height: props.windowObj.innerHeight,
+ tiles: [{
+ id,
+ pos: position
+ }]
+ }));
+ }
+ }
+ function mapTopicItem(topic, index) {
+ return /*#__PURE__*/external_React_default().createElement("li", {
+ key: topic.name,
+ className: topic.overflow ? "ds-topics-widget-list-overflow-item" : ""
+ }, /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ url: topic.url || `https://getpocket.com/explore/${topic.name}${queryParams}`,
+ dispatch: dispatch,
+ onLinkClick: () => onLinkClick(topic.name, index)
+ }, topic.label));
+ }
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-topics-widget"
+ }, /*#__PURE__*/external_React_default().createElement("header", {
+ className: "ds-topics-widget-header"
+ }, "Popular Topics"), /*#__PURE__*/external_React_default().createElement("hr", null), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-topics-widget-list-container"
+ }, /*#__PURE__*/external_React_default().createElement("ul", null, topics.map(mapTopicItem))), /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ className: "ds-topics-widget-button button primary",
+ url: `https://getpocket.com/${queryParams}`,
+ dispatch: dispatch,
+ onLinkClick: () => onLinkClick("more-topics")
+ }, "More Topics"), /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, {
+ dispatch: dispatch,
+ rows: [{
+ id,
+ pos: position
+ }],
+ source: source
+ }));
+}
+_TopicsWidget.defaultProps = {
+ windowObj: window // Added to support unit tests
+};
+const TopicsWidget = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ DiscoveryStream: state.DiscoveryStream
+}))(_TopicsWidget);
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+
+
+const PREF_ONBOARDING_EXPERIENCE_DISMISSED = "discoverystream.onboardingExperience.dismissed";
+const CardGrid_INTERSECTION_RATIO = 0.5;
+const CardGrid_VISIBLE = "visible";
+const CardGrid_VISIBILITY_CHANGE_EVENT = "visibilitychange";
+const WIDGET_IDS = {
+ TOPICS: 1
+};
+function DSSubHeader({
+ children
+}) {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "section-top-bar ds-sub-header"
+ }, /*#__PURE__*/external_React_default().createElement("h3", {
+ className: "section-title-container"
+ }, children));
+}
+function OnboardingExperience({
+ children,
+ dispatch,
+ windowObj = __webpack_require__.g
+}) {
+ const [dismissed, setDismissed] = (0,external_React_namespaceObject.useState)(false);
+ const [maxHeight, setMaxHeight] = (0,external_React_namespaceObject.useState)(null);
+ const heightElement = (0,external_React_namespaceObject.useRef)(null);
+ const onDismissClick = (0,external_React_namespaceObject.useCallback)(() => {
+ // We update this as state and redux.
+ // The state update is for this newtab,
+ // and the redux update is for other tabs, offscreen tabs, and future tabs.
+ // We need the state update for this tab to support the transition.
+ setDismissed(true);
+ dispatch(actionCreators.SetPref(PREF_ONBOARDING_EXPERIENCE_DISMISSED, true));
+ dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "BLOCK",
+ source: "POCKET_ONBOARDING"
+ }));
+ }, [dispatch]);
+ (0,external_React_namespaceObject.useEffect)(() => {
+ const resizeObserver = new windowObj.ResizeObserver(() => {
+ if (heightElement.current) {
+ setMaxHeight(heightElement.current.offsetHeight);
+ }
+ });
+ const options = {
+ threshold: CardGrid_INTERSECTION_RATIO
+ };
+ const intersectionObserver = new windowObj.IntersectionObserver(entries => {
+ if (entries.some(entry => entry.isIntersecting && entry.intersectionRatio >= CardGrid_INTERSECTION_RATIO)) {
+ dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "IMPRESSION",
+ source: "POCKET_ONBOARDING"
+ }));
+ // Once we have observed an impression, we can stop for this instance of newtab.
+ intersectionObserver.unobserve(heightElement.current);
+ }
+ }, options);
+ const onVisibilityChange = () => {
+ intersectionObserver.observe(heightElement.current);
+ windowObj.document.removeEventListener(CardGrid_VISIBILITY_CHANGE_EVENT, onVisibilityChange);
+ };
+ if (heightElement.current) {
+ resizeObserver.observe(heightElement.current);
+ // Check visibility or setup a visibility event to make
+ // sure we don't fire this for off screen pre loaded tabs.
+ if (windowObj.document.visibilityState === CardGrid_VISIBLE) {
+ intersectionObserver.observe(heightElement.current);
+ } else {
+ windowObj.document.addEventListener(CardGrid_VISIBILITY_CHANGE_EVENT, onVisibilityChange);
+ }
+ setMaxHeight(heightElement.current.offsetHeight);
+ }
+
+ // Return unmount callback to clean up observers.
+ return () => {
+ resizeObserver?.disconnect();
+ intersectionObserver?.disconnect();
+ windowObj.document.removeEventListener(CardGrid_VISIBILITY_CHANGE_EVENT, onVisibilityChange);
+ };
+ }, [dispatch, windowObj]);
+ const style = {};
+ if (dismissed) {
+ style.maxHeight = "0";
+ style.opacity = "0";
+ style.transition = "max-height 0.26s ease, opacity 0.26s ease";
+ } else if (maxHeight) {
+ style.maxHeight = `${maxHeight}px`;
+ }
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ style: style
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-onboarding-ref",
+ ref: heightElement
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-onboarding-container"
+ }, /*#__PURE__*/external_React_default().createElement(DSDismiss, {
+ onDismissClick: onDismissClick,
+ extraClasses: `ds-onboarding`
+ }, /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("header", null, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "icon icon-pocket"
+ }), /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": "newtab-pocket-onboarding-discover"
+ })), /*#__PURE__*/external_React_default().createElement("p", {
+ "data-l10n-id": "newtab-pocket-onboarding-cta"
+ })), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-onboarding-graphic"
+ })))));
+}
+function CardGrid_IntersectionObserver({
+ children,
+ windowObj = window,
+ onIntersecting
+}) {
+ const intersectionElement = (0,external_React_namespaceObject.useRef)(null);
+ (0,external_React_namespaceObject.useEffect)(() => {
+ let observer;
+ if (!observer && onIntersecting && intersectionElement.current) {
+ observer = new windowObj.IntersectionObserver(entries => {
+ const entry = entries.find(e => e.isIntersecting);
+ if (entry) {
+ // Stop observing since element has been seen
+ if (observer && intersectionElement.current) {
+ observer.unobserve(intersectionElement.current);
+ }
+ onIntersecting();
+ }
+ });
+ observer.observe(intersectionElement.current);
+ }
+ // Cleanup
+ return () => observer?.disconnect();
+ }, [windowObj, onIntersecting]);
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ ref: intersectionElement
+ }, children);
+}
+function RecentSavesContainer({
+ gridClassName = "",
+ dispatch,
+ windowObj = window,
+ items = 3,
+ source = "CARDGRID_RECENT_SAVES"
+}) {
+ const {
+ recentSavesData,
+ isUserLoggedIn,
+ experimentData: {
+ utmCampaign,
+ utmContent,
+ utmSource
+ }
+ } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream);
+ const [visible, setVisible] = (0,external_React_namespaceObject.useState)(false);
+ const onIntersecting = (0,external_React_namespaceObject.useCallback)(() => setVisible(true), []);
+ (0,external_React_namespaceObject.useEffect)(() => {
+ if (visible) {
+ dispatch(actionCreators.AlsoToMain({
+ type: actionTypes.DISCOVERY_STREAM_POCKET_STATE_INIT
+ }));
+ }
+ }, [visible, dispatch]);
+
+ // The user has not yet scrolled to this section,
+ // so wait before potentially requesting Pocket data.
+ if (!visible) {
+ return /*#__PURE__*/external_React_default().createElement(CardGrid_IntersectionObserver, {
+ windowObj: windowObj,
+ onIntersecting: onIntersecting
+ });
+ }
+
+ // Intersection observer has finished, but we're not yet logged in.
+ if (visible && !isUserLoggedIn) {
+ return null;
+ }
+ let queryParams = `?utm_source=${utmSource}`;
+ // We really only need to add these params to urls we own.
+ if (utmCampaign && utmContent) {
+ queryParams += `&utm_content=${utmContent}&utm_campaign=${utmCampaign}`;
+ }
+ function renderCard(rec, index) {
+ const url = new URL(rec.url);
+ const urlSearchParams = new URLSearchParams(queryParams);
+ if (rec?.id && !url.href.match(/getpocket\.com\/read/)) {
+ url.href = `https://getpocket.com/read/${rec.id}`;
+ }
+ for (let [key, val] of urlSearchParams.entries()) {
+ url.searchParams.set(key, val);
+ }
+ return /*#__PURE__*/external_React_default().createElement(DSCard, {
+ key: `dscard-${rec?.id || index}`,
+ id: rec.id,
+ pos: index,
+ type: source,
+ image_src: rec.image_src,
+ raw_image_src: rec.raw_image_src,
+ word_count: rec.word_count,
+ time_to_read: rec.time_to_read,
+ title: rec.title,
+ excerpt: rec.excerpt,
+ url: url.href,
+ source: rec.domain,
+ isRecentSave: true,
+ dispatch: dispatch
+ });
+ }
+ function onMyListClicked() {
+ dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: `${source}_VIEW_LIST`
+ }));
+ }
+ const recentSavesCards = [];
+ // We fill the cards with a for loop over an inline map because
+ // we want empty placeholders if there are not enough cards.
+ for (let index = 0; index < items; index++) {
+ const recentSave = recentSavesData[index];
+ if (!recentSave) {
+ recentSavesCards.push( /*#__PURE__*/external_React_default().createElement(PlaceholderDSCard, {
+ key: `dscard-${index}`
+ }));
+ } else {
+ recentSavesCards.push(renderCard({
+ id: recentSave.id,
+ image_src: recentSave.top_image_url,
+ raw_image_src: recentSave.top_image_url,
+ word_count: recentSave.word_count,
+ time_to_read: recentSave.time_to_read,
+ title: recentSave.resolved_title || recentSave.given_title,
+ url: recentSave.resolved_url || recentSave.given_url,
+ domain: recentSave.domain_metadata?.name,
+ excerpt: recentSave.excerpt
+ }, index));
+ }
+ }
+
+ // We are visible and logged in.
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(DSSubHeader, null, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "section-title"
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: "Recently Saved to your List"
+ })), /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ onLinkClick: onMyListClicked,
+ className: "section-sub-link",
+ url: `https://getpocket.com/a${queryParams}`
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: "View My List"
+ }))), /*#__PURE__*/external_React_default().createElement("div", {
+ className: `ds-card-grid-recent-saves ${gridClassName}`
+ }, recentSavesCards));
+}
+class _CardGrid extends (external_React_default()).PureComponent {
+ renderCards() {
+ const prefs = this.props.Prefs.values;
+ const {
+ items,
+ hybridLayout,
+ hideCardBackground,
+ fourCardLayout,
+ compactGrid,
+ essentialReadsHeader,
+ editorsPicksHeader,
+ onboardingExperience,
+ ctaButtonSponsors,
+ ctaButtonVariant,
+ widgets,
+ recentSavesEnabled,
+ hideDescriptions,
+ DiscoveryStream
+ } = this.props;
+ const {
+ saveToPocketCard
+ } = DiscoveryStream;
+ const showRecentSaves = prefs.showRecentSaves && recentSavesEnabled;
+ const isOnboardingExperienceDismissed = prefs[PREF_ONBOARDING_EXPERIENCE_DISMISSED];
+ const recs = this.props.data.recommendations.slice(0, items);
+ const cards = [];
+ let essentialReadsCards = [];
+ let editorsPicksCards = [];
+ for (let index = 0; index < items; index++) {
+ const rec = recs[index];
+ cards.push(!rec || rec.placeholder ? /*#__PURE__*/external_React_default().createElement(PlaceholderDSCard, {
+ key: `dscard-${index}`
+ }) : /*#__PURE__*/external_React_default().createElement(DSCard, {
+ key: `dscard-${rec.id}`,
+ pos: rec.pos,
+ flightId: rec.flight_id,
+ image_src: rec.image_src,
+ raw_image_src: rec.raw_image_src,
+ word_count: rec.word_count,
+ time_to_read: rec.time_to_read,
+ title: rec.title,
+ excerpt: rec.excerpt,
+ url: rec.url,
+ id: rec.id,
+ shim: rec.shim,
+ type: this.props.type,
+ context: rec.context,
+ sponsor: rec.sponsor,
+ sponsored_by_override: rec.sponsored_by_override,
+ dispatch: this.props.dispatch,
+ source: rec.domain,
+ publisher: rec.publisher,
+ pocket_id: rec.pocket_id,
+ context_type: rec.context_type,
+ bookmarkGuid: rec.bookmarkGuid,
+ is_collection: this.props.is_collection,
+ saveToPocketCard: saveToPocketCard,
+ ctaButtonSponsors: ctaButtonSponsors,
+ ctaButtonVariant: ctaButtonVariant,
+ recommendation_id: rec.recommendation_id
+ }));
+ }
+ if (widgets?.positions?.length && widgets?.data?.length) {
+ let positionIndex = 0;
+ const source = "CARDGRID_WIDGET";
+ for (const widget of widgets.data) {
+ let widgetComponent = null;
+ const position = widgets.positions[positionIndex];
+
+ // Stop if we run out of positions to place widgets.
+ if (!position) {
+ break;
+ }
+ switch (widget?.type) {
+ case "TopicsWidget":
+ widgetComponent = /*#__PURE__*/external_React_default().createElement(TopicsWidget, {
+ position: position.index,
+ dispatch: this.props.dispatch,
+ source: source,
+ id: WIDGET_IDS.TOPICS
+ });
+ break;
+ }
+ if (widgetComponent) {
+ // We found a widget, so up the position for next try.
+ positionIndex++;
+ // We replace an existing card with the widget.
+ cards.splice(position.index, 1, widgetComponent);
+ }
+ }
+ }
+ let moreRecsHeader = "";
+ // For now this is English only.
+ if (showRecentSaves || essentialReadsHeader && editorsPicksHeader) {
+ let spliceAt = 6;
+ // For 4 card row layouts, second row is 8 cards, and regular it is 6 cards.
+ if (fourCardLayout) {
+ spliceAt = 8;
+ }
+ // If we have a custom header, ensure the more recs section also has a header.
+ moreRecsHeader = "More Recommendations";
+ // Put the first 2 rows into essentialReadsCards.
+ essentialReadsCards = [...cards.splice(0, spliceAt)];
+ // Put the rest into editorsPicksCards.
+ if (essentialReadsHeader && editorsPicksHeader) {
+ editorsPicksCards = [...cards.splice(0, cards.length)];
+ }
+ }
+ const hideCardBackgroundClass = hideCardBackground ? `ds-card-grid-hide-background` : ``;
+ const fourCardLayoutClass = fourCardLayout ? `ds-card-grid-four-card-variant` : ``;
+ const hideDescriptionsClassName = !hideDescriptions ? `ds-card-grid-include-descriptions` : ``;
+ const compactGridClassName = compactGrid ? `ds-card-grid-compact` : ``;
+ const hybridLayoutClassName = hybridLayout ? `ds-card-grid-hybrid-layout` : ``;
+ const gridClassName = `ds-card-grid ${hybridLayoutClassName} ${hideCardBackgroundClass} ${fourCardLayoutClass} ${hideDescriptionsClassName} ${compactGridClassName}`;
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, !isOnboardingExperienceDismissed && onboardingExperience && /*#__PURE__*/external_React_default().createElement(OnboardingExperience, {
+ dispatch: this.props.dispatch
+ }), essentialReadsCards?.length > 0 && /*#__PURE__*/external_React_default().createElement("div", {
+ className: gridClassName
+ }, essentialReadsCards), showRecentSaves && /*#__PURE__*/external_React_default().createElement(RecentSavesContainer, {
+ gridClassName: gridClassName,
+ dispatch: this.props.dispatch
+ }), editorsPicksCards?.length > 0 && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(DSSubHeader, null, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "section-title"
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: "Editor\u2019s Picks"
+ }))), /*#__PURE__*/external_React_default().createElement("div", {
+ className: gridClassName
+ }, editorsPicksCards)), cards?.length > 0 && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, moreRecsHeader && /*#__PURE__*/external_React_default().createElement(DSSubHeader, null, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "section-title"
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: moreRecsHeader
+ }))), /*#__PURE__*/external_React_default().createElement("div", {
+ className: gridClassName
+ }, cards)));
+ }
+ render() {
+ const {
+ data
+ } = this.props;
+
+ // Handle a render before feed has been fetched by displaying nothing
+ if (!data) {
+ return null;
+ }
+
+ // Handle the case where a user has dismissed all recommendations
+ const isEmpty = data.recommendations.length === 0;
+ return /*#__PURE__*/external_React_default().createElement("div", null, this.props.title && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-header"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "title"
+ }, this.props.title), this.props.context && /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: this.props.context
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-context"
+ }))), isEmpty ? /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-card-grid empty"
+ }, /*#__PURE__*/external_React_default().createElement(DSEmptyState, {
+ status: data.status,
+ dispatch: this.props.dispatch,
+ feed: this.props.feed
+ })) : this.renderCards());
+ }
+}
+_CardGrid.defaultProps = {
+ items: 4 // Number of stories to display
+};
+const CardGrid = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Prefs: state.Prefs,
+ DiscoveryStream: state.DiscoveryStream
+}))(_CardGrid);
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+class CollectionCardGrid extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onDismissClick = this.onDismissClick.bind(this);
+ this.state = {
+ dismissed: false
+ };
+ }
+ onDismissClick() {
+ const {
+ data
+ } = this.props;
+ if (this.props.dispatch && data && data.spocs && data.spocs.length) {
+ this.setState({
+ dismissed: true
+ });
+ const pos = 0;
+ const source = this.props.type.toUpperCase();
+ // Grab the available items in the array to dismiss.
+ // This fires a ping for all items available, even if below the fold.
+ const spocsData = data.spocs.map(item => ({
+ url: item.url,
+ guid: item.id,
+ shim: item.shim,
+ flight_id: item.flightId
+ }));
+ const blockUrlOption = LinkMenuOptions.BlockUrls(spocsData, pos, source);
+ const {
+ action,
+ impression,
+ userEvent
+ } = blockUrlOption;
+ this.props.dispatch(action);
+ this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: userEvent,
+ source,
+ action_position: pos
+ }));
+ if (impression) {
+ this.props.dispatch(impression);
+ }
+ }
+ }
+ render() {
+ const {
+ data,
+ dismissible,
+ pocket_button_enabled
+ } = this.props;
+ if (this.state.dismissed || !data || !data.spocs || !data.spocs[0] ||
+ // We only display complete collections.
+ data.spocs.length < 3) {
+ return null;
+ }
+ const {
+ spocs,
+ placement,
+ feed
+ } = this.props;
+ // spocs.data is spocs state data, and not an array of spocs.
+ const {
+ title,
+ context,
+ sponsored_by_override,
+ sponsor
+ } = spocs.data[placement.name] || {};
+ // Just in case of bad data, don't display a broken collection.
+ if (!title) {
+ return null;
+ }
+ let sponsoredByMessage = "";
+
+ // If override is not false or an empty string.
+ if (sponsored_by_override || sponsored_by_override === "") {
+ // We specifically want to display nothing if the server returns an empty string.
+ // So the server can turn off the label.
+ // This is to support the use cases where the sponsored context is displayed elsewhere.
+ sponsoredByMessage = sponsored_by_override;
+ } else if (sponsor) {
+ sponsoredByMessage = {
+ id: `newtab-label-sponsored-by`,
+ values: {
+ sponsor
+ }
+ };
+ } else if (context) {
+ sponsoredByMessage = context;
+ }
+
+ // Generally a card grid displays recs with spocs already injected.
+ // Normally it doesn't care which rec is a spoc and which isn't,
+ // it just displays content in a grid.
+ // For collections, we're only displaying a list of spocs.
+ // We don't need to tell the card grid that our list of cards are spocs,
+ // it shouldn't need to care. So we just pass our spocs along as recs.
+ // Think of it as injecting all rec positions with spocs.
+ // Consider maybe making recommendations in CardGrid use a more generic name.
+ const recsData = {
+ recommendations: data.spocs
+ };
+
+ // All cards inside of a collection card grid have a slightly different type.
+ // For the case of interactions to the card grid, we use the type "COLLECTIONCARDGRID".
+ // Example, you dismiss the whole collection, we use the type "COLLECTIONCARDGRID".
+ // For interactions inside the card grid, example, you dismiss a single card in the collection,
+ // we use the type "COLLECTIONCARDGRID_CARD".
+ const type = `${this.props.type}_card`;
+ const collectionGrid = /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-collection-card-grid"
+ }, /*#__PURE__*/external_React_default().createElement(CardGrid, {
+ pocket_button_enabled: pocket_button_enabled,
+ title: title,
+ context: sponsoredByMessage,
+ data: recsData,
+ feed: feed,
+ type: type,
+ is_collection: true,
+ dispatch: this.props.dispatch,
+ items: this.props.items
+ }));
+ if (dismissible) {
+ return /*#__PURE__*/external_React_default().createElement(DSDismiss, {
+ onDismissClick: this.onDismissClick,
+ extraClasses: `ds-dismiss-ds-collection`
+ }, collectionGrid);
+ }
+ return collectionGrid;
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/A11yLinkButton/A11yLinkButton.jsx
+function A11yLinkButton_extends() { A11yLinkButton_extends = Object.assign ? Object.assign.bind() : 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; }; return A11yLinkButton_extends.apply(this, arguments); }
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+function A11yLinkButton(props) {
+ // function for merging classes, if necessary
+ let className = "a11y-link-button";
+ if (props.className) {
+ className += ` ${props.className}`;
+ }
+ return /*#__PURE__*/external_React_default().createElement("button", A11yLinkButton_extends({
+ type: "button"
+ }, props, {
+ className: className
+ }), props.children);
+}
+;// CONCATENATED MODULE: ./content-src/components/ErrorBoundary/ErrorBoundary.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+class ErrorBoundaryFallback extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.windowObj = this.props.windowObj || window;
+ this.onClick = this.onClick.bind(this);
+ }
+
+ /**
+ * Since we only get here if part of the page has crashed, do a
+ * forced reload to give us the best chance at recovering.
+ */
+ onClick() {
+ this.windowObj.location.reload(true);
+ }
+ render() {
+ const defaultClass = "as-error-fallback";
+ let className;
+ if ("className" in this.props) {
+ className = `${this.props.className} ${defaultClass}`;
+ } else {
+ className = defaultClass;
+ }
+
+ // "A11yLinkButton" to force normal link styling stuff (eg cursor on hover)
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: className
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ "data-l10n-id": "newtab-error-fallback-info"
+ }), /*#__PURE__*/external_React_default().createElement("span", null, /*#__PURE__*/external_React_default().createElement(A11yLinkButton, {
+ className: "reload-button",
+ onClick: this.onClick,
+ "data-l10n-id": "newtab-error-fallback-refresh-link"
+ })));
+ }
+}
+ErrorBoundaryFallback.defaultProps = {
+ className: "as-error-fallback"
+};
+class ErrorBoundary extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ hasError: false
+ };
+ }
+ componentDidCatch(error, info) {
+ this.setState({
+ hasError: true
+ });
+ }
+ render() {
+ if (!this.state.hasError) {
+ return this.props.children;
+ }
+ return /*#__PURE__*/external_React_default().createElement(this.props.FallbackComponent, {
+ className: this.props.className
+ });
+ }
+}
+ErrorBoundary.defaultProps = {
+ FallbackComponent: ErrorBoundaryFallback
+};
+;// CONCATENATED MODULE: ./content-src/components/CollapsibleSection/CollapsibleSection.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+/**
+ * A section that can collapse. As of bug 1710937, it can no longer collapse.
+ * See bug 1727365 for follow-up work to simplify this component.
+ */
+class _CollapsibleSection extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onBodyMount = this.onBodyMount.bind(this);
+ this.onMenuButtonMouseEnter = this.onMenuButtonMouseEnter.bind(this);
+ this.onMenuButtonMouseLeave = this.onMenuButtonMouseLeave.bind(this);
+ this.onMenuUpdate = this.onMenuUpdate.bind(this);
+ this.state = {
+ menuButtonHover: false,
+ showContextMenu: false
+ };
+ this.setContextMenuButtonRef = this.setContextMenuButtonRef.bind(this);
+ }
+ setContextMenuButtonRef(element) {
+ this.contextMenuButtonRef = element;
+ }
+ onBodyMount(node) {
+ this.sectionBody = node;
+ }
+ onMenuButtonMouseEnter() {
+ this.setState({
+ menuButtonHover: true
+ });
+ }
+ onMenuButtonMouseLeave() {
+ this.setState({
+ menuButtonHover: false
+ });
+ }
+ onMenuUpdate(showContextMenu) {
+ this.setState({
+ showContextMenu
+ });
+ }
+ render() {
+ const {
+ isAnimating,
+ maxHeight,
+ menuButtonHover,
+ showContextMenu
+ } = this.state;
+ const {
+ id,
+ collapsed,
+ learnMore,
+ title,
+ subTitle
+ } = this.props;
+ const active = menuButtonHover || showContextMenu;
+ let bodyStyle;
+ if (isAnimating && !collapsed) {
+ bodyStyle = {
+ maxHeight
+ };
+ } else if (!isAnimating && collapsed) {
+ bodyStyle = {
+ display: "none"
+ };
+ }
+ let titleStyle;
+ if (this.props.hideTitle) {
+ titleStyle = {
+ visibility: "hidden"
+ };
+ }
+ const hasSubtitleClassName = subTitle ? `has-subtitle` : ``;
+ return /*#__PURE__*/external_React_default().createElement("section", {
+ className: `collapsible-section ${this.props.className}${active ? " active" : ""}`
+ // Note: data-section-id is used for web extension api tests in mozilla central
+ ,
+ "data-section-id": id
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "section-top-bar"
+ }, /*#__PURE__*/external_React_default().createElement("h3", {
+ className: `section-title-container ${hasSubtitleClassName}`,
+ style: titleStyle
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "section-title"
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: title
+ })), /*#__PURE__*/external_React_default().createElement("span", {
+ className: "learn-more-link-wrapper"
+ }, learnMore && /*#__PURE__*/external_React_default().createElement("span", {
+ className: "learn-more-link"
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: learnMore.link.message
+ }, /*#__PURE__*/external_React_default().createElement("a", {
+ href: learnMore.link.href
+ })))), subTitle && /*#__PURE__*/external_React_default().createElement("span", {
+ className: "section-sub-title"
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: subTitle
+ })))), /*#__PURE__*/external_React_default().createElement(ErrorBoundary, {
+ className: "section-body-fallback"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ ref: this.onBodyMount,
+ style: bodyStyle
+ }, this.props.children)));
+ }
+}
+_CollapsibleSection.defaultProps = {
+ document: __webpack_require__.g.document || {
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ visibilityState: "hidden"
+ }
+};
+const CollapsibleSection = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Prefs: state.Prefs
+}))(_CollapsibleSection);
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+class DSMessage extends (external_React_default()).PureComponent {
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-message"
+ }, /*#__PURE__*/external_React_default().createElement("header", {
+ className: "title"
+ }, this.props.icon && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "glyph",
+ style: {
+ backgroundImage: `url(${this.props.icon})`
+ }
+ }), this.props.title && /*#__PURE__*/external_React_default().createElement("span", {
+ className: "title-text"
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: this.props.title
+ })), this.props.link_text && this.props.link_url && /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ className: "link",
+ url: this.props.link_url
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: this.props.link_text
+ }))));
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/ModalOverlay/ModalOverlay.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class ModalOverlayWrapper extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ }
+
+ // The intended behaviour is to listen for an escape key
+ // but not for a click; see Bug 1582242
+ onKeyDown(event) {
+ if (event.key === "Escape") {
+ this.props.onClose(event);
+ }
+ }
+ componentWillMount() {
+ this.props.document.addEventListener("keydown", this.onKeyDown);
+ this.props.document.body.classList.add("modal-open");
+ }
+ componentWillUnmount() {
+ this.props.document.removeEventListener("keydown", this.onKeyDown);
+ this.props.document.body.classList.remove("modal-open");
+ }
+ render() {
+ const {
+ props
+ } = this;
+ let className = props.unstyled ? "" : "modalOverlayInner active";
+ if (props.innerClassName) {
+ className += ` ${props.innerClassName}`;
+ }
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "modalOverlayOuter active",
+ onKeyDown: this.onKeyDown,
+ role: "presentation"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: className,
+ "aria-labelledby": props.headerId,
+ id: props.id,
+ role: "dialog"
+ }, props.children));
+ }
+}
+ModalOverlayWrapper.defaultProps = {
+ document: __webpack_require__.g.document
+};
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+class DSPrivacyModal extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.closeModal = this.closeModal.bind(this);
+ this.onLearnLinkClick = this.onLearnLinkClick.bind(this);
+ this.onManageLinkClick = this.onManageLinkClick.bind(this);
+ }
+ onLearnLinkClick(event) {
+ this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "CLICK_PRIVACY_INFO",
+ source: "DS_PRIVACY_MODAL"
+ }));
+ }
+ onManageLinkClick(event) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.SETTINGS_OPEN
+ }));
+ }
+ closeModal() {
+ this.props.dispatch({
+ type: `HIDE_PRIVACY_INFO`,
+ data: {}
+ });
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement(ModalOverlayWrapper, {
+ onClose: this.closeModal,
+ innerClassName: "ds-privacy-modal"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "privacy-notice"
+ }, /*#__PURE__*/external_React_default().createElement("h3", {
+ "data-l10n-id": "newtab-privacy-modal-header"
+ }), /*#__PURE__*/external_React_default().createElement("p", {
+ "data-l10n-id": "newtab-privacy-modal-paragraph-2"
+ }), /*#__PURE__*/external_React_default().createElement("a", {
+ className: "modal-link modal-link-privacy",
+ "data-l10n-id": "newtab-privacy-modal-link",
+ onClick: this.onLearnLinkClick,
+ href: "https://help.getpocket.com/article/1142-firefox-new-tab-recommendations-faq"
+ }), /*#__PURE__*/external_React_default().createElement("button", {
+ className: "modal-link modal-link-manage",
+ "data-l10n-id": "newtab-privacy-modal-button-manage",
+ onClick: this.onManageLinkClick
+ })), /*#__PURE__*/external_React_default().createElement("section", {
+ className: "actions"
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ className: "done",
+ type: "submit",
+ onClick: this.closeModal,
+ "data-l10n-id": "newtab-privacy-modal-button-done"
+ })));
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+class DSSignup extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ active: false,
+ lastItem: false
+ };
+ this.onMenuButtonUpdate = this.onMenuButtonUpdate.bind(this);
+ this.onLinkClick = this.onLinkClick.bind(this);
+ this.onMenuShow = this.onMenuShow.bind(this);
+ }
+ onMenuButtonUpdate(showContextMenu) {
+ if (!showContextMenu) {
+ this.setState({
+ active: false,
+ lastItem: false
+ });
+ }
+ }
+ nextAnimationFrame() {
+ return new Promise(resolve => this.props.windowObj.requestAnimationFrame(resolve));
+ }
+ async onMenuShow() {
+ let {
+ lastItem
+ } = this.state;
+ // Wait for next frame before computing scrollMaxX to allow fluent menu strings to be visible
+ await this.nextAnimationFrame();
+ if (this.props.windowObj.scrollMaxX > 0) {
+ lastItem = true;
+ }
+ this.setState({
+ active: true,
+ lastItem
+ });
+ }
+ onLinkClick() {
+ const {
+ data
+ } = this.props;
+ if (this.props.dispatch && data && data.spocs && data.spocs.length) {
+ const source = this.props.type.toUpperCase();
+ // Grab the first item in the array as we only have 1 spoc position.
+ const [spoc] = data.spocs;
+ this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source,
+ action_position: 0
+ }));
+ this.props.dispatch(actionCreators.ImpressionStats({
+ source,
+ click: 0,
+ tiles: [{
+ id: spoc.id,
+ pos: 0,
+ ...(spoc.shim && spoc.shim.click ? {
+ shim: spoc.shim.click
+ } : {})
+ }]
+ }));
+ }
+ }
+ render() {
+ const {
+ data,
+ dispatch,
+ type
+ } = this.props;
+ if (!data || !data.spocs || !data.spocs[0]) {
+ return null;
+ }
+ // Grab the first item in the array as we only have 1 spoc position.
+ const [spoc] = data.spocs;
+ const {
+ title,
+ url,
+ excerpt,
+ flight_id,
+ id,
+ shim
+ } = spoc;
+ const SIGNUP_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", ...(flight_id ? ["ShowPrivacyInfo"] : [])];
+ const outerClassName = ["ds-signup", this.state.active && "active", this.state.lastItem && "last-item"].filter(v => v).join(" ");
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: outerClassName
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-signup-content"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "icon icon-small-spacer icon-mail"
+ }), /*#__PURE__*/external_React_default().createElement("span", null, title, " ", /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ className: "ds-chevron-link",
+ dispatch: dispatch,
+ onLinkClick: this.onLinkClick,
+ url: url
+ }, excerpt)), /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, {
+ flightId: flight_id,
+ rows: [{
+ id,
+ pos: 0,
+ shim: shim && shim.impression
+ }],
+ dispatch: dispatch,
+ source: type
+ })), /*#__PURE__*/external_React_default().createElement(ContextMenuButton, {
+ tooltip: "newtab-menu-content-tooltip",
+ tooltipArgs: {
+ title
+ },
+ onUpdate: this.onMenuButtonUpdate
+ }, /*#__PURE__*/external_React_default().createElement(LinkMenu, {
+ dispatch: dispatch,
+ index: 0,
+ source: type.toUpperCase(),
+ onShow: this.onMenuShow,
+ options: SIGNUP_CONTEXT_MENU_OPTIONS,
+ shouldSendImpressionStats: true,
+ userEvent: actionCreators.DiscoveryStreamUserEvent,
+ site: {
+ referrer: "https://getpocket.com/recommendations",
+ title,
+ type,
+ url,
+ guid: id,
+ shim,
+ flight_id
+ }
+ })));
+ }
+}
+DSSignup.defaultProps = {
+ windowObj: window // Added to support unit tests
+};
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+class DSTextPromo extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onLinkClick = this.onLinkClick.bind(this);
+ this.onDismissClick = this.onDismissClick.bind(this);
+ }
+ onLinkClick() {
+ const {
+ data
+ } = this.props;
+ if (this.props.dispatch && data && data.spocs && data.spocs.length) {
+ const source = this.props.type.toUpperCase();
+ // Grab the first item in the array as we only have 1 spoc position.
+ const [spoc] = data.spocs;
+ this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source,
+ action_position: 0
+ }));
+ this.props.dispatch(actionCreators.ImpressionStats({
+ source,
+ click: 0,
+ tiles: [{
+ id: spoc.id,
+ pos: 0,
+ ...(spoc.shim && spoc.shim.click ? {
+ shim: spoc.shim.click
+ } : {})
+ }]
+ }));
+ }
+ }
+ onDismissClick() {
+ const {
+ data
+ } = this.props;
+ if (this.props.dispatch && data && data.spocs && data.spocs.length) {
+ const index = 0;
+ const source = this.props.type.toUpperCase();
+ // Grab the first item in the array as we only have 1 spoc position.
+ const [spoc] = data.spocs;
+ const spocData = {
+ url: spoc.url,
+ guid: spoc.id,
+ shim: spoc.shim
+ };
+ const blockUrlOption = LinkMenuOptions.BlockUrl(spocData, index, source);
+ const {
+ action,
+ impression,
+ userEvent
+ } = blockUrlOption;
+ this.props.dispatch(action);
+ this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: userEvent,
+ source,
+ action_position: index
+ }));
+ if (impression) {
+ this.props.dispatch(impression);
+ }
+ }
+ }
+ render() {
+ const {
+ data
+ } = this.props;
+ if (!data || !data.spocs || !data.spocs[0]) {
+ return null;
+ }
+ // Grab the first item in the array as we only have 1 spoc position.
+ const [spoc] = data.spocs;
+ const {
+ image_src,
+ raw_image_src,
+ alt_text,
+ title,
+ url,
+ context,
+ cta,
+ flight_id,
+ id,
+ shim
+ } = spoc;
+ return /*#__PURE__*/external_React_default().createElement(DSDismiss, {
+ onDismissClick: this.onDismissClick,
+ extraClasses: `ds-dismiss-ds-text-promo`
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-text-promo"
+ }, /*#__PURE__*/external_React_default().createElement(DSImage, {
+ alt_text: alt_text,
+ source: image_src,
+ rawSource: raw_image_src
+ }), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "text"
+ }, /*#__PURE__*/external_React_default().createElement("h3", null, `${title}\u2003`, /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ className: "ds-chevron-link",
+ dispatch: this.props.dispatch,
+ onLinkClick: this.onLinkClick,
+ url: url
+ }, cta)), /*#__PURE__*/external_React_default().createElement("p", {
+ className: "subtitle"
+ }, context)), /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, {
+ flightId: flight_id,
+ rows: [{
+ id,
+ pos: 0,
+ shim: shim && shim.impression
+ }],
+ dispatch: this.props.dispatch,
+ source: this.props.type
+ })));
+ }
+}
+;// CONCATENATED MODULE: ./content-src/lib/screenshot-utils.js
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * List of helper functions for screenshot-based images.
+ *
+ * There are two kinds of images:
+ * 1. Remote Image: This is the image from the main process and it refers to
+ * the image in the React props. This can either be an object with the `data`
+ * and `path` properties, if it is a blob, or a string, if it is a normal image.
+ * 2. Local Image: This is the image object in the content process and it refers
+ * to the image *object* in the React component's state. All local image
+ * objects have the `url` property, and an additional property `path`, if they
+ * are blobs.
+ */
+const ScreenshotUtils = {
+ isBlob(isLocal, image) {
+ return !!(image && image.path && (!isLocal && image.data || isLocal && image.url));
+ },
+ // This should always be called with a remote image and not a local image.
+ createLocalImageObject(remoteImage) {
+ if (!remoteImage) {
+ return null;
+ }
+ if (this.isBlob(false, remoteImage)) {
+ return {
+ url: __webpack_require__.g.URL.createObjectURL(remoteImage.data),
+ path: remoteImage.path
+ };
+ }
+ return {
+ url: remoteImage
+ };
+ },
+ // Revokes the object URL of the image if the local image is a blob.
+ // This should always be called with a local image and not a remote image.
+ maybeRevokeBlobObjectURL(localImage) {
+ if (this.isBlob(true, localImage)) {
+ __webpack_require__.g.URL.revokeObjectURL(localImage.url);
+ }
+ },
+ // Checks if remoteImage and localImage are the same.
+ isRemoteImageLocal(localImage, remoteImage) {
+ // Both remoteImage and localImage are present.
+ if (remoteImage && localImage) {
+ return this.isBlob(false, remoteImage) ? localImage.path === remoteImage.path : localImage.url === remoteImage;
+ }
+
+ // This will only handle the remaining three possible outcomes.
+ // (i.e. everything except when both image and localImage are present)
+ return !remoteImage && !localImage;
+ }
+};
+;// CONCATENATED MODULE: ./content-src/components/Card/Card.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+
+// Keep track of pending image loads to only request once
+const gImageLoading = new Map();
+
+/**
+ * Card component.
+ * Cards are found within a Section component and contain information about a link such
+ * as preview image, page title, page description, and some context about if the page
+ * was visited, bookmarked, trending etc...
+ * Each Section can make an unordered list of Cards which will create one instane of
+ * this class. Each card will then get a context menu which reflects the actions that
+ * can be done on this Card.
+ */
+class _Card extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ activeCard: null,
+ imageLoaded: false,
+ cardImage: null
+ };
+ this.onMenuButtonUpdate = this.onMenuButtonUpdate.bind(this);
+ this.onLinkClick = this.onLinkClick.bind(this);
+ }
+
+ /**
+ * Helper to conditionally load an image and update state when it loads.
+ */
+ async maybeLoadImage() {
+ // No need to load if it's already loaded or no image
+ const {
+ cardImage
+ } = this.state;
+ if (!cardImage) {
+ return;
+ }
+ const imageUrl = cardImage.url;
+ if (!this.state.imageLoaded) {
+ // Initialize a promise to share a load across multiple card updates
+ if (!gImageLoading.has(imageUrl)) {
+ const loaderPromise = new Promise((resolve, reject) => {
+ const loader = new Image();
+ loader.addEventListener("load", resolve);
+ loader.addEventListener("error", reject);
+ loader.src = imageUrl;
+ });
+
+ // Save and remove the promise only while it's pending
+ gImageLoading.set(imageUrl, loaderPromise);
+ loaderPromise.catch(ex => ex).then(() => gImageLoading.delete(imageUrl)).catch();
+ }
+
+ // Wait for the image whether just started loading or reused promise
+ try {
+ await gImageLoading.get(imageUrl);
+ } catch (ex) {
+ // Ignore the failed image without changing state
+ return;
+ }
+
+ // Only update state if we're still waiting to load the original image
+ if (ScreenshotUtils.isRemoteImageLocal(this.state.cardImage, this.props.link.image) && !this.state.imageLoaded) {
+ this.setState({
+ imageLoaded: true
+ });
+ }
+ }
+ }
+
+ /**
+ * Helper to obtain the next state based on nextProps and prevState.
+ *
+ * NOTE: Rename this method to getDerivedStateFromProps when we update React
+ * to >= 16.3. We will need to update tests as well. We cannot rename this
+ * method to getDerivedStateFromProps now because there is a mismatch in
+ * the React version that we are using for both testing and production.
+ * (i.e. react-test-render => "16.3.2", react => "16.2.0").
+ *
+ * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43.
+ */
+ static getNextStateFromProps(nextProps, prevState) {
+ const {
+ image
+ } = nextProps.link;
+ const imageInState = ScreenshotUtils.isRemoteImageLocal(prevState.cardImage, image);
+ let nextState = null;
+
+ // Image is updating.
+ if (!imageInState && nextProps.link) {
+ nextState = {
+ imageLoaded: false
+ };
+ }
+ if (imageInState) {
+ return nextState;
+ }
+
+ // Since image was updated, attempt to revoke old image blob URL, if it exists.
+ ScreenshotUtils.maybeRevokeBlobObjectURL(prevState.cardImage);
+ nextState = nextState || {};
+ nextState.cardImage = ScreenshotUtils.createLocalImageObject(image);
+ return nextState;
+ }
+ onMenuButtonUpdate(isOpen) {
+ if (isOpen) {
+ this.setState({
+ activeCard: this.props.index
+ });
+ } else {
+ this.setState({
+ activeCard: null
+ });
+ }
+ }
+
+ /**
+ * Report to telemetry additional information about the item.
+ */
+ _getTelemetryInfo() {
+ // Filter out "history" type for being the default
+ if (this.props.link.type !== "history") {
+ return {
+ value: {
+ card_type: this.props.link.type
+ }
+ };
+ }
+ return null;
+ }
+ onLinkClick(event) {
+ event.preventDefault();
+ const {
+ altKey,
+ button,
+ ctrlKey,
+ metaKey,
+ shiftKey
+ } = event;
+ if (this.props.link.type === "download") {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.OPEN_DOWNLOAD_FILE,
+ data: Object.assign(this.props.link, {
+ event: {
+ button,
+ ctrlKey,
+ metaKey,
+ shiftKey
+ }
+ })
+ }));
+ } else {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.OPEN_LINK,
+ data: Object.assign(this.props.link, {
+ event: {
+ altKey,
+ button,
+ ctrlKey,
+ metaKey,
+ shiftKey
+ }
+ })
+ }));
+ }
+ if (this.props.isWebExtension) {
+ this.props.dispatch(actionCreators.WebExtEvent(actionTypes.WEBEXT_CLICK, {
+ source: this.props.eventSource,
+ url: this.props.link.url,
+ action_position: this.props.index
+ }));
+ } else {
+ this.props.dispatch(actionCreators.UserEvent(Object.assign({
+ event: "CLICK",
+ source: this.props.eventSource,
+ action_position: this.props.index
+ }, this._getTelemetryInfo())));
+ if (this.props.shouldSendImpressionStats) {
+ this.props.dispatch(actionCreators.ImpressionStats({
+ source: this.props.eventSource,
+ click: 0,
+ tiles: [{
+ id: this.props.link.guid,
+ pos: this.props.index
+ }]
+ }));
+ }
+ }
+ }
+ componentDidMount() {
+ this.maybeLoadImage();
+ }
+ componentDidUpdate() {
+ this.maybeLoadImage();
+ }
+
+ // NOTE: Remove this function when we update React to >= 16.3 since React will
+ // call getDerivedStateFromProps automatically. We will also need to
+ // rename getNextStateFromProps to getDerivedStateFromProps.
+ componentWillMount() {
+ const nextState = _Card.getNextStateFromProps(this.props, this.state);
+ if (nextState) {
+ this.setState(nextState);
+ }
+ }
+
+ // NOTE: Remove this function when we update React to >= 16.3 since React will
+ // call getDerivedStateFromProps automatically. We will also need to
+ // rename getNextStateFromProps to getDerivedStateFromProps.
+ componentWillReceiveProps(nextProps) {
+ const nextState = _Card.getNextStateFromProps(nextProps, this.state);
+ if (nextState) {
+ this.setState(nextState);
+ }
+ }
+ componentWillUnmount() {
+ ScreenshotUtils.maybeRevokeBlobObjectURL(this.state.cardImage);
+ }
+ render() {
+ const {
+ index,
+ className,
+ link,
+ dispatch,
+ contextMenuOptions,
+ eventSource,
+ shouldSendImpressionStats
+ } = this.props;
+ const {
+ props
+ } = this;
+ const title = link.title || link.hostname;
+ const isContextMenuOpen = this.state.activeCard === index;
+ // Display "now" as "trending" until we have new strings #3402
+ const {
+ icon,
+ fluentID
+ } = cardContextTypes[link.type === "now" ? "trending" : link.type] || {};
+ const hasImage = this.state.cardImage || link.hasImage;
+ const imageStyle = {
+ backgroundImage: this.state.cardImage ? `url(${this.state.cardImage.url})` : "none"
+ };
+ const outerClassName = ["card-outer", className, isContextMenuOpen && "active", props.placeholder && "placeholder"].filter(v => v).join(" ");
+ return /*#__PURE__*/external_React_default().createElement("li", {
+ className: outerClassName
+ }, /*#__PURE__*/external_React_default().createElement("a", {
+ href: link.type === "pocket" ? link.open_url : link.url,
+ onClick: !props.placeholder ? this.onLinkClick : undefined
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card-preview-image-outer"
+ }, hasImage && /*#__PURE__*/external_React_default().createElement("div", {
+ className: `card-preview-image${this.state.imageLoaded ? " loaded" : ""}`,
+ style: imageStyle
+ })), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card-details"
+ }, link.type === "download" && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card-host-name alternate",
+ "data-l10n-id": "newtab-menu-open-file"
+ }), link.hostname && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card-host-name"
+ }, link.hostname.slice(0, 100), link.type === "download" && ` \u2014 ${link.description}`), /*#__PURE__*/external_React_default().createElement("div", {
+ className: ["card-text", icon ? "" : "no-context", link.description ? "" : "no-description", link.hostname ? "" : "no-host-name"].join(" ")
+ }, /*#__PURE__*/external_React_default().createElement("h4", {
+ className: "card-title",
+ dir: "auto"
+ }, link.title), /*#__PURE__*/external_React_default().createElement("p", {
+ className: "card-description",
+ dir: "auto"
+ }, link.description)), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card-context"
+ }, icon && !link.context && /*#__PURE__*/external_React_default().createElement("span", {
+ "aria-haspopup": "true",
+ className: `card-context-icon icon icon-${icon}`
+ }), link.icon && link.context && /*#__PURE__*/external_React_default().createElement("span", {
+ "aria-haspopup": "true",
+ className: "card-context-icon icon",
+ style: {
+ backgroundImage: `url('${link.icon}')`
+ }
+ }), fluentID && !link.context && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card-context-label",
+ "data-l10n-id": fluentID
+ }), link.context && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "card-context-label"
+ }, link.context))))), !props.placeholder && /*#__PURE__*/external_React_default().createElement(ContextMenuButton, {
+ tooltip: "newtab-menu-content-tooltip",
+ tooltipArgs: {
+ title
+ },
+ onUpdate: this.onMenuButtonUpdate
+ }, /*#__PURE__*/external_React_default().createElement(LinkMenu, {
+ dispatch: dispatch,
+ index: index,
+ source: eventSource,
+ options: link.contextMenuOptions || contextMenuOptions,
+ site: link,
+ siteInfo: this._getTelemetryInfo(),
+ shouldSendImpressionStats: shouldSendImpressionStats
+ })));
+ }
+}
+_Card.defaultProps = {
+ link: {}
+};
+const Card = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ platform: state.Prefs.values.platform
+}))(_Card);
+const PlaceholderCard = props => /*#__PURE__*/external_React_default().createElement(Card, {
+ placeholder: true,
+ className: props.className
+});
+;// CONCATENATED MODULE: ./content-src/lib/perf-service.js
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+let usablePerfObj = window.performance;
+function _PerfService(options) {
+ // For testing, so that we can use a fake Window.performance object with
+ // known state.
+ if (options && options.performanceObj) {
+ this._perf = options.performanceObj;
+ } else {
+ this._perf = usablePerfObj;
+ }
+}
+_PerfService.prototype = {
+ /**
+ * Calls the underlying mark() method on the appropriate Window.performance
+ * object to add a mark with the given name to the appropriate performance
+ * timeline.
+ *
+ * @param {String} name the name to give the current mark
+ * @return {void}
+ */
+ mark: function mark(str) {
+ this._perf.mark(str);
+ },
+ /**
+ * Calls the underlying getEntriesByName on the appropriate Window.performance
+ * object.
+ *
+ * @param {String} name
+ * @param {String} type eg "mark"
+ * @return {Array} Performance* objects
+ */
+ getEntriesByName: function getEntriesByName(name, type) {
+ return this._perf.getEntriesByName(name, type);
+ },
+ /**
+ * The timeOrigin property from the appropriate performance object.
+ * Used to ensure that timestamps from the add-on code and the content code
+ * are comparable.
+ *
+ * @note If this is called from a context without a window
+ * (eg a JSM in chrome), it will return the timeOrigin of the XUL hidden
+ * window, which appears to be the first created window (and thus
+ * timeOrigin) in the browser. Note also, however, there is also a private
+ * hidden window, presumably for private browsing, which appears to be
+ * created dynamically later. Exactly how/when that shows up needs to be
+ * investigated.
+ *
+ * @return {Number} A double of milliseconds with a precision of 0.5us.
+ */
+ get timeOrigin() {
+ return this._perf.timeOrigin;
+ },
+ /**
+ * Returns the "absolute" version of performance.now(), i.e. one that
+ * should ([bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)
+ * be comparable across both chrome and content.
+ *
+ * @return {Number}
+ */
+ absNow: function absNow() {
+ return this.timeOrigin + this._perf.now();
+ },
+ /**
+ * This returns the absolute startTime from the most recent performance.mark()
+ * with the given name.
+ *
+ * @param {String} name the name to lookup the start time for
+ *
+ * @return {Number} the returned start time, as a DOMHighResTimeStamp
+ *
+ * @throws {Error} "No Marks with the name ..." if none are available
+ *
+ * @note Always surround calls to this by try/catch. Otherwise your code
+ * may fail when the `privacy.resistFingerprinting` pref is true. When
+ * this pref is set, all attempts to get marks will likely fail, which will
+ * cause this method to throw.
+ *
+ * See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303)
+ * for more info.
+ */
+ getMostRecentAbsMarkStartByName(name) {
+ let entries = this.getEntriesByName(name, "mark");
+ if (!entries.length) {
+ throw new Error(`No marks with the name ${name}`);
+ }
+ let mostRecentEntry = entries[entries.length - 1];
+ return this._perf.timeOrigin + mostRecentEntry.startTime;
+ }
+};
+const perfService = new _PerfService();
+;// CONCATENATED MODULE: ./content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+// Currently record only a fixed set of sections. This will prevent data
+// from custom sections from showing up or from topstories.
+const RECORDED_SECTIONS = ["highlights", "topsites"];
+class ComponentPerfTimer extends (external_React_default()).Component {
+ constructor(props) {
+ super(props);
+ // Just for test dependency injection:
+ this.perfSvc = this.props.perfSvc || perfService;
+ this._sendBadStateEvent = this._sendBadStateEvent.bind(this);
+ this._sendPaintedEvent = this._sendPaintedEvent.bind(this);
+ this._reportMissingData = false;
+ this._timestampHandled = false;
+ this._recordedFirstRender = false;
+ }
+ componentDidMount() {
+ if (!RECORDED_SECTIONS.includes(this.props.id)) {
+ return;
+ }
+ this._maybeSendPaintedEvent();
+ }
+ componentDidUpdate() {
+ if (!RECORDED_SECTIONS.includes(this.props.id)) {
+ return;
+ }
+ this._maybeSendPaintedEvent();
+ }
+
+ /**
+ * Call the given callback after the upcoming frame paints.
+ *
+ * @note Both setTimeout and requestAnimationFrame are throttled when the page
+ * is hidden, so this callback may get called up to a second or so after the
+ * requestAnimationFrame "paint" for hidden tabs.
+ *
+ * Newtabs hidden while loading will presumably be fairly rare (other than
+ * preloaded tabs, which we will be filtering out on the server side), so such
+ * cases should get lost in the noise.
+ *
+ * If we decide that it's important to find out when something that's hidden
+ * has "painted", however, another option is to post a message to this window.
+ * That should happen even faster than setTimeout, and, at least as of this
+ * writing, it's not throttled in hidden windows in Firefox.
+ *
+ * @param {Function} callback
+ *
+ * @returns void
+ */
+ _afterFramePaint(callback) {
+ requestAnimationFrame(() => setTimeout(callback, 0));
+ }
+ _maybeSendBadStateEvent() {
+ // Follow up bugs:
+ // https://github.com/mozilla/activity-stream/issues/3691
+ if (!this.props.initialized) {
+ // Remember to report back when data is available.
+ this._reportMissingData = true;
+ } else if (this._reportMissingData) {
+ this._reportMissingData = false;
+ // Report how long it took for component to become initialized.
+ this._sendBadStateEvent();
+ }
+ }
+ _maybeSendPaintedEvent() {
+ // If we've already handled a timestamp, don't do it again.
+ if (this._timestampHandled || !this.props.initialized) {
+ return;
+ }
+
+ // And if we haven't, we're doing so now, so remember that. Even if
+ // something goes wrong in the callback, we can't try again, as we'd be
+ // sending back the wrong data, and we have to do it here, so that other
+ // calls to this method while waiting for the next frame won't also try to
+ // handle it.
+ this._timestampHandled = true;
+ this._afterFramePaint(this._sendPaintedEvent);
+ }
+
+ /**
+ * Triggered by call to render. Only first call goes through due to
+ * `_recordedFirstRender`.
+ */
+ _ensureFirstRenderTsRecorded() {
+ // Used as t0 for recording how long component took to initialize.
+ if (!this._recordedFirstRender) {
+ this._recordedFirstRender = true;
+ // topsites_first_render_ts, highlights_first_render_ts.
+ const key = `${this.props.id}_first_render_ts`;
+ this.perfSvc.mark(key);
+ }
+ }
+
+ /**
+ * Creates `SAVE_SESSION_PERF_DATA` with timestamp in ms
+ * of how much longer the data took to be ready for display than it would
+ * have been the ideal case.
+ * https://github.com/mozilla/ping-centre/issues/98
+ */
+ _sendBadStateEvent() {
+ // highlights_data_ready_ts, topsites_data_ready_ts.
+ const dataReadyKey = `${this.props.id}_data_ready_ts`;
+ this.perfSvc.mark(dataReadyKey);
+ try {
+ const firstRenderKey = `${this.props.id}_first_render_ts`;
+ // value has to be Int32.
+ const value = parseInt(this.perfSvc.getMostRecentAbsMarkStartByName(dataReadyKey) - this.perfSvc.getMostRecentAbsMarkStartByName(firstRenderKey), 10);
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.SAVE_SESSION_PERF_DATA,
+ // highlights_data_late_by_ms, topsites_data_late_by_ms.
+ data: {
+ [`${this.props.id}_data_late_by_ms`]: value
+ }
+ }));
+ } catch (ex) {
+ // If this failed, it's likely because the `privacy.resistFingerprinting`
+ // pref is true.
+ }
+ }
+ _sendPaintedEvent() {
+ // Record first_painted event but only send if topsites.
+ if (this.props.id !== "topsites") {
+ return;
+ }
+
+ // topsites_first_painted_ts.
+ const key = `${this.props.id}_first_painted_ts`;
+ this.perfSvc.mark(key);
+ try {
+ const data = {};
+ data[key] = this.perfSvc.getMostRecentAbsMarkStartByName(key);
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.SAVE_SESSION_PERF_DATA,
+ data
+ }));
+ } catch (ex) {
+ // If this failed, it's likely because the `privacy.resistFingerprinting`
+ // pref is true. We should at least not blow up, and should continue
+ // to set this._timestampHandled to avoid going through this again.
+ }
+ }
+ render() {
+ if (RECORDED_SECTIONS.includes(this.props.id)) {
+ this._ensureFirstRenderTsRecorded();
+ this._maybeSendBadStateEvent();
+ }
+ return this.props.children;
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/MoreRecommendations/MoreRecommendations.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class MoreRecommendations extends (external_React_default()).PureComponent {
+ render() {
+ const {
+ read_more_endpoint
+ } = this.props;
+ if (read_more_endpoint) {
+ return /*#__PURE__*/external_React_default().createElement("a", {
+ className: "more-recommendations",
+ href: read_more_endpoint,
+ "data-l10n-id": "newtab-pocket-more-recommendations"
+ });
+ }
+ return null;
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/PocketLoggedInCta/PocketLoggedInCta.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+class _PocketLoggedInCta extends (external_React_default()).PureComponent {
+ render() {
+ const {
+ pocketCta
+ } = this.props.Pocket;
+ return /*#__PURE__*/external_React_default().createElement("span", {
+ className: "pocket-logged-in-cta"
+ }, /*#__PURE__*/external_React_default().createElement("a", {
+ className: "pocket-cta-button",
+ href: pocketCta.ctaUrl ? pocketCta.ctaUrl : "https://getpocket.com/"
+ }, pocketCta.ctaButton ? pocketCta.ctaButton : /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": "newtab-pocket-cta-button"
+ })), /*#__PURE__*/external_React_default().createElement("a", {
+ href: pocketCta.ctaUrl ? pocketCta.ctaUrl : "https://getpocket.com/"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "cta-text"
+ }, pocketCta.ctaText ? pocketCta.ctaText : /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": "newtab-pocket-cta-text"
+ }))));
+ }
+}
+const PocketLoggedInCta = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Pocket: state.Pocket
+}))(_PocketLoggedInCta);
+;// CONCATENATED MODULE: ./content-src/components/Topics/Topics.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class Topic extends (external_React_default()).PureComponent {
+ render() {
+ const {
+ url,
+ name
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement("li", null, /*#__PURE__*/external_React_default().createElement("a", {
+ key: name,
+ href: url
+ }, name));
+ }
+}
+class Topics extends (external_React_default()).PureComponent {
+ render() {
+ const {
+ topics
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement("span", {
+ className: "topics"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": "newtab-pocket-read-more"
+ }), /*#__PURE__*/external_React_default().createElement("ul", null, topics && topics.map(t => /*#__PURE__*/external_React_default().createElement(Topic, {
+ key: t.name,
+ url: t.url,
+ name: t.name
+ }))));
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/TopSites/SearchShortcutsForm.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+class SelectableSearchShortcut extends (external_React_default()).PureComponent {
+ render() {
+ const {
+ shortcut,
+ selected
+ } = this.props;
+ const imageStyle = {
+ backgroundImage: `url("${shortcut.tippyTopIcon}")`
+ };
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "top-site-outer search-shortcut"
+ }, /*#__PURE__*/external_React_default().createElement("input", {
+ type: "checkbox",
+ id: shortcut.keyword,
+ name: shortcut.keyword,
+ checked: selected,
+ onChange: this.props.onChange
+ }), /*#__PURE__*/external_React_default().createElement("label", {
+ htmlFor: shortcut.keyword
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "top-site-inner"
+ }, /*#__PURE__*/external_React_default().createElement("span", null, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "tile"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "top-site-icon rich-icon",
+ style: imageStyle,
+ "data-fallback": "@"
+ }), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "top-site-icon search-topsite"
+ })), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "title"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ dir: "auto"
+ }, shortcut.keyword))))));
+ }
+}
+class SearchShortcutsForm extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.handleChange = this.handleChange.bind(this);
+ this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
+ this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
+
+ // clone the shortcuts and add them to the state so we can add isSelected property
+ const shortcuts = [];
+ const {
+ rows,
+ searchShortcuts
+ } = props.TopSites;
+ searchShortcuts.forEach(shortcut => {
+ shortcuts.push({
+ ...shortcut,
+ isSelected: !!rows.find(row => row && row.isPinned && row.searchTopSite && row.label === shortcut.keyword)
+ });
+ });
+ this.state = {
+ shortcuts
+ };
+ }
+ handleChange(event) {
+ const {
+ target
+ } = event;
+ const {
+ name,
+ checked
+ } = target;
+ this.setState(prevState => {
+ const shortcuts = prevState.shortcuts.slice();
+ let shortcut = shortcuts.find(({
+ keyword
+ }) => keyword === name);
+ shortcut.isSelected = checked;
+ return {
+ shortcuts
+ };
+ });
+ }
+ onCancelButtonClick(ev) {
+ ev.preventDefault();
+ this.props.onClose();
+ }
+ onSaveButtonClick(ev) {
+ ev.preventDefault();
+
+ // Check if there were any changes and act accordingly
+ const {
+ rows
+ } = this.props.TopSites;
+ const pinQueue = [];
+ const unpinQueue = [];
+ this.state.shortcuts.forEach(shortcut => {
+ const alreadyPinned = rows.find(row => row && row.isPinned && row.searchTopSite && row.label === shortcut.keyword);
+ if (shortcut.isSelected && !alreadyPinned) {
+ pinQueue.push(this._searchTopSite(shortcut));
+ } else if (!shortcut.isSelected && alreadyPinned) {
+ unpinQueue.push({
+ url: alreadyPinned.url,
+ searchVendor: shortcut.shortURL
+ });
+ }
+ });
+
+ // Tell the feed to do the work.
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.UPDATE_PINNED_SEARCH_SHORTCUTS,
+ data: {
+ addedShortcuts: pinQueue,
+ deletedShortcuts: unpinQueue
+ }
+ }));
+
+ // Send the Telemetry pings.
+ pinQueue.forEach(shortcut => {
+ this.props.dispatch(actionCreators.UserEvent({
+ source: TOP_SITES_SOURCE,
+ event: "SEARCH_EDIT_ADD",
+ value: {
+ search_vendor: shortcut.searchVendor
+ }
+ }));
+ });
+ unpinQueue.forEach(shortcut => {
+ this.props.dispatch(actionCreators.UserEvent({
+ source: TOP_SITES_SOURCE,
+ event: "SEARCH_EDIT_DELETE",
+ value: {
+ search_vendor: shortcut.searchVendor
+ }
+ }));
+ });
+ this.props.onClose();
+ }
+ _searchTopSite(shortcut) {
+ return {
+ url: shortcut.url,
+ searchTopSite: true,
+ label: shortcut.keyword,
+ searchVendor: shortcut.shortURL
+ };
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("form", {
+ className: "topsite-form"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "search-shortcuts-container"
+ }, /*#__PURE__*/external_React_default().createElement("h3", {
+ className: "section-title grey-title",
+ "data-l10n-id": "newtab-topsites-add-search-engine-header"
+ }), /*#__PURE__*/external_React_default().createElement("div", null, this.state.shortcuts.map(shortcut => /*#__PURE__*/external_React_default().createElement(SelectableSearchShortcut, {
+ key: shortcut.keyword,
+ shortcut: shortcut,
+ selected: shortcut.isSelected,
+ onChange: this.handleChange
+ })))), /*#__PURE__*/external_React_default().createElement("section", {
+ className: "actions"
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ className: "cancel",
+ type: "button",
+ onClick: this.onCancelButtonClick,
+ "data-l10n-id": "newtab-topsites-cancel-button"
+ }), /*#__PURE__*/external_React_default().createElement("button", {
+ className: "done",
+ type: "submit",
+ onClick: this.onSaveButtonClick,
+ "data-l10n-id": "newtab-topsites-save-button"
+ })));
+ }
+}
+;// CONCATENATED MODULE: ./common/Dedupe.sys.mjs
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+class Dedupe {
+ constructor(createKey) {
+ this.createKey = createKey || this.defaultCreateKey;
+ }
+
+ defaultCreateKey(item) {
+ return item;
+ }
+
+ /**
+ * Dedupe any number of grouped elements favoring those from earlier groups.
+ *
+ * @param {Array} groups Contains an arbitrary number of arrays of elements.
+ * @returns {Array} A matching array of each provided group deduped.
+ */
+ group(...groups) {
+ const globalKeys = new Set();
+ const result = [];
+ for (const values of groups) {
+ const valueMap = new Map();
+ for (const value of values) {
+ const key = this.createKey(value);
+ if (!globalKeys.has(key) && !valueMap.has(key)) {
+ valueMap.set(key, value);
+ }
+ }
+ result.push(valueMap);
+ valueMap.forEach((value, key) => globalKeys.add(key));
+ }
+ return result.map(m => Array.from(m.values()));
+ }
+}
+
+;// CONCATENATED MODULE: ./common/Reducers.sys.mjs
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+const TOP_SITES_DEFAULT_ROWS = 1;
+const TOP_SITES_MAX_SITES_PER_ROW = 8;
+const PREF_COLLECTION_DISMISSIBLE = "discoverystream.isCollectionDismissible";
+
+const dedupe = new Dedupe(site => site && site.url);
+
+const INITIAL_STATE = {
+ App: {
+ // Have we received real data from the app yet?
+ initialized: false,
+ locale: "",
+ isForStartupCache: false,
+ customizeMenuVisible: false,
+ },
+ ASRouter: { initialized: false },
+ TopSites: {
+ // Have we received real data from history yet?
+ initialized: false,
+ // The history (and possibly default) links
+ rows: [],
+ // Used in content only to dispatch action to TopSiteForm.
+ editForm: null,
+ // Used in content only to open the SearchShortcutsForm modal.
+ showSearchShortcutsForm: false,
+ // The list of available search shortcuts.
+ searchShortcuts: [],
+ // The "Share-of-Voice" allocations generated by TopSitesFeed
+ sov: {
+ ready: false,
+ positions: [
+ // {position: 0, assignedPartner: "amp"},
+ // {position: 1, assignedPartner: "moz-sales"},
+ ],
+ },
+ },
+ Prefs: {
+ initialized: false,
+ values: { featureConfig: {} },
+ },
+ Dialog: {
+ visible: false,
+ data: {},
+ },
+ Sections: [],
+ Pocket: {
+ isUserLoggedIn: null,
+ pocketCta: {},
+ waitingForSpoc: true,
+ },
+ // This is the new pocket configurable layout state.
+ DiscoveryStream: {
+ // This is a JSON-parsed copy of the discoverystream.config pref value.
+ config: { enabled: false },
+ layout: [],
+ isPrivacyInfoModalVisible: false,
+ isCollectionDismissible: false,
+ feeds: {
+ data: {
+ // "https://foo.com/feed1": {lastUpdated: 123, data: [], personalized: false}
+ },
+ loaded: false,
+ },
+ spocs: {
+ spocs_endpoint: "",
+ lastUpdated: null,
+ data: {
+ // "spocs": {title: "", context: "", items: [], personalized: false},
+ // "placement1": {title: "", context: "", items: [], personalized: false},
+ },
+ loaded: false,
+ frequency_caps: [],
+ blocked: [],
+ placements: [],
+ },
+ experimentData: {
+ utmSource: "pocket-newtab",
+ utmCampaign: undefined,
+ utmContent: undefined,
+ },
+ recentSavesData: [],
+ isUserLoggedIn: false,
+ recentSavesEnabled: false,
+ },
+ Personalization: {
+ lastUpdated: null,
+ initialized: false,
+ },
+ Search: {
+ // When search hand-off is enabled, we render a big button that is styled to
+ // look like a search textbox. If the button is clicked, we style
+ // the button as if it was a focused search box and show a fake cursor but
+ // really focus the awesomebar without the focus styles ("hidden focus").
+ fakeFocus: false,
+ // Hide the search box after handing off to AwesomeBar and user starts typing.
+ hide: false,
+ },
+};
+
+function App(prevState = INITIAL_STATE.App, action) {
+ switch (action.type) {
+ case actionTypes.INIT:
+ return Object.assign({}, prevState, action.data || {}, {
+ initialized: true,
+ });
+ case actionTypes.TOP_SITES_UPDATED:
+ // Toggle `isForStartupCache` when receiving the `TOP_SITES_UPDATE` action
+ // so that sponsored tiles can be rendered as usual. See Bug 1826360.
+ return Object.assign({}, prevState, action.data || {}, {
+ isForStartupCache: false,
+ });
+ case actionTypes.SHOW_PERSONALIZE:
+ return Object.assign({}, prevState, {
+ customizeMenuVisible: true,
+ });
+ case actionTypes.HIDE_PERSONALIZE:
+ return Object.assign({}, prevState, {
+ customizeMenuVisible: false,
+ });
+ default:
+ return prevState;
+ }
+}
+
+function ASRouter(prevState = INITIAL_STATE.ASRouter, action) {
+ switch (action.type) {
+ case actionTypes.AS_ROUTER_INITIALIZED:
+ return { ...action.data, initialized: true };
+ default:
+ return prevState;
+ }
+}
+
+/**
+ * insertPinned - Inserts pinned links in their specified slots
+ *
+ * @param {array} a list of links
+ * @param {array} a list of pinned links
+ * @return {array} resulting list of links with pinned links inserted
+ */
+function insertPinned(links, pinned) {
+ // Remove any pinned links
+ const pinnedUrls = pinned.map(link => link && link.url);
+ let newLinks = links.filter(link =>
+ link ? !pinnedUrls.includes(link.url) : false
+ );
+ newLinks = newLinks.map(link => {
+ if (link && link.isPinned) {
+ delete link.isPinned;
+ delete link.pinIndex;
+ }
+ return link;
+ });
+
+ // Then insert them in their specified location
+ pinned.forEach((val, index) => {
+ if (!val) {
+ return;
+ }
+ let link = Object.assign({}, val, { isPinned: true, pinIndex: index });
+ if (index > newLinks.length) {
+ newLinks[index] = link;
+ } else {
+ newLinks.splice(index, 0, link);
+ }
+ });
+
+ return newLinks;
+}
+
+function TopSites(prevState = INITIAL_STATE.TopSites, action) {
+ let hasMatch;
+ let newRows;
+ switch (action.type) {
+ case actionTypes.TOP_SITES_UPDATED:
+ if (!action.data || !action.data.links) {
+ return prevState;
+ }
+ return Object.assign(
+ {},
+ prevState,
+ { initialized: true, rows: action.data.links },
+ action.data.pref ? { pref: action.data.pref } : {}
+ );
+ case actionTypes.TOP_SITES_PREFS_UPDATED:
+ return Object.assign({}, prevState, { pref: action.data.pref });
+ case actionTypes.TOP_SITES_EDIT:
+ return Object.assign({}, prevState, {
+ editForm: {
+ index: action.data.index,
+ previewResponse: null,
+ },
+ });
+ case actionTypes.TOP_SITES_CANCEL_EDIT:
+ return Object.assign({}, prevState, { editForm: null });
+ case actionTypes.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL:
+ return Object.assign({}, prevState, { showSearchShortcutsForm: true });
+ case actionTypes.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL:
+ return Object.assign({}, prevState, { showSearchShortcutsForm: false });
+ case actionTypes.PREVIEW_RESPONSE:
+ if (
+ !prevState.editForm ||
+ action.data.url !== prevState.editForm.previewUrl
+ ) {
+ return prevState;
+ }
+ return Object.assign({}, prevState, {
+ editForm: {
+ index: prevState.editForm.index,
+ previewResponse: action.data.preview,
+ previewUrl: action.data.url,
+ },
+ });
+ case actionTypes.PREVIEW_REQUEST:
+ if (!prevState.editForm) {
+ return prevState;
+ }
+ return Object.assign({}, prevState, {
+ editForm: {
+ index: prevState.editForm.index,
+ previewResponse: null,
+ previewUrl: action.data.url,
+ },
+ });
+ case actionTypes.PREVIEW_REQUEST_CANCEL:
+ if (!prevState.editForm) {
+ return prevState;
+ }
+ return Object.assign({}, prevState, {
+ editForm: {
+ index: prevState.editForm.index,
+ previewResponse: null,
+ },
+ });
+ case actionTypes.SCREENSHOT_UPDATED:
+ newRows = prevState.rows.map(row => {
+ if (row && row.url === action.data.url) {
+ hasMatch = true;
+ return Object.assign({}, row, { screenshot: action.data.screenshot });
+ }
+ return row;
+ });
+ return hasMatch
+ ? Object.assign({}, prevState, { rows: newRows })
+ : prevState;
+ case actionTypes.PLACES_BOOKMARK_ADDED:
+ if (!action.data) {
+ return prevState;
+ }
+ newRows = prevState.rows.map(site => {
+ if (site && site.url === action.data.url) {
+ const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
+ return Object.assign({}, site, {
+ bookmarkGuid,
+ bookmarkTitle,
+ bookmarkDateCreated: dateAdded,
+ });
+ }
+ return site;
+ });
+ return Object.assign({}, prevState, { rows: newRows });
+ case actionTypes.PLACES_BOOKMARKS_REMOVED:
+ if (!action.data) {
+ return prevState;
+ }
+ newRows = prevState.rows.map(site => {
+ if (site && action.data.urls.includes(site.url)) {
+ const newSite = Object.assign({}, site);
+ delete newSite.bookmarkGuid;
+ delete newSite.bookmarkTitle;
+ delete newSite.bookmarkDateCreated;
+ return newSite;
+ }
+ return site;
+ });
+ return Object.assign({}, prevState, { rows: newRows });
+ case actionTypes.PLACES_LINKS_DELETED:
+ if (!action.data) {
+ return prevState;
+ }
+ newRows = prevState.rows.filter(
+ site => !action.data.urls.includes(site.url)
+ );
+ return Object.assign({}, prevState, { rows: newRows });
+ case actionTypes.UPDATE_SEARCH_SHORTCUTS:
+ return { ...prevState, searchShortcuts: action.data.searchShortcuts };
+ case actionTypes.SOV_UPDATED:
+ const sov = {
+ ready: action.data.ready,
+ positions: action.data.positions,
+ };
+ return { ...prevState, sov };
+ default:
+ return prevState;
+ }
+}
+
+function Dialog(prevState = INITIAL_STATE.Dialog, action) {
+ switch (action.type) {
+ case actionTypes.DIALOG_OPEN:
+ return Object.assign({}, prevState, { visible: true, data: action.data });
+ case actionTypes.DIALOG_CANCEL:
+ return Object.assign({}, prevState, { visible: false });
+ case actionTypes.DELETE_HISTORY_URL:
+ return Object.assign({}, INITIAL_STATE.Dialog);
+ default:
+ return prevState;
+ }
+}
+
+function Prefs(prevState = INITIAL_STATE.Prefs, action) {
+ let newValues;
+ switch (action.type) {
+ case actionTypes.PREFS_INITIAL_VALUES:
+ return Object.assign({}, prevState, {
+ initialized: true,
+ values: action.data,
+ });
+ case actionTypes.PREF_CHANGED:
+ newValues = Object.assign({}, prevState.values);
+ newValues[action.data.name] = action.data.value;
+ return Object.assign({}, prevState, { values: newValues });
+ default:
+ return prevState;
+ }
+}
+
+function Sections(prevState = INITIAL_STATE.Sections, action) {
+ let hasMatch;
+ let newState;
+ switch (action.type) {
+ case actionTypes.SECTION_DEREGISTER:
+ return prevState.filter(section => section.id !== action.data);
+ case actionTypes.SECTION_REGISTER:
+ // If section exists in prevState, update it
+ newState = prevState.map(section => {
+ if (section && section.id === action.data.id) {
+ hasMatch = true;
+ return Object.assign({}, section, action.data);
+ }
+ return section;
+ });
+ // Otherwise, append it
+ if (!hasMatch) {
+ const initialized = !!(action.data.rows && !!action.data.rows.length);
+ const section = Object.assign(
+ { title: "", rows: [], enabled: false },
+ action.data,
+ { initialized }
+ );
+ newState.push(section);
+ }
+ return newState;
+ case actionTypes.SECTION_UPDATE:
+ newState = prevState.map(section => {
+ if (section && section.id === action.data.id) {
+ // If the action is updating rows, we should consider initialized to be true.
+ // This can be overridden if initialized is defined in the action.data
+ const initialized = action.data.rows ? { initialized: true } : {};
+
+ // Make sure pinned cards stay at their current position when rows are updated.
+ // Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards.
+ if (
+ action.data.rows &&
+ !!action.data.rows.length &&
+ section.rows.find(card => card.pinned)
+ ) {
+ const rows = Array.from(action.data.rows);
+ section.rows.forEach((card, index) => {
+ if (card.pinned) {
+ // Only add it if it's not already there.
+ if (rows[index].guid !== card.guid) {
+ rows.splice(index, 0, card);
+ }
+ }
+ });
+ return Object.assign(
+ {},
+ section,
+ initialized,
+ Object.assign({}, action.data, { rows })
+ );
+ }
+
+ return Object.assign({}, section, initialized, action.data);
+ }
+ return section;
+ });
+
+ if (!action.data.dedupeConfigurations) {
+ return newState;
+ }
+
+ action.data.dedupeConfigurations.forEach(dedupeConf => {
+ newState = newState.map(section => {
+ if (section.id === dedupeConf.id) {
+ const dedupedRows = dedupeConf.dedupeFrom.reduce(
+ (rows, dedupeSectionId) => {
+ const dedupeSection = newState.find(
+ s => s.id === dedupeSectionId
+ );
+ const [, newRows] = dedupe.group(dedupeSection.rows, rows);
+ return newRows;
+ },
+ section.rows
+ );
+
+ return Object.assign({}, section, { rows: dedupedRows });
+ }
+
+ return section;
+ });
+ });
+
+ return newState;
+ case actionTypes.SECTION_UPDATE_CARD:
+ return prevState.map(section => {
+ if (section && section.id === action.data.id && section.rows) {
+ const newRows = section.rows.map(card => {
+ if (card.url === action.data.url) {
+ return Object.assign({}, card, action.data.options);
+ }
+ return card;
+ });
+ return Object.assign({}, section, { rows: newRows });
+ }
+ return section;
+ });
+ case actionTypes.PLACES_BOOKMARK_ADDED:
+ if (!action.data) {
+ return prevState;
+ }
+ return prevState.map(section =>
+ Object.assign({}, section, {
+ rows: section.rows.map(item => {
+ // find the item within the rows that is attempted to be bookmarked
+ if (item.url === action.data.url) {
+ const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
+ return Object.assign({}, item, {
+ bookmarkGuid,
+ bookmarkTitle,
+ bookmarkDateCreated: dateAdded,
+ type: "bookmark",
+ });
+ }
+ return item;
+ }),
+ })
+ );
+ case actionTypes.PLACES_SAVED_TO_POCKET:
+ if (!action.data) {
+ return prevState;
+ }
+ return prevState.map(section =>
+ Object.assign({}, section, {
+ rows: section.rows.map(item => {
+ if (item.url === action.data.url) {
+ return Object.assign({}, item, {
+ open_url: action.data.open_url,
+ pocket_id: action.data.pocket_id,
+ title: action.data.title,
+ type: "pocket",
+ });
+ }
+ return item;
+ }),
+ })
+ );
+ case actionTypes.PLACES_BOOKMARKS_REMOVED:
+ if (!action.data) {
+ return prevState;
+ }
+ return prevState.map(section =>
+ Object.assign({}, section, {
+ rows: section.rows.map(item => {
+ // find the bookmark within the rows that is attempted to be removed
+ if (action.data.urls.includes(item.url)) {
+ const newSite = Object.assign({}, item);
+ delete newSite.bookmarkGuid;
+ delete newSite.bookmarkTitle;
+ delete newSite.bookmarkDateCreated;
+ if (!newSite.type || newSite.type === "bookmark") {
+ newSite.type = "history";
+ }
+ return newSite;
+ }
+ return item;
+ }),
+ })
+ );
+ case actionTypes.PLACES_LINKS_DELETED:
+ if (!action.data) {
+ return prevState;
+ }
+ return prevState.map(section =>
+ Object.assign({}, section, {
+ rows: section.rows.filter(
+ site => !action.data.urls.includes(site.url)
+ ),
+ })
+ );
+ case actionTypes.PLACES_LINK_BLOCKED:
+ if (!action.data) {
+ return prevState;
+ }
+ return prevState.map(section =>
+ Object.assign({}, section, {
+ rows: section.rows.filter(site => site.url !== action.data.url),
+ })
+ );
+ case actionTypes.DELETE_FROM_POCKET:
+ case actionTypes.ARCHIVE_FROM_POCKET:
+ return prevState.map(section =>
+ Object.assign({}, section, {
+ rows: section.rows.filter(
+ site => site.pocket_id !== action.data.pocket_id
+ ),
+ })
+ );
+ default:
+ return prevState;
+ }
+}
+
+function Pocket(prevState = INITIAL_STATE.Pocket, action) {
+ switch (action.type) {
+ case actionTypes.POCKET_WAITING_FOR_SPOC:
+ return { ...prevState, waitingForSpoc: action.data };
+ case actionTypes.POCKET_LOGGED_IN:
+ return { ...prevState, isUserLoggedIn: !!action.data };
+ case actionTypes.POCKET_CTA:
+ return {
+ ...prevState,
+ pocketCta: {
+ ctaButton: action.data.cta_button,
+ ctaText: action.data.cta_text,
+ ctaUrl: action.data.cta_url,
+ useCta: action.data.use_cta,
+ },
+ };
+ default:
+ return prevState;
+ }
+}
+
+function Reducers_sys_Personalization(prevState = INITIAL_STATE.Personalization, action) {
+ switch (action.type) {
+ case actionTypes.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED:
+ return {
+ ...prevState,
+ lastUpdated: action.data.lastUpdated,
+ };
+ case actionTypes.DISCOVERY_STREAM_PERSONALIZATION_INIT:
+ return {
+ ...prevState,
+ initialized: true,
+ };
+ case actionTypes.DISCOVERY_STREAM_PERSONALIZATION_RESET:
+ return { ...INITIAL_STATE.Personalization };
+ default:
+ return prevState;
+ }
+}
+
+// eslint-disable-next-line complexity
+function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
+ // Return if action data is empty, or spocs or feeds data is not loaded
+ const isNotReady = () =>
+ !action.data || !prevState.spocs.loaded || !prevState.feeds.loaded;
+
+ const handlePlacements = handleSites => {
+ const { data, placements } = prevState.spocs;
+ const result = {};
+
+ const forPlacement = placement => {
+ const placementSpocs = data[placement.name];
+
+ if (
+ !placementSpocs ||
+ !placementSpocs.items ||
+ !placementSpocs.items.length
+ ) {
+ return;
+ }
+
+ result[placement.name] = {
+ ...placementSpocs,
+ items: handleSites(placementSpocs.items),
+ };
+ };
+
+ if (!placements || !placements.length) {
+ [{ name: "spocs" }].forEach(forPlacement);
+ } else {
+ placements.forEach(forPlacement);
+ }
+ return result;
+ };
+
+ const nextState = handleSites => ({
+ ...prevState,
+ spocs: {
+ ...prevState.spocs,
+ data: handlePlacements(handleSites),
+ },
+ feeds: {
+ ...prevState.feeds,
+ data: Object.keys(prevState.feeds.data).reduce(
+ (accumulator, feed_url) => {
+ accumulator[feed_url] = {
+ data: {
+ ...prevState.feeds.data[feed_url].data,
+ recommendations: handleSites(
+ prevState.feeds.data[feed_url].data.recommendations
+ ),
+ },
+ };
+ return accumulator;
+ },
+ {}
+ ),
+ },
+ });
+
+ switch (action.type) {
+ case actionTypes.DISCOVERY_STREAM_CONFIG_CHANGE:
+ // Fall through to a separate action is so it doesn't trigger a listener update on init
+ case actionTypes.DISCOVERY_STREAM_CONFIG_SETUP:
+ return { ...prevState, config: action.data || {} };
+ case actionTypes.DISCOVERY_STREAM_EXPERIMENT_DATA:
+ return { ...prevState, experimentData: action.data || {} };
+ case actionTypes.DISCOVERY_STREAM_LAYOUT_UPDATE:
+ return {
+ ...prevState,
+ layout: action.data.layout || [],
+ };
+ case actionTypes.DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE:
+ return {
+ ...prevState,
+ isCollectionDismissible: action.data.value,
+ };
+ case actionTypes.DISCOVERY_STREAM_PREFS_SETUP:
+ return {
+ ...prevState,
+ recentSavesEnabled: action.data.recentSavesEnabled,
+ pocketButtonEnabled: action.data.pocketButtonEnabled,
+ saveToPocketCard: action.data.saveToPocketCard,
+ hideDescriptions: action.data.hideDescriptions,
+ compactImages: action.data.compactImages,
+ imageGradient: action.data.imageGradient,
+ newSponsoredLabel: action.data.newSponsoredLabel,
+ titleLines: action.data.titleLines,
+ descLines: action.data.descLines,
+ readTime: action.data.readTime,
+ };
+ case actionTypes.DISCOVERY_STREAM_RECENT_SAVES:
+ return {
+ ...prevState,
+ recentSavesData: action.data.recentSaves,
+ };
+ case actionTypes.DISCOVERY_STREAM_POCKET_STATE_SET:
+ return {
+ ...prevState,
+ isUserLoggedIn: action.data.isUserLoggedIn,
+ };
+ case actionTypes.HIDE_PRIVACY_INFO:
+ return {
+ ...prevState,
+ isPrivacyInfoModalVisible: false,
+ };
+ case actionTypes.SHOW_PRIVACY_INFO:
+ return {
+ ...prevState,
+ isPrivacyInfoModalVisible: true,
+ };
+ case actionTypes.DISCOVERY_STREAM_LAYOUT_RESET:
+ return { ...INITIAL_STATE.DiscoveryStream, config: prevState.config };
+ case actionTypes.DISCOVERY_STREAM_FEEDS_UPDATE:
+ return {
+ ...prevState,
+ feeds: {
+ ...prevState.feeds,
+ loaded: true,
+ },
+ };
+ case actionTypes.DISCOVERY_STREAM_FEED_UPDATE:
+ const newData = {};
+ newData[action.data.url] = action.data.feed;
+ return {
+ ...prevState,
+ feeds: {
+ ...prevState.feeds,
+ data: {
+ ...prevState.feeds.data,
+ ...newData,
+ },
+ },
+ };
+ case actionTypes.DISCOVERY_STREAM_SPOCS_CAPS:
+ return {
+ ...prevState,
+ spocs: {
+ ...prevState.spocs,
+ frequency_caps: [...prevState.spocs.frequency_caps, ...action.data],
+ },
+ };
+ case actionTypes.DISCOVERY_STREAM_SPOCS_ENDPOINT:
+ return {
+ ...prevState,
+ spocs: {
+ ...INITIAL_STATE.DiscoveryStream.spocs,
+ spocs_endpoint:
+ action.data.url ||
+ INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint,
+ },
+ };
+ case actionTypes.DISCOVERY_STREAM_SPOCS_PLACEMENTS:
+ return {
+ ...prevState,
+ spocs: {
+ ...prevState.spocs,
+ placements:
+ action.data.placements ||
+ INITIAL_STATE.DiscoveryStream.spocs.placements,
+ },
+ };
+ case actionTypes.DISCOVERY_STREAM_SPOCS_UPDATE:
+ if (action.data) {
+ return {
+ ...prevState,
+ spocs: {
+ ...prevState.spocs,
+ lastUpdated: action.data.lastUpdated,
+ data: action.data.spocs,
+ loaded: true,
+ },
+ };
+ }
+ return prevState;
+ case actionTypes.DISCOVERY_STREAM_SPOC_BLOCKED:
+ return {
+ ...prevState,
+ spocs: {
+ ...prevState.spocs,
+ blocked: [...prevState.spocs.blocked, action.data.url],
+ },
+ };
+ case actionTypes.DISCOVERY_STREAM_LINK_BLOCKED:
+ return isNotReady()
+ ? prevState
+ : nextState(items =>
+ items.filter(item => item.url !== action.data.url)
+ );
+
+ case actionTypes.PLACES_SAVED_TO_POCKET:
+ const addPocketInfo = item => {
+ if (item.url === action.data.url) {
+ return Object.assign({}, item, {
+ open_url: action.data.open_url,
+ pocket_id: action.data.pocket_id,
+ context_type: "pocket",
+ });
+ }
+ return item;
+ };
+ return isNotReady()
+ ? prevState
+ : nextState(items => items.map(addPocketInfo));
+
+ case actionTypes.DELETE_FROM_POCKET:
+ case actionTypes.ARCHIVE_FROM_POCKET:
+ return isNotReady()
+ ? prevState
+ : nextState(items =>
+ items.filter(item => item.pocket_id !== action.data.pocket_id)
+ );
+
+ case actionTypes.PLACES_BOOKMARK_ADDED:
+ const updateBookmarkInfo = item => {
+ if (item.url === action.data.url) {
+ const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
+ return Object.assign({}, item, {
+ bookmarkGuid,
+ bookmarkTitle,
+ bookmarkDateCreated: dateAdded,
+ context_type: "bookmark",
+ });
+ }
+ return item;
+ };
+ return isNotReady()
+ ? prevState
+ : nextState(items => items.map(updateBookmarkInfo));
+
+ case actionTypes.PLACES_BOOKMARKS_REMOVED:
+ const removeBookmarkInfo = item => {
+ if (action.data.urls.includes(item.url)) {
+ const newSite = Object.assign({}, item);
+ delete newSite.bookmarkGuid;
+ delete newSite.bookmarkTitle;
+ delete newSite.bookmarkDateCreated;
+ if (!newSite.context_type || newSite.context_type === "bookmark") {
+ newSite.context_type = "removedBookmark";
+ }
+ return newSite;
+ }
+ return item;
+ };
+ return isNotReady()
+ ? prevState
+ : nextState(items => items.map(removeBookmarkInfo));
+ case actionTypes.PREF_CHANGED:
+ if (action.data.name === PREF_COLLECTION_DISMISSIBLE) {
+ return {
+ ...prevState,
+ isCollectionDismissible: action.data.value,
+ };
+ }
+ return prevState;
+ default:
+ return prevState;
+ }
+}
+
+function Search(prevState = INITIAL_STATE.Search, action) {
+ switch (action.type) {
+ case actionTypes.DISABLE_SEARCH:
+ return Object.assign({ ...prevState, disable: true });
+ case actionTypes.FAKE_FOCUS_SEARCH:
+ return Object.assign({ ...prevState, fakeFocus: true });
+ case actionTypes.SHOW_SEARCH:
+ return Object.assign({ ...prevState, disable: false, fakeFocus: false });
+ default:
+ return prevState;
+ }
+}
+
+const reducers = {
+ TopSites,
+ App,
+ ASRouter,
+ Prefs,
+ Dialog,
+ Sections,
+ Pocket,
+ Personalization: Reducers_sys_Personalization,
+ DiscoveryStream,
+ Search,
+};
+
+;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class TopSiteFormInput extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ validationError: this.props.validationError
+ };
+ this.onChange = this.onChange.bind(this);
+ this.onMount = this.onMount.bind(this);
+ this.onClearIconPress = this.onClearIconPress.bind(this);
+ }
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.shouldFocus && !this.props.shouldFocus) {
+ this.input.focus();
+ }
+ if (nextProps.validationError && !this.props.validationError) {
+ this.setState({
+ validationError: true
+ });
+ }
+ // If the component is in an error state but the value was cleared by the parent
+ if (this.state.validationError && !nextProps.value) {
+ this.setState({
+ validationError: false
+ });
+ }
+ }
+ onClearIconPress(event) {
+ // If there is input in the URL or custom image URL fields,
+ // and we hit 'enter' while tabbed over the clear icon,
+ // we should execute the function to clear the field.
+ if (event.key === "Enter") {
+ this.props.onClear();
+ }
+ }
+ onChange(ev) {
+ if (this.state.validationError) {
+ this.setState({
+ validationError: false
+ });
+ }
+ this.props.onChange(ev);
+ }
+ onMount(input) {
+ this.input = input;
+ }
+ renderLoadingOrCloseButton() {
+ const showClearButton = this.props.value && this.props.onClear;
+ if (this.props.loading) {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "loading-container"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "loading-animation"
+ }));
+ } else if (showClearButton) {
+ return /*#__PURE__*/external_React_default().createElement("button", {
+ type: "button",
+ className: "icon icon-clear-input icon-button-style",
+ onClick: this.props.onClear,
+ onKeyPress: this.onClearIconPress
+ });
+ }
+ return null;
+ }
+ render() {
+ const {
+ typeUrl
+ } = this.props;
+ const {
+ validationError
+ } = this.state;
+ return /*#__PURE__*/external_React_default().createElement("label", null, /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": this.props.titleId
+ }), /*#__PURE__*/external_React_default().createElement("div", {
+ className: `field ${typeUrl ? "url" : ""}${validationError ? " invalid" : ""}`
+ }, /*#__PURE__*/external_React_default().createElement("input", {
+ type: "text",
+ value: this.props.value,
+ ref: this.onMount,
+ onChange: this.onChange,
+ "data-l10n-id": this.props.placeholderId
+ // Set focus on error if the url field is valid or when the input is first rendered and is empty
+ // eslint-disable-next-line jsx-a11y/no-autofocus
+ ,
+ autoFocus: this.props.autoFocusOnOpen,
+ disabled: this.props.loading
+ }), this.renderLoadingOrCloseButton(), validationError && /*#__PURE__*/external_React_default().createElement("aside", {
+ className: "error-tooltip",
+ "data-l10n-id": this.props.errorMessageId
+ })));
+ }
+}
+TopSiteFormInput.defaultProps = {
+ showClearButton: false,
+ value: "",
+ validationError: false
+};
+;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteImpressionWrapper.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+const TopSiteImpressionWrapper_VISIBLE = "visible";
+const TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT = "visibilitychange";
+
+// Per analytical requirement, we set the minimal intersection ratio to
+// 0.5, and an impression is identified when the wrapped item has at least
+// 50% visibility.
+//
+// This constant is exported for unit test
+const TopSiteImpressionWrapper_INTERSECTION_RATIO = 0.5;
+
+/**
+ * Impression wrapper for a TopSite tile.
+ *
+ * It makses use of the Intersection Observer API to detect the visibility,
+ * and relies on page visibility to ensure the impression is reported
+ * only when the component is visible on the page.
+ */
+class TopSiteImpressionWrapper extends (external_React_default()).PureComponent {
+ _dispatchImpressionStats() {
+ const {
+ actionType,
+ tile
+ } = this.props;
+ if (!actionType) {
+ return;
+ }
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionType,
+ data: {
+ type: "impression",
+ ...tile
+ }
+ }));
+ }
+ setImpressionObserverOrAddListener() {
+ const {
+ props
+ } = this;
+ if (!props.dispatch) {
+ return;
+ }
+ if (props.document.visibilityState === TopSiteImpressionWrapper_VISIBLE) {
+ this.setImpressionObserver();
+ } else {
+ // We should only ever send the latest impression stats ping, so remove any
+ // older listeners.
+ if (this._onVisibilityChange) {
+ props.document.removeEventListener(TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ this._onVisibilityChange = () => {
+ if (props.document.visibilityState === TopSiteImpressionWrapper_VISIBLE) {
+ this.setImpressionObserver();
+ props.document.removeEventListener(TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ };
+ props.document.addEventListener(TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+
+ /**
+ * Set an impression observer for the wrapped component. It makes use of
+ * the Intersection Observer API to detect if the wrapped component is
+ * visible with a desired ratio, and only sends impression if that's the case.
+ *
+ * See more details about Intersection Observer API at:
+ * https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
+ */
+ setImpressionObserver() {
+ const {
+ props
+ } = this;
+ if (!props.tile) {
+ return;
+ }
+ this._handleIntersect = entries => {
+ if (entries.some(entry => entry.isIntersecting && entry.intersectionRatio >= TopSiteImpressionWrapper_INTERSECTION_RATIO)) {
+ this._dispatchImpressionStats();
+ this.impressionObserver.unobserve(this.refs.topsite_impression_wrapper);
+ }
+ };
+ const options = {
+ threshold: TopSiteImpressionWrapper_INTERSECTION_RATIO
+ };
+ this.impressionObserver = new props.IntersectionObserver(this._handleIntersect, options);
+ this.impressionObserver.observe(this.refs.topsite_impression_wrapper);
+ }
+ componentDidMount() {
+ if (this.props.tile) {
+ this.setImpressionObserverOrAddListener();
+ }
+ }
+ componentWillUnmount() {
+ if (this._handleIntersect && this.impressionObserver) {
+ this.impressionObserver.unobserve(this.refs.topsite_impression_wrapper);
+ }
+ if (this._onVisibilityChange) {
+ this.props.document.removeEventListener(TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ ref: "topsite_impression_wrapper",
+ className: "topsite-impression-observer"
+ }, this.props.children);
+ }
+}
+TopSiteImpressionWrapper.defaultProps = {
+ IntersectionObserver: __webpack_require__.g.IntersectionObserver,
+ document: __webpack_require__.g.document,
+ actionType: null,
+ tile: null
+};
+;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSite.jsx
+function TopSite_extends() { TopSite_extends = Object.assign ? Object.assign.bind() : 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; }; return TopSite_extends.apply(this, arguments); }
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+
+
+
+const SPOC_TYPE = "SPOC";
+const NEWTAB_SOURCE = "newtab";
+
+// For cases if we want to know if this is sponsored by either sponsored_position or type.
+// We have two sources for sponsored topsites, and
+// sponsored_position is set by one sponsored source, and type is set by another.
+// This is not called in all cases, sometimes we want to know if it's one source
+// or the other. This function is only applicable in cases where we only care if it's either.
+function isSponsored(link) {
+ return link?.sponsored_position || link?.type === SPOC_TYPE;
+}
+class TopSiteLink extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ screenshotImage: null
+ };
+ this.onDragEvent = this.onDragEvent.bind(this);
+ this.onKeyPress = this.onKeyPress.bind(this);
+ }
+
+ /*
+ * Helper to determine whether the drop zone should allow a drop. We only allow
+ * dropping top sites for now. We don't allow dropping on sponsored top sites
+ * as their position is fixed.
+ */
+ _allowDrop(e) {
+ return (this.dragged || !isSponsored(this.props.link)) && e.dataTransfer.types.includes("text/topsite-index");
+ }
+ onDragEvent(event) {
+ switch (event.type) {
+ case "click":
+ // Stop any link clicks if we started any dragging
+ if (this.dragged) {
+ event.preventDefault();
+ }
+ break;
+ case "dragstart":
+ event.target.blur();
+ if (isSponsored(this.props.link)) {
+ event.preventDefault();
+ break;
+ }
+ this.dragged = true;
+ event.dataTransfer.effectAllowed = "move";
+ event.dataTransfer.setData("text/topsite-index", this.props.index);
+ this.props.onDragEvent(event, this.props.index, this.props.link, this.props.title);
+ break;
+ case "dragend":
+ this.props.onDragEvent(event);
+ break;
+ case "dragenter":
+ case "dragover":
+ case "drop":
+ if (this._allowDrop(event)) {
+ event.preventDefault();
+ this.props.onDragEvent(event, this.props.index);
+ }
+ break;
+ case "mousedown":
+ // Block the scroll wheel from appearing for middle clicks on search top sites
+ if (event.button === 1 && this.props.link.searchTopSite) {
+ event.preventDefault();
+ }
+ // Reset at the first mouse event of a potential drag
+ this.dragged = false;
+ break;
+ }
+ }
+
+ /**
+ * Helper to obtain the next state based on nextProps and prevState.
+ *
+ * NOTE: Rename this method to getDerivedStateFromProps when we update React
+ * to >= 16.3. We will need to update tests as well. We cannot rename this
+ * method to getDerivedStateFromProps now because there is a mismatch in
+ * the React version that we are using for both testing and production.
+ * (i.e. react-test-render => "16.3.2", react => "16.2.0").
+ *
+ * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43.
+ */
+ static getNextStateFromProps(nextProps, prevState) {
+ const {
+ screenshot
+ } = nextProps.link;
+ const imageInState = ScreenshotUtils.isRemoteImageLocal(prevState.screenshotImage, screenshot);
+ if (imageInState) {
+ return null;
+ }
+
+ // Since image was updated, attempt to revoke old image blob URL, if it exists.
+ ScreenshotUtils.maybeRevokeBlobObjectURL(prevState.screenshotImage);
+ return {
+ screenshotImage: ScreenshotUtils.createLocalImageObject(screenshot)
+ };
+ }
+
+ // NOTE: Remove this function when we update React to >= 16.3 since React will
+ // call getDerivedStateFromProps automatically. We will also need to
+ // rename getNextStateFromProps to getDerivedStateFromProps.
+ componentWillMount() {
+ const nextState = TopSiteLink.getNextStateFromProps(this.props, this.state);
+ if (nextState) {
+ this.setState(nextState);
+ }
+ }
+
+ // NOTE: Remove this function when we update React to >= 16.3 since React will
+ // call getDerivedStateFromProps automatically. We will also need to
+ // rename getNextStateFromProps to getDerivedStateFromProps.
+ componentWillReceiveProps(nextProps) {
+ const nextState = TopSiteLink.getNextStateFromProps(nextProps, this.state);
+ if (nextState) {
+ this.setState(nextState);
+ }
+ }
+ componentWillUnmount() {
+ ScreenshotUtils.maybeRevokeBlobObjectURL(this.state.screenshotImage);
+ }
+ onKeyPress(event) {
+ // If we have tabbed to a search shortcut top site, and we click 'enter',
+ // we should execute the onClick function. This needs to be added because
+ // search top sites are anchor tags without an href. See bug 1483135
+ if (this.props.link.searchTopSite && event.key === "Enter") {
+ this.props.onClick(event);
+ }
+ }
+
+ /*
+ * Takes the url as a string, runs it through a simple (non-secure) hash turning it into a random number
+ * Apply that random number to the color array. The same url will always generate the same color.
+ */
+ generateColor() {
+ let {
+ title,
+ colors
+ } = this.props;
+ if (!colors) {
+ return "";
+ }
+ let colorArray = colors.split(",");
+ const hashStr = str => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ let charCode = str.charCodeAt(i);
+ hash += charCode;
+ }
+ return hash;
+ };
+ let hash = hashStr(title);
+ let index = hash % colorArray.length;
+ return colorArray[index];
+ }
+ calculateStyle() {
+ const {
+ defaultStyle,
+ link
+ } = this.props;
+ const {
+ tippyTopIcon,
+ faviconSize
+ } = link;
+ let imageClassName;
+ let imageStyle;
+ let showSmallFavicon = false;
+ let smallFaviconStyle;
+ let hasScreenshotImage = this.state.screenshotImage && this.state.screenshotImage.url;
+ let selectedColor;
+ if (defaultStyle) {
+ // force no styles (letter fallback) even if the link has imagery
+ selectedColor = this.generateColor();
+ } else if (link.searchTopSite) {
+ imageClassName = "top-site-icon rich-icon";
+ imageStyle = {
+ backgroundColor: link.backgroundColor,
+ backgroundImage: `url(${tippyTopIcon})`
+ };
+ smallFaviconStyle = {
+ backgroundImage: `url(${tippyTopIcon})`
+ };
+ } else if (link.customScreenshotURL) {
+ // assume high quality custom screenshot and use rich icon styles and class names
+ imageClassName = "top-site-icon rich-icon";
+ imageStyle = {
+ backgroundColor: link.backgroundColor,
+ backgroundImage: hasScreenshotImage ? `url(${this.state.screenshotImage.url})` : ""
+ };
+ } else if (tippyTopIcon || link.type === SPOC_TYPE || faviconSize >= MIN_RICH_FAVICON_SIZE) {
+ // styles and class names for top sites with rich icons
+ imageClassName = "top-site-icon rich-icon";
+ imageStyle = {
+ backgroundColor: link.backgroundColor,
+ backgroundImage: `url(${tippyTopIcon || link.favicon})`
+ };
+ } else if (faviconSize >= MIN_SMALL_FAVICON_SIZE) {
+ showSmallFavicon = true;
+ smallFaviconStyle = {
+ backgroundImage: `url(${link.favicon})`
+ };
+ } else {
+ selectedColor = this.generateColor();
+ imageClassName = "";
+ }
+ return {
+ showSmallFavicon,
+ smallFaviconStyle,
+ imageStyle,
+ imageClassName,
+ selectedColor
+ };
+ }
+ render() {
+ const {
+ children,
+ className,
+ isDraggable,
+ link,
+ onClick,
+ title
+ } = this.props;
+ const topSiteOuterClassName = `top-site-outer${className ? ` ${className}` : ""}${link.isDragged ? " dragged" : ""}${link.searchTopSite ? " search-shortcut" : ""}`;
+ const [letterFallback] = title;
+ const {
+ showSmallFavicon,
+ smallFaviconStyle,
+ imageStyle,
+ imageClassName,
+ selectedColor
+ } = this.calculateStyle();
+ let draggableProps = {};
+ if (isDraggable) {
+ draggableProps = {
+ onClick: this.onDragEvent,
+ onDragEnd: this.onDragEvent,
+ onDragStart: this.onDragEvent,
+ onMouseDown: this.onDragEvent
+ };
+ }
+ let impressionStats = null;
+ if (link.type === SPOC_TYPE) {
+ // Record impressions for Pocket tiles.
+ impressionStats = /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, {
+ flightId: link.flightId,
+ rows: [{
+ id: link.id,
+ pos: link.pos,
+ shim: link.shim && link.shim.impression,
+ advertiser: title.toLocaleLowerCase()
+ }],
+ dispatch: this.props.dispatch,
+ source: TOP_SITES_SOURCE
+ });
+ } else if (isSponsored(link)) {
+ // Record impressions for non-Pocket sponsored tiles.
+ impressionStats = /*#__PURE__*/external_React_default().createElement(TopSiteImpressionWrapper, {
+ actionType: actionTypes.TOP_SITES_SPONSORED_IMPRESSION_STATS,
+ tile: {
+ position: this.props.index,
+ tile_id: link.sponsored_tile_id || -1,
+ reporting_url: link.sponsored_impression_url,
+ advertiser: title.toLocaleLowerCase(),
+ source: NEWTAB_SOURCE
+ }
+ // For testing.
+ ,
+ IntersectionObserver: this.props.IntersectionObserver,
+ document: this.props.document,
+ dispatch: this.props.dispatch
+ });
+ } else {
+ // Record impressions for organic tiles.
+ impressionStats = /*#__PURE__*/external_React_default().createElement(TopSiteImpressionWrapper, {
+ actionType: actionTypes.TOP_SITES_ORGANIC_IMPRESSION_STATS,
+ tile: {
+ position: this.props.index,
+ source: NEWTAB_SOURCE
+ }
+ // For testing.
+ ,
+ IntersectionObserver: this.props.IntersectionObserver,
+ document: this.props.document,
+ dispatch: this.props.dispatch
+ });
+ }
+ return /*#__PURE__*/external_React_default().createElement("li", TopSite_extends({
+ className: topSiteOuterClassName,
+ onDrop: this.onDragEvent,
+ onDragOver: this.onDragEvent,
+ onDragEnter: this.onDragEvent,
+ onDragLeave: this.onDragEvent
+ }, draggableProps), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "top-site-inner"
+ }, /*#__PURE__*/external_React_default().createElement("a", {
+ className: "top-site-button",
+ href: link.searchTopSite ? undefined : link.url,
+ tabIndex: "0",
+ onKeyPress: this.onKeyPress,
+ onClick: onClick,
+ draggable: true,
+ "data-is-sponsored-link": !!link.sponsored_tile_id
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "tile",
+ "aria-hidden": true
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: selectedColor ? "icon-wrapper letter-fallback" : "icon-wrapper",
+ "data-fallback": letterFallback,
+ style: selectedColor ? {
+ backgroundColor: selectedColor
+ } : {}
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: imageClassName,
+ style: imageStyle
+ }), showSmallFavicon && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "top-site-icon default-icon",
+ "data-fallback": smallFaviconStyle ? "" : letterFallback,
+ style: smallFaviconStyle
+ })), link.searchTopSite && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "top-site-icon search-topsite"
+ })), /*#__PURE__*/external_React_default().createElement("div", {
+ className: `title${link.isPinned ? " has-icon pinned" : ""}${link.type === SPOC_TYPE || link.show_sponsored_label ? " sponsored" : ""}`
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ dir: "auto"
+ }, link.isPinned && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "icon icon-pin-small"
+ }), title || /*#__PURE__*/external_React_default().createElement("br", null), /*#__PURE__*/external_React_default().createElement("span", {
+ className: "sponsored-label",
+ "data-l10n-id": "newtab-topsite-sponsored"
+ })))), children, impressionStats));
+ }
+}
+TopSiteLink.defaultProps = {
+ title: "",
+ link: {},
+ isDraggable: true
+};
+class TopSite extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ showContextMenu: false
+ };
+ this.onLinkClick = this.onLinkClick.bind(this);
+ this.onMenuUpdate = this.onMenuUpdate.bind(this);
+ }
+
+ /**
+ * Report to telemetry additional information about the item.
+ */
+ _getTelemetryInfo() {
+ const value = {
+ icon_type: this.props.link.iconType
+ };
+ // Filter out "not_pinned" type for being the default
+ if (this.props.link.isPinned) {
+ value.card_type = "pinned";
+ }
+ if (this.props.link.searchTopSite) {
+ // Set the card_type as "search" regardless of its pinning status
+ value.card_type = "search";
+ value.search_vendor = this.props.link.hostname;
+ }
+ if (isSponsored(this.props.link)) {
+ value.card_type = "spoc";
+ }
+ return {
+ value
+ };
+ }
+ userEvent(event) {
+ this.props.dispatch(actionCreators.UserEvent(Object.assign({
+ event,
+ source: TOP_SITES_SOURCE,
+ action_position: this.props.index
+ }, this._getTelemetryInfo())));
+ }
+ onLinkClick(event) {
+ this.userEvent("CLICK");
+
+ // Specially handle a top site link click for "typed" frecency bonus as
+ // specified as a property on the link.
+ event.preventDefault();
+ const {
+ altKey,
+ button,
+ ctrlKey,
+ metaKey,
+ shiftKey
+ } = event;
+ if (!this.props.link.searchTopSite) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.OPEN_LINK,
+ data: Object.assign(this.props.link, {
+ event: {
+ altKey,
+ button,
+ ctrlKey,
+ metaKey,
+ shiftKey
+ }
+ })
+ }));
+ if (this.props.link.type === SPOC_TYPE) {
+ // Record a Pocket-specific click.
+ this.props.dispatch(actionCreators.ImpressionStats({
+ source: TOP_SITES_SOURCE,
+ click: 0,
+ tiles: [{
+ id: this.props.link.id,
+ pos: this.props.link.pos,
+ shim: this.props.link.shim && this.props.link.shim.click
+ }]
+ }));
+
+ // Record a click for a Pocket sponsored tile.
+ // This first event is for the shim property
+ // and is used by our ad service provider.
+ this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: TOP_SITES_SOURCE,
+ action_position: this.props.link.pos,
+ value: {
+ card_type: "spoc",
+ tile_id: this.props.link.id,
+ shim: this.props.link.shim && this.props.link.shim.click
+ }
+ }));
+
+ // A second event is recoded for internal usage.
+ const title = this.props.link.label || this.props.link.hostname;
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.TOP_SITES_SPONSORED_IMPRESSION_STATS,
+ data: {
+ type: "click",
+ position: this.props.link.pos,
+ tile_id: this.props.link.id,
+ advertiser: title.toLocaleLowerCase(),
+ source: NEWTAB_SOURCE
+ }
+ }));
+ } else if (isSponsored(this.props.link)) {
+ // Record a click for a non-Pocket sponsored tile.
+ const title = this.props.link.label || this.props.link.hostname;
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.TOP_SITES_SPONSORED_IMPRESSION_STATS,
+ data: {
+ type: "click",
+ position: this.props.index,
+ tile_id: this.props.link.sponsored_tile_id || -1,
+ reporting_url: this.props.link.sponsored_click_url,
+ advertiser: title.toLocaleLowerCase(),
+ source: NEWTAB_SOURCE
+ }
+ }));
+ } else {
+ // Record a click for an organic tile.
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.TOP_SITES_ORGANIC_IMPRESSION_STATS,
+ data: {
+ type: "click",
+ position: this.props.index,
+ source: NEWTAB_SOURCE
+ }
+ }));
+ }
+ if (this.props.link.sendAttributionRequest) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.PARTNER_LINK_ATTRIBUTION,
+ data: {
+ targetURL: this.props.link.url,
+ source: "newtab"
+ }
+ }));
+ }
+ } else {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.FILL_SEARCH_TERM,
+ data: {
+ label: this.props.link.label
+ }
+ }));
+ }
+ }
+ onMenuUpdate(isOpen) {
+ if (isOpen) {
+ this.props.onActivate(this.props.index);
+ } else {
+ this.props.onActivate();
+ }
+ }
+ render() {
+ const {
+ props
+ } = this;
+ const {
+ link
+ } = props;
+ const isContextMenuOpen = props.activeIndex === props.index;
+ const title = link.label || link.hostname;
+ let menuOptions;
+ if (link.sponsored_position) {
+ menuOptions = TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS;
+ } else if (link.searchTopSite) {
+ menuOptions = TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS;
+ } else if (link.type === SPOC_TYPE) {
+ menuOptions = TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS;
+ } else {
+ menuOptions = TOP_SITES_CONTEXT_MENU_OPTIONS;
+ }
+ return /*#__PURE__*/external_React_default().createElement(TopSiteLink, TopSite_extends({}, props, {
+ onClick: this.onLinkClick,
+ onDragEvent: this.props.onDragEvent,
+ className: `${props.className || ""}${isContextMenuOpen ? " active" : ""}`,
+ title: title
+ }), /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement(ContextMenuButton, {
+ tooltip: "newtab-menu-content-tooltip",
+ tooltipArgs: {
+ title
+ },
+ onUpdate: this.onMenuUpdate
+ }, /*#__PURE__*/external_React_default().createElement(LinkMenu, {
+ dispatch: props.dispatch,
+ index: props.index,
+ onUpdate: this.onMenuUpdate,
+ options: menuOptions,
+ site: link,
+ shouldSendImpressionStats: link.type === SPOC_TYPE,
+ siteInfo: this._getTelemetryInfo(),
+ source: TOP_SITES_SOURCE
+ }))));
+ }
+}
+TopSite.defaultProps = {
+ link: {},
+ onActivate() {}
+};
+class TopSitePlaceholder extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onEditButtonClick = this.onEditButtonClick.bind(this);
+ }
+ onEditButtonClick() {
+ this.props.dispatch({
+ type: actionTypes.TOP_SITES_EDIT,
+ data: {
+ index: this.props.index
+ }
+ });
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement(TopSiteLink, TopSite_extends({}, this.props, {
+ className: `placeholder ${this.props.className || ""}`,
+ isDraggable: false
+ }), /*#__PURE__*/external_React_default().createElement("button", {
+ "aria-haspopup": "dialog",
+ className: "context-menu-button edit-button icon",
+ "data-l10n-id": "newtab-menu-topsites-placeholder-tooltip",
+ onClick: this.onEditButtonClick
+ }));
+ }
+}
+class _TopSiteList extends (external_React_default()).PureComponent {
+ static get DEFAULT_STATE() {
+ return {
+ activeIndex: null,
+ draggedIndex: null,
+ draggedSite: null,
+ draggedTitle: null,
+ topSitesPreview: null
+ };
+ }
+ constructor(props) {
+ super(props);
+ this.state = _TopSiteList.DEFAULT_STATE;
+ this.onDragEvent = this.onDragEvent.bind(this);
+ this.onActivate = this.onActivate.bind(this);
+ }
+ componentWillReceiveProps(nextProps) {
+ if (this.state.draggedSite) {
+ const prevTopSites = this.props.TopSites && this.props.TopSites.rows;
+ const newTopSites = nextProps.TopSites && nextProps.TopSites.rows;
+ if (prevTopSites && prevTopSites[this.state.draggedIndex] && prevTopSites[this.state.draggedIndex].url === this.state.draggedSite.url && (!newTopSites[this.state.draggedIndex] || newTopSites[this.state.draggedIndex].url !== this.state.draggedSite.url)) {
+ // We got the new order from the redux store via props. We can clear state now.
+ this.setState(_TopSiteList.DEFAULT_STATE);
+ }
+ }
+ }
+ userEvent(event, index) {
+ this.props.dispatch(actionCreators.UserEvent({
+ event,
+ source: TOP_SITES_SOURCE,
+ action_position: index
+ }));
+ }
+ onDragEvent(event, index, link, title) {
+ switch (event.type) {
+ case "dragstart":
+ this.dropped = false;
+ this.setState({
+ draggedIndex: index,
+ draggedSite: link,
+ draggedTitle: title,
+ activeIndex: null
+ });
+ this.userEvent("DRAG", index);
+ break;
+ case "dragend":
+ if (!this.dropped) {
+ // If there was no drop event, reset the state to the default.
+ this.setState(_TopSiteList.DEFAULT_STATE);
+ }
+ break;
+ case "dragenter":
+ if (index === this.state.draggedIndex) {
+ this.setState({
+ topSitesPreview: null
+ });
+ } else {
+ this.setState({
+ topSitesPreview: this._makeTopSitesPreview(index)
+ });
+ }
+ break;
+ case "drop":
+ if (index !== this.state.draggedIndex) {
+ this.dropped = true;
+ this.props.dispatch(actionCreators.AlsoToMain({
+ type: actionTypes.TOP_SITES_INSERT,
+ data: {
+ site: {
+ url: this.state.draggedSite.url,
+ label: this.state.draggedTitle,
+ customScreenshotURL: this.state.draggedSite.customScreenshotURL,
+ // Only if the search topsites experiment is enabled
+ ...(this.state.draggedSite.searchTopSite && {
+ searchTopSite: true
+ })
+ },
+ index,
+ draggedFromIndex: this.state.draggedIndex
+ }
+ }));
+ this.userEvent("DROP", index);
+ }
+ break;
+ }
+ }
+ _getTopSites() {
+ // Make a copy of the sites to truncate or extend to desired length
+ let topSites = this.props.TopSites.rows.slice();
+ topSites.length = this.props.TopSitesRows * TOP_SITES_MAX_SITES_PER_ROW;
+ return topSites;
+ }
+
+ /**
+ * Make a preview of the topsites that will be the result of dropping the currently
+ * dragged site at the specified index.
+ */
+ _makeTopSitesPreview(index) {
+ const topSites = this._getTopSites();
+ topSites[this.state.draggedIndex] = null;
+ const preview = topSites.map(site => site && (site.isPinned || isSponsored(site)) ? site : null);
+ const unpinned = topSites.filter(site => site && !site.isPinned && !isSponsored(site));
+ const siteToInsert = Object.assign({}, this.state.draggedSite, {
+ isPinned: true,
+ isDragged: true
+ });
+ if (!preview[index]) {
+ preview[index] = siteToInsert;
+ } else {
+ // Find the hole to shift the pinned site(s) towards. We shift towards the
+ // hole left by the site being dragged.
+ let holeIndex = index;
+ const indexStep = index > this.state.draggedIndex ? -1 : 1;
+ while (preview[holeIndex]) {
+ holeIndex += indexStep;
+ }
+
+ // Shift towards the hole.
+ const shiftingStep = index > this.state.draggedIndex ? 1 : -1;
+ while (index > this.state.draggedIndex ? holeIndex < index : holeIndex > index) {
+ let nextIndex = holeIndex + shiftingStep;
+ while (isSponsored(preview[nextIndex])) {
+ nextIndex += shiftingStep;
+ }
+ preview[holeIndex] = preview[nextIndex];
+ holeIndex = nextIndex;
+ }
+ preview[index] = siteToInsert;
+ }
+
+ // Fill in the remaining holes with unpinned sites.
+ for (let i = 0; i < preview.length; i++) {
+ if (!preview[i]) {
+ preview[i] = unpinned.shift() || null;
+ }
+ }
+ return preview;
+ }
+ onActivate(index) {
+ this.setState({
+ activeIndex: index
+ });
+ }
+ render() {
+ const {
+ props
+ } = this;
+ const topSites = this.state.topSitesPreview || this._getTopSites();
+ const topSitesUI = [];
+ const commonProps = {
+ onDragEvent: this.onDragEvent,
+ dispatch: props.dispatch
+ };
+ // We assign a key to each placeholder slot. We need it to be independent
+ // of the slot index (i below) so that the keys used stay the same during
+ // drag and drop reordering and the underlying DOM nodes are reused.
+ // This mostly (only?) affects linux so be sure to test on linux before changing.
+ let holeIndex = 0;
+
+ // On narrow viewports, we only show 6 sites per row. We'll mark the rest as
+ // .hide-for-narrow to hide in CSS via @media query.
+ const maxNarrowVisibleIndex = props.TopSitesRows * 6;
+ for (let i = 0, l = topSites.length; i < l; i++) {
+ const link = topSites[i] && Object.assign({}, topSites[i], {
+ iconType: this.props.topSiteIconType(topSites[i])
+ });
+ const slotProps = {
+ key: link ? link.url : holeIndex++,
+ index: i
+ };
+ if (i >= maxNarrowVisibleIndex) {
+ slotProps.className = "hide-for-narrow";
+ }
+ let topSiteLink;
+ // Use a placeholder if the link is empty or it's rendering a sponsored
+ // tile for the about:home startup cache.
+ if (!link || props.App.isForStartupCache && isSponsored(link)) {
+ topSiteLink = /*#__PURE__*/external_React_default().createElement(TopSitePlaceholder, TopSite_extends({}, slotProps, commonProps));
+ } else {
+ topSiteLink = /*#__PURE__*/external_React_default().createElement(TopSite, TopSite_extends({
+ link: link,
+ activeIndex: this.state.activeIndex,
+ onActivate: this.onActivate
+ }, slotProps, commonProps, {
+ colors: props.colors
+ }));
+ }
+ topSitesUI.push(topSiteLink);
+ }
+ return /*#__PURE__*/external_React_default().createElement("ul", {
+ className: `top-sites-list${this.state.draggedSite ? " dnd-active" : ""}`
+ }, topSitesUI);
+ }
+}
+const TopSiteList = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ App: state.App
+}))(_TopSiteList);
+;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteForm.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+class TopSiteForm extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ const {
+ site
+ } = props;
+ this.state = {
+ label: site ? site.label || site.hostname : "",
+ url: site ? site.url : "",
+ validationError: false,
+ customScreenshotUrl: site ? site.customScreenshotURL : "",
+ showCustomScreenshotForm: site ? site.customScreenshotURL : false
+ };
+ this.onClearScreenshotInput = this.onClearScreenshotInput.bind(this);
+ this.onLabelChange = this.onLabelChange.bind(this);
+ this.onUrlChange = this.onUrlChange.bind(this);
+ this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
+ this.onClearUrlClick = this.onClearUrlClick.bind(this);
+ this.onDoneButtonClick = this.onDoneButtonClick.bind(this);
+ this.onCustomScreenshotUrlChange = this.onCustomScreenshotUrlChange.bind(this);
+ this.onPreviewButtonClick = this.onPreviewButtonClick.bind(this);
+ this.onEnableScreenshotUrlForm = this.onEnableScreenshotUrlForm.bind(this);
+ this.validateUrl = this.validateUrl.bind(this);
+ }
+ onLabelChange(event) {
+ this.setState({
+ label: event.target.value
+ });
+ }
+ onUrlChange(event) {
+ this.setState({
+ url: event.target.value,
+ validationError: false
+ });
+ }
+ onClearUrlClick() {
+ this.setState({
+ url: "",
+ validationError: false
+ });
+ }
+ onEnableScreenshotUrlForm() {
+ this.setState({
+ showCustomScreenshotForm: true
+ });
+ }
+ _updateCustomScreenshotInput(customScreenshotUrl) {
+ this.setState({
+ customScreenshotUrl,
+ validationError: false
+ });
+ this.props.dispatch({
+ type: actionTypes.PREVIEW_REQUEST_CANCEL
+ });
+ }
+ onCustomScreenshotUrlChange(event) {
+ this._updateCustomScreenshotInput(event.target.value);
+ }
+ onClearScreenshotInput() {
+ this._updateCustomScreenshotInput("");
+ }
+ onCancelButtonClick(ev) {
+ ev.preventDefault();
+ this.props.onClose();
+ }
+ onDoneButtonClick(ev) {
+ ev.preventDefault();
+ if (this.validateForm()) {
+ const site = {
+ url: this.cleanUrl(this.state.url)
+ };
+ const {
+ index
+ } = this.props;
+ if (this.state.label !== "") {
+ site.label = this.state.label;
+ }
+ if (this.state.customScreenshotUrl) {
+ site.customScreenshotURL = this.cleanUrl(this.state.customScreenshotUrl);
+ } else if (this.props.site && this.props.site.customScreenshotURL) {
+ // Used to flag that previously cached screenshot should be removed
+ site.customScreenshotURL = null;
+ }
+ this.props.dispatch(actionCreators.AlsoToMain({
+ type: actionTypes.TOP_SITES_PIN,
+ data: {
+ site,
+ index
+ }
+ }));
+ this.props.dispatch(actionCreators.UserEvent({
+ source: TOP_SITES_SOURCE,
+ event: "TOP_SITES_EDIT",
+ action_position: index
+ }));
+ this.props.onClose();
+ }
+ }
+ onPreviewButtonClick(event) {
+ event.preventDefault();
+ if (this.validateForm()) {
+ this.props.dispatch(actionCreators.AlsoToMain({
+ type: actionTypes.PREVIEW_REQUEST,
+ data: {
+ url: this.cleanUrl(this.state.customScreenshotUrl)
+ }
+ }));
+ this.props.dispatch(actionCreators.UserEvent({
+ source: TOP_SITES_SOURCE,
+ event: "PREVIEW_REQUEST"
+ }));
+ }
+ }
+ cleanUrl(url) {
+ // If we are missing a protocol, prepend http://
+ if (!url.startsWith("http:") && !url.startsWith("https:")) {
+ return `http://${url}`;
+ }
+ return url;
+ }
+ _tryParseUrl(url) {
+ try {
+ return new URL(url);
+ } catch (e) {
+ return null;
+ }
+ }
+ validateUrl(url) {
+ const validProtocols = ["http:", "https:"];
+ const urlObj = this._tryParseUrl(url) || this._tryParseUrl(this.cleanUrl(url));
+ return urlObj && validProtocols.includes(urlObj.protocol);
+ }
+ validateCustomScreenshotUrl() {
+ const {
+ customScreenshotUrl
+ } = this.state;
+ return !customScreenshotUrl || this.validateUrl(customScreenshotUrl);
+ }
+ validateForm() {
+ const validate = this.validateUrl(this.state.url) && this.validateCustomScreenshotUrl();
+ if (!validate) {
+ this.setState({
+ validationError: true
+ });
+ }
+ return validate;
+ }
+ _renderCustomScreenshotInput() {
+ const {
+ customScreenshotUrl
+ } = this.state;
+ const requestFailed = this.props.previewResponse === "";
+ const validationError = this.state.validationError && !this.validateCustomScreenshotUrl() || requestFailed;
+ // Set focus on error if the url field is valid or when the input is first rendered and is empty
+ const shouldFocus = validationError && this.validateUrl(this.state.url) || !customScreenshotUrl;
+ const isLoading = this.props.previewResponse === null && customScreenshotUrl && this.props.previewUrl === this.cleanUrl(customScreenshotUrl);
+ if (!this.state.showCustomScreenshotForm) {
+ return /*#__PURE__*/external_React_default().createElement(A11yLinkButton, {
+ onClick: this.onEnableScreenshotUrlForm,
+ className: "enable-custom-image-input",
+ "data-l10n-id": "newtab-topsites-use-image-link"
+ });
+ }
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "custom-image-input-container"
+ }, /*#__PURE__*/external_React_default().createElement(TopSiteFormInput, {
+ errorMessageId: requestFailed ? "newtab-topsites-image-validation" : "newtab-topsites-url-validation",
+ loading: isLoading,
+ onChange: this.onCustomScreenshotUrlChange,
+ onClear: this.onClearScreenshotInput,
+ shouldFocus: shouldFocus,
+ typeUrl: true,
+ value: customScreenshotUrl,
+ validationError: validationError,
+ titleId: "newtab-topsites-image-url-label",
+ placeholderId: "newtab-topsites-url-input"
+ }));
+ }
+ render() {
+ const {
+ customScreenshotUrl
+ } = this.state;
+ const requestFailed = this.props.previewResponse === "";
+ // For UI purposes, editing without an existing link is "add"
+ const showAsAdd = !this.props.site;
+ const previous = this.props.site && this.props.site.customScreenshotURL || "";
+ const changed = customScreenshotUrl && this.cleanUrl(customScreenshotUrl) !== previous;
+ // Preview mode if changes were made to the custom screenshot URL and no preview was received yet
+ // or the request failed
+ const previewMode = changed && !this.props.previewResponse;
+ const previewLink = Object.assign({}, this.props.site);
+ if (this.props.previewResponse) {
+ previewLink.screenshot = this.props.previewResponse;
+ previewLink.customScreenshotURL = this.props.previewUrl;
+ }
+ // Handles the form submit so an enter press performs the correct action
+ const onSubmit = previewMode ? this.onPreviewButtonClick : this.onDoneButtonClick;
+ const addTopsitesHeaderL10nId = "newtab-topsites-add-shortcut-header";
+ const editTopsitesHeaderL10nId = "newtab-topsites-edit-shortcut-header";
+ return /*#__PURE__*/external_React_default().createElement("form", {
+ className: "topsite-form",
+ onSubmit: onSubmit
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "form-input-container"
+ }, /*#__PURE__*/external_React_default().createElement("h3", {
+ className: "section-title grey-title",
+ "data-l10n-id": showAsAdd ? addTopsitesHeaderL10nId : editTopsitesHeaderL10nId
+ }), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "fields-and-preview"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "form-wrapper"
+ }, /*#__PURE__*/external_React_default().createElement(TopSiteFormInput, {
+ onChange: this.onLabelChange,
+ value: this.state.label,
+ titleId: "newtab-topsites-title-label",
+ placeholderId: "newtab-topsites-title-input",
+ autoFocusOnOpen: true
+ }), /*#__PURE__*/external_React_default().createElement(TopSiteFormInput, {
+ onChange: this.onUrlChange,
+ shouldFocus: this.state.validationError && !this.validateUrl(this.state.url),
+ value: this.state.url,
+ onClear: this.onClearUrlClick,
+ validationError: this.state.validationError && !this.validateUrl(this.state.url),
+ titleId: "newtab-topsites-url-label",
+ typeUrl: true,
+ placeholderId: "newtab-topsites-url-input",
+ errorMessageId: "newtab-topsites-url-validation"
+ }), this._renderCustomScreenshotInput()), /*#__PURE__*/external_React_default().createElement(TopSiteLink, {
+ link: previewLink,
+ defaultStyle: requestFailed,
+ title: this.state.label
+ }))), /*#__PURE__*/external_React_default().createElement("section", {
+ className: "actions"
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ className: "cancel",
+ type: "button",
+ onClick: this.onCancelButtonClick,
+ "data-l10n-id": "newtab-topsites-cancel-button"
+ }), previewMode ? /*#__PURE__*/external_React_default().createElement("button", {
+ className: "done preview",
+ type: "submit",
+ "data-l10n-id": "newtab-topsites-preview-button"
+ }) : /*#__PURE__*/external_React_default().createElement("button", {
+ className: "done",
+ type: "submit",
+ "data-l10n-id": showAsAdd ? "newtab-topsites-add-button" : "newtab-topsites-save-button"
+ })));
+ }
+}
+TopSiteForm.defaultProps = {
+ site: null,
+ index: -1
+};
+;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSites.jsx
+function TopSites_extends() { TopSites_extends = Object.assign ? Object.assign.bind() : 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; }; return TopSites_extends.apply(this, arguments); }
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+
+
+
+
+function topSiteIconType(link) {
+ if (link.customScreenshotURL) {
+ return "custom_screenshot";
+ }
+ if (link.tippyTopIcon || link.faviconRef === "tippytop") {
+ return "tippytop";
+ }
+ if (link.faviconSize >= MIN_RICH_FAVICON_SIZE) {
+ return "rich_icon";
+ }
+ if (link.screenshot) {
+ return "screenshot";
+ }
+ return "no_image";
+}
+
+/**
+ * Iterates through TopSites and counts types of images.
+ * @param acc Accumulator for reducer.
+ * @param topsite Entry in TopSites.
+ */
+function countTopSitesIconsTypes(topSites) {
+ const countTopSitesTypes = (acc, link) => {
+ acc[topSiteIconType(link)]++;
+ return acc;
+ };
+ return topSites.reduce(countTopSitesTypes, {
+ custom_screenshot: 0,
+ screenshot: 0,
+ tippytop: 0,
+ rich_icon: 0,
+ no_image: 0
+ });
+}
+class _TopSites extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onEditFormClose = this.onEditFormClose.bind(this);
+ this.onSearchShortcutsFormClose = this.onSearchShortcutsFormClose.bind(this);
+ }
+
+ /**
+ * Dispatch session statistics about the quality of TopSites icons and pinned count.
+ */
+ _dispatchTopSitesStats() {
+ const topSites = this._getVisibleTopSites().filter(topSite => topSite !== null && topSite !== undefined);
+ const topSitesIconsStats = countTopSitesIconsTypes(topSites);
+ const topSitesPinned = topSites.filter(site => !!site.isPinned).length;
+ const searchShortcuts = topSites.filter(site => !!site.searchTopSite).length;
+ // Dispatch telemetry event with the count of TopSites images types.
+ this.props.dispatch(actionCreators.AlsoToMain({
+ type: actionTypes.SAVE_SESSION_PERF_DATA,
+ data: {
+ topsites_icon_stats: topSitesIconsStats,
+ topsites_pinned: topSitesPinned,
+ topsites_search_shortcuts: searchShortcuts
+ }
+ }));
+ }
+
+ /**
+ * Return the TopSites that are visible based on prefs and window width.
+ */
+ _getVisibleTopSites() {
+ // We hide 2 sites per row when not in the wide layout.
+ let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW;
+ // $break-point-widest = 1072px (from _variables.scss)
+ if (!__webpack_require__.g.matchMedia(`(min-width: 1072px)`).matches) {
+ sitesPerRow -= 2;
+ }
+ return this.props.TopSites.rows.slice(0, this.props.TopSitesRows * sitesPerRow);
+ }
+ componentDidUpdate() {
+ this._dispatchTopSitesStats();
+ }
+ componentDidMount() {
+ this._dispatchTopSitesStats();
+ }
+ onEditFormClose() {
+ this.props.dispatch(actionCreators.UserEvent({
+ source: TOP_SITES_SOURCE,
+ event: "TOP_SITES_EDIT_CLOSE"
+ }));
+ this.props.dispatch({
+ type: actionTypes.TOP_SITES_CANCEL_EDIT
+ });
+ }
+ onSearchShortcutsFormClose() {
+ this.props.dispatch(actionCreators.UserEvent({
+ source: TOP_SITES_SOURCE,
+ event: "SEARCH_EDIT_CLOSE"
+ }));
+ this.props.dispatch({
+ type: actionTypes.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL
+ });
+ }
+ render() {
+ const {
+ props
+ } = this;
+ const {
+ editForm,
+ showSearchShortcutsForm
+ } = props.TopSites;
+ const extraMenuOptions = ["AddTopSite"];
+ const colors = props.Prefs.values["newNewtabExperience.colors"];
+ if (props.Prefs.values["improvesearch.topSiteSearchShortcuts"]) {
+ extraMenuOptions.push("AddSearchShortcut");
+ }
+ return /*#__PURE__*/external_React_default().createElement(ComponentPerfTimer, {
+ id: "topsites",
+ initialized: props.TopSites.initialized,
+ dispatch: props.dispatch
+ }, /*#__PURE__*/external_React_default().createElement(CollapsibleSection, {
+ className: "top-sites",
+ id: "topsites",
+ title: props.title || {
+ id: "newtab-section-header-topsites"
+ },
+ hideTitle: true,
+ extraMenuOptions: extraMenuOptions,
+ showPrefName: "feeds.topsites",
+ eventSource: TOP_SITES_SOURCE,
+ collapsed: false,
+ isFixed: props.isFixed,
+ isFirst: props.isFirst,
+ isLast: props.isLast,
+ dispatch: props.dispatch
+ }, /*#__PURE__*/external_React_default().createElement(TopSiteList, {
+ TopSites: props.TopSites,
+ TopSitesRows: props.TopSitesRows,
+ dispatch: props.dispatch,
+ topSiteIconType: topSiteIconType,
+ colors: colors
+ }), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "edit-topsites-wrapper"
+ }, editForm && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "edit-topsites"
+ }, /*#__PURE__*/external_React_default().createElement(ModalOverlayWrapper, {
+ unstyled: true,
+ onClose: this.onEditFormClose,
+ innerClassName: "modal"
+ }, /*#__PURE__*/external_React_default().createElement(TopSiteForm, TopSites_extends({
+ site: props.TopSites.rows[editForm.index],
+ onClose: this.onEditFormClose,
+ dispatch: this.props.dispatch
+ }, editForm)))), showSearchShortcutsForm && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "edit-search-shortcuts"
+ }, /*#__PURE__*/external_React_default().createElement(ModalOverlayWrapper, {
+ unstyled: true,
+ onClose: this.onSearchShortcutsFormClose,
+ innerClassName: "modal"
+ }, /*#__PURE__*/external_React_default().createElement(SearchShortcutsForm, {
+ TopSites: props.TopSites,
+ onClose: this.onSearchShortcutsFormClose,
+ dispatch: this.props.dispatch
+ }))))));
+ }
+}
+const TopSites_TopSites = (0,external_ReactRedux_namespaceObject.connect)((state, props) => ({
+ TopSites: state.TopSites,
+ Prefs: state.Prefs,
+ TopSitesRows: state.Prefs.values.topSitesRows
+}))(_TopSites);
+;// CONCATENATED MODULE: ./content-src/components/Sections/Sections.jsx
+function Sections_extends() { Sections_extends = Object.assign ? Object.assign.bind() : 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; }; return Sections_extends.apply(this, arguments); }
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+
+
+
+
+const Sections_VISIBLE = "visible";
+const Sections_VISIBILITY_CHANGE_EVENT = "visibilitychange";
+const CARDS_PER_ROW_DEFAULT = 3;
+const CARDS_PER_ROW_COMPACT_WIDE = 4;
+class Section extends (external_React_default()).PureComponent {
+ get numRows() {
+ const {
+ rowsPref,
+ maxRows,
+ Prefs
+ } = this.props;
+ return rowsPref ? Prefs.values[rowsPref] : maxRows;
+ }
+ _dispatchImpressionStats() {
+ const {
+ props
+ } = this;
+ let cardsPerRow = CARDS_PER_ROW_DEFAULT;
+ if (props.compactCards && __webpack_require__.g.matchMedia(`(min-width: 1072px)`).matches) {
+ // If the section has compact cards and the viewport is wide enough, we show
+ // 4 columns instead of 3.
+ // $break-point-widest = 1072px (from _variables.scss)
+ cardsPerRow = CARDS_PER_ROW_COMPACT_WIDE;
+ }
+ const maxCards = cardsPerRow * this.numRows;
+ const cards = props.rows.slice(0, maxCards);
+ if (this.needsImpressionStats(cards)) {
+ props.dispatch(actionCreators.ImpressionStats({
+ source: props.eventSource,
+ tiles: cards.map(link => ({
+ id: link.guid
+ }))
+ }));
+ this.impressionCardGuids = cards.map(link => link.guid);
+ }
+ }
+
+ // This sends an event when a user sees a set of new content. If content
+ // changes while the page is hidden (i.e. preloaded or on a hidden tab),
+ // only send the event if the page becomes visible again.
+ sendImpressionStatsOrAddListener() {
+ const {
+ props
+ } = this;
+ if (!props.shouldSendImpressionStats || !props.dispatch) {
+ return;
+ }
+ if (props.document.visibilityState === Sections_VISIBLE) {
+ this._dispatchImpressionStats();
+ } else {
+ // We should only ever send the latest impression stats ping, so remove any
+ // older listeners.
+ if (this._onVisibilityChange) {
+ props.document.removeEventListener(Sections_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+
+ // When the page becomes visible, send the impression stats ping if the section isn't collapsed.
+ this._onVisibilityChange = () => {
+ if (props.document.visibilityState === Sections_VISIBLE) {
+ if (!this.props.pref.collapsed) {
+ this._dispatchImpressionStats();
+ }
+ props.document.removeEventListener(Sections_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ };
+ props.document.addEventListener(Sections_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+ componentWillMount() {
+ this.sendNewTabRehydrated(this.props.initialized);
+ }
+ componentDidMount() {
+ if (this.props.rows.length && !this.props.pref.collapsed) {
+ this.sendImpressionStatsOrAddListener();
+ }
+ }
+ componentDidUpdate(prevProps) {
+ const {
+ props
+ } = this;
+ const isCollapsed = props.pref.collapsed;
+ const wasCollapsed = prevProps.pref.collapsed;
+ if (
+ // Don't send impression stats for the empty state
+ props.rows.length && (
+ // We only want to send impression stats if the content of the cards has changed
+ // and the section is not collapsed...
+ props.rows !== prevProps.rows && !isCollapsed ||
+ // or if we are expanding a section that was collapsed.
+ wasCollapsed && !isCollapsed)) {
+ this.sendImpressionStatsOrAddListener();
+ }
+ }
+ componentWillUpdate(nextProps) {
+ this.sendNewTabRehydrated(nextProps.initialized);
+ }
+ componentWillUnmount() {
+ if (this._onVisibilityChange) {
+ this.props.document.removeEventListener(Sections_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+ needsImpressionStats(cards) {
+ if (!this.impressionCardGuids || this.impressionCardGuids.length !== cards.length) {
+ return true;
+ }
+ for (let i = 0; i < cards.length; i++) {
+ if (cards[i].guid !== this.impressionCardGuids[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // The NEW_TAB_REHYDRATED event is used to inform feeds that their
+ // data has been consumed e.g. for counting the number of tabs that
+ // have rendered that data.
+ sendNewTabRehydrated(initialized) {
+ if (initialized && !this.renderNotified) {
+ this.props.dispatch(actionCreators.AlsoToMain({
+ type: actionTypes.NEW_TAB_REHYDRATED,
+ data: {}
+ }));
+ this.renderNotified = true;
+ }
+ }
+ render() {
+ const {
+ id,
+ eventSource,
+ title,
+ rows,
+ Pocket,
+ topics,
+ emptyState,
+ dispatch,
+ compactCards,
+ read_more_endpoint,
+ contextMenuOptions,
+ initialized,
+ learnMore,
+ pref,
+ privacyNoticeURL,
+ isFirst,
+ isLast
+ } = this.props;
+ const waitingForSpoc = id === "topstories" && this.props.Pocket.waitingForSpoc;
+ const maxCardsPerRow = compactCards ? CARDS_PER_ROW_COMPACT_WIDE : CARDS_PER_ROW_DEFAULT;
+ const {
+ numRows
+ } = this;
+ const maxCards = maxCardsPerRow * numRows;
+ const maxCardsOnNarrow = CARDS_PER_ROW_DEFAULT * numRows;
+ const {
+ pocketCta,
+ isUserLoggedIn
+ } = Pocket || {};
+ const {
+ useCta
+ } = pocketCta || {};
+
+ // Don't display anything until we have a definitve result from Pocket,
+ // to avoid a flash of logged out state while we render.
+ const isPocketLoggedInDefined = isUserLoggedIn === true || isUserLoggedIn === false;
+ const hasTopics = topics && !!topics.length;
+ const shouldShowPocketCta = id === "topstories" && useCta && isUserLoggedIn === false;
+
+ // Show topics only for top stories and if it has loaded with topics.
+ // The classs .top-stories-bottom-container ensures content doesn't shift as things load.
+ const shouldShowTopics = id === "topstories" && hasTopics && (useCta && isUserLoggedIn === true || !useCta && isPocketLoggedInDefined);
+
+ // We use topics to determine language support for read more.
+ const shouldShowReadMore = read_more_endpoint && hasTopics;
+ const realRows = rows.slice(0, maxCards);
+
+ // The empty state should only be shown after we have initialized and there is no content.
+ // Otherwise, we should show placeholders.
+ const shouldShowEmptyState = initialized && !rows.length;
+ const cards = [];
+ if (!shouldShowEmptyState) {
+ for (let i = 0; i < maxCards; i++) {
+ const link = realRows[i];
+ // On narrow viewports, we only show 3 cards per row. We'll mark the rest as
+ // .hide-for-narrow to hide in CSS via @media query.
+ const className = i >= maxCardsOnNarrow ? "hide-for-narrow" : "";
+ let usePlaceholder = !link;
+ // If we are in the third card and waiting for spoc,
+ // use the placeholder.
+ if (!usePlaceholder && i === 2 && waitingForSpoc) {
+ usePlaceholder = true;
+ }
+ cards.push(!usePlaceholder ? /*#__PURE__*/external_React_default().createElement(Card, {
+ key: i,
+ index: i,
+ className: className,
+ dispatch: dispatch,
+ link: link,
+ contextMenuOptions: contextMenuOptions,
+ eventSource: eventSource,
+ shouldSendImpressionStats: this.props.shouldSendImpressionStats,
+ isWebExtension: this.props.isWebExtension
+ }) : /*#__PURE__*/external_React_default().createElement(PlaceholderCard, {
+ key: i,
+ className: className
+ }));
+ }
+ }
+ const sectionClassName = ["section", compactCards ? "compact-cards" : "normal-cards"].join(" ");
+
+ // <Section> <-- React component
+ // <section> <-- HTML5 element
+ return /*#__PURE__*/external_React_default().createElement(ComponentPerfTimer, this.props, /*#__PURE__*/external_React_default().createElement(CollapsibleSection, {
+ className: sectionClassName,
+ title: title,
+ id: id,
+ eventSource: eventSource,
+ collapsed: this.props.pref.collapsed,
+ showPrefName: pref && pref.feed || id,
+ privacyNoticeURL: privacyNoticeURL,
+ Prefs: this.props.Prefs,
+ isFixed: this.props.isFixed,
+ isFirst: isFirst,
+ isLast: isLast,
+ learnMore: learnMore,
+ dispatch: this.props.dispatch,
+ isWebExtension: this.props.isWebExtension
+ }, !shouldShowEmptyState && /*#__PURE__*/external_React_default().createElement("ul", {
+ className: "section-list",
+ style: {
+ padding: 0
+ }
+ }, cards), shouldShowEmptyState && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "section-empty-state"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "empty-state"
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: emptyState.message
+ }, /*#__PURE__*/external_React_default().createElement("p", {
+ className: "empty-state-message"
+ })))), id === "topstories" && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "top-stories-bottom-container"
+ }, shouldShowTopics && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "wrapper-topics"
+ }, /*#__PURE__*/external_React_default().createElement(Topics, {
+ topics: this.props.topics
+ })), shouldShowPocketCta && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "wrapper-cta"
+ }, /*#__PURE__*/external_React_default().createElement(PocketLoggedInCta, null)), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "wrapper-more-recommendations"
+ }, shouldShowReadMore && /*#__PURE__*/external_React_default().createElement(MoreRecommendations, {
+ read_more_endpoint: read_more_endpoint
+ })))));
+ }
+}
+Section.defaultProps = {
+ document: __webpack_require__.g.document,
+ rows: [],
+ emptyState: {},
+ pref: {},
+ title: ""
+};
+const SectionIntl = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Prefs: state.Prefs,
+ Pocket: state.Pocket
+}))(Section);
+class _Sections extends (external_React_default()).PureComponent {
+ renderSections() {
+ const sections = [];
+ const enabledSections = this.props.Sections.filter(section => section.enabled);
+ const {
+ sectionOrder,
+ "feeds.topsites": showTopSites
+ } = this.props.Prefs.values;
+ // Enabled sections doesn't include Top Sites, so we add it if enabled.
+ const expectedCount = enabledSections.length + ~~showTopSites;
+ for (const sectionId of sectionOrder.split(",")) {
+ const commonProps = {
+ key: sectionId,
+ isFirst: sections.length === 0,
+ isLast: sections.length === expectedCount - 1
+ };
+ if (sectionId === "topsites" && showTopSites) {
+ sections.push( /*#__PURE__*/external_React_default().createElement(TopSites_TopSites, commonProps));
+ } else {
+ const section = enabledSections.find(s => s.id === sectionId);
+ if (section) {
+ sections.push( /*#__PURE__*/external_React_default().createElement(SectionIntl, Sections_extends({}, section, commonProps)));
+ }
+ }
+ }
+ return sections;
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "sections-list"
+ }, this.renderSections());
+ }
+}
+const Sections_Sections = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Sections: state.Sections,
+ Prefs: state.Prefs
+}))(_Sections);
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx
+function Highlights_extends() { Highlights_extends = Object.assign ? Object.assign.bind() : 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; }; return Highlights_extends.apply(this, arguments); }
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+class _Highlights extends (external_React_default()).PureComponent {
+ render() {
+ const section = this.props.Sections.find(s => s.id === "highlights");
+ if (!section || !section.enabled) {
+ return null;
+ }
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-highlights sections-list"
+ }, /*#__PURE__*/external_React_default().createElement(SectionIntl, Highlights_extends({}, section, {
+ isFixed: true
+ })));
+ }
+}
+const Highlights = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Sections: state.Sections
+}))(_Highlights);
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class HorizontalRule extends (external_React_default()).PureComponent {
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("hr", {
+ className: "ds-hr"
+ });
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+class Navigation_Topic extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onLinkClick = this.onLinkClick.bind(this);
+ }
+ onLinkClick(event) {
+ if (this.props.dispatch) {
+ this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "POPULAR_TOPICS",
+ action_position: 0,
+ value: {
+ topic: event.target.text.toLowerCase().replace(` `, `-`)
+ }
+ }));
+ }
+ }
+ render() {
+ const {
+ url,
+ name
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ onLinkClick: this.onLinkClick,
+ className: this.props.className,
+ url: url
+ }, name);
+ }
+}
+class Navigation extends (external_React_default()).PureComponent {
+ render() {
+ let links = this.props.links || [];
+ const alignment = this.props.alignment || "centered";
+ const header = this.props.header || {};
+ const english = this.props.locale.startsWith("en-");
+ const privacyNotice = this.props.privacyNoticeURL || {};
+ const {
+ newFooterSection
+ } = this.props;
+ const className = `ds-navigation ds-navigation-${alignment} ${newFooterSection ? `ds-navigation-new-topics` : ``}`;
+ let {
+ title
+ } = header;
+ if (newFooterSection) {
+ title = {
+ id: "newtab-pocket-new-topics-title"
+ };
+ if (this.props.extraLinks) {
+ links = [...links.slice(0, links.length - 1), ...this.props.extraLinks, links[links.length - 1]];
+ }
+ }
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: className
+ }, title && english ? /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: title
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "ds-navigation-header"
+ })) : null, english ? /*#__PURE__*/external_React_default().createElement("ul", null, links && links.map(t => /*#__PURE__*/external_React_default().createElement("li", {
+ key: t.name
+ }, /*#__PURE__*/external_React_default().createElement(Navigation_Topic, {
+ url: t.url,
+ name: t.name,
+ dispatch: this.props.dispatch
+ })))) : null, !newFooterSection ? /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ className: "ds-navigation-privacy",
+ url: privacyNotice.url
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: privacyNotice.title
+ })) : null, newFooterSection ? /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-navigation-family"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "icon firefox-logo"
+ }), /*#__PURE__*/external_React_default().createElement("span", null, "|"), /*#__PURE__*/external_React_default().createElement("span", {
+ className: "icon pocket-logo"
+ }), /*#__PURE__*/external_React_default().createElement("span", {
+ className: "ds-navigation-family-message",
+ "data-l10n-id": "newtab-pocket-pocket-firefox-family"
+ })) : null);
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+class PrivacyLink extends (external_React_default()).PureComponent {
+ render() {
+ const {
+ properties
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-privacy-link"
+ }, /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+ url: properties.url
+ }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+ message: properties.title
+ })));
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class SectionTitle extends (external_React_default()).PureComponent {
+ render() {
+ const {
+ header: {
+ title,
+ subtitle
+ }
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-section-title"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "title"
+ }, title), subtitle ? /*#__PURE__*/external_React_default().createElement("div", {
+ className: "subtitle"
+ }, subtitle) : null);
+ }
+}
+;// CONCATENATED MODULE: ./content-src/lib/selectLayoutRender.js
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const selectLayoutRender = ({
+ state = {},
+ prefs = {},
+ locale = ""
+}) => {
+ const {
+ layout,
+ feeds,
+ spocs
+ } = state;
+ let spocIndexPlacementMap = {};
+
+ /* This function fills spoc positions on a per placement basis with available spocs.
+ * It does this by looping through each position for a placement and replacing a rec with a spoc.
+ * If it runs out of spocs or positions, it stops.
+ * If it sees the same placement again, it remembers the previous spoc index, and continues.
+ * If it sees a blocked spoc, it skips that position leaving in a regular story.
+ */
+ function fillSpocPositionsForPlacement(data, spocsConfig, spocsData, placementName) {
+ if (!spocIndexPlacementMap[placementName] && spocIndexPlacementMap[placementName] !== 0) {
+ spocIndexPlacementMap[placementName] = 0;
+ }
+ const results = [...data];
+ for (let position of spocsConfig.positions) {
+ const spoc = spocsData[spocIndexPlacementMap[placementName]];
+ // If there are no spocs left, we can stop filling positions.
+ if (!spoc) {
+ break;
+ }
+
+ // A placement could be used in two sections.
+ // In these cases, we want to maintain the index of the previous section.
+ // If we didn't do this, it might duplicate spocs.
+ spocIndexPlacementMap[placementName]++;
+
+ // A spoc that's blocked is removed from the source for subsequent newtab loads.
+ // If we have a spoc in the source that's blocked, it means it was *just* blocked,
+ // and in this case, we skip this position, and show a regular spoc instead.
+ if (!spocs.blocked.includes(spoc.url)) {
+ results.splice(position.index, 0, spoc);
+ }
+ }
+ return results;
+ }
+ const positions = {};
+ const DS_COMPONENTS = ["Message", "TextPromo", "SectionTitle", "Signup", "Navigation", "CardGrid", "CollectionCardGrid", "HorizontalRule", "PrivacyLink"];
+ const filterArray = [];
+ if (!prefs["feeds.topsites"]) {
+ filterArray.push("TopSites");
+ }
+ const pocketEnabled = prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"];
+ if (!pocketEnabled) {
+ filterArray.push(...DS_COMPONENTS);
+ }
+ const placeholderComponent = component => {
+ if (!component.feed) {
+ // TODO we now need a placeholder for topsites and textPromo.
+ return {
+ ...component,
+ data: {
+ spocs: []
+ }
+ };
+ }
+ const data = {
+ recommendations: []
+ };
+ let items = 0;
+ if (component.properties && component.properties.items) {
+ items = component.properties.items;
+ }
+ for (let i = 0; i < items; i++) {
+ data.recommendations.push({
+ placeholder: true
+ });
+ }
+ return {
+ ...component,
+ data
+ };
+ };
+
+ // TODO update devtools to show placements
+ const handleSpocs = (data, component) => {
+ let result = [...data];
+ // Do we ever expect to possibly have a spoc.
+ if (component.spocs && component.spocs.positions && component.spocs.positions.length) {
+ const placement = component.placement || {};
+ const placementName = placement.name || "spocs";
+ const spocsData = spocs.data[placementName];
+ // We expect a spoc, spocs are loaded, and the server returned spocs.
+ if (spocs.loaded && spocsData && spocsData.items && spocsData.items.length) {
+ result = fillSpocPositionsForPlacement(result, component.spocs, spocsData.items, placementName);
+ }
+ }
+ return result;
+ };
+ const handleComponent = component => {
+ if (component.spocs && component.spocs.positions && component.spocs.positions.length) {
+ const placement = component.placement || {};
+ const placementName = placement.name || "spocs";
+ const spocsData = spocs.data[placementName];
+ if (spocs.loaded && spocsData && spocsData.items && spocsData.items.length) {
+ return {
+ ...component,
+ data: {
+ spocs: spocsData.items.filter(spoc => spoc && !spocs.blocked.includes(spoc.url)).map((spoc, index) => ({
+ ...spoc,
+ pos: index
+ }))
+ }
+ };
+ }
+ }
+ return {
+ ...component,
+ data: {
+ spocs: []
+ }
+ };
+ };
+ const handleComponentWithFeed = component => {
+ positions[component.type] = positions[component.type] || 0;
+ let data = {
+ recommendations: []
+ };
+ const feed = feeds.data[component.feed.url];
+ if (feed && feed.data) {
+ data = {
+ ...feed.data,
+ recommendations: [...(feed.data.recommendations || [])]
+ };
+ }
+ if (component && component.properties && component.properties.offset) {
+ data = {
+ ...data,
+ recommendations: data.recommendations.slice(component.properties.offset)
+ };
+ }
+ data = {
+ ...data,
+ recommendations: handleSpocs(data.recommendations, component)
+ };
+ let items = 0;
+ if (component.properties && component.properties.items) {
+ items = Math.min(component.properties.items, data.recommendations.length);
+ }
+
+ // loop through a component items
+ // Store the items position sequentially for multiple components of the same type.
+ // Example: A second card grid starts pos offset from the last card grid.
+ for (let i = 0; i < items; i++) {
+ data.recommendations[i] = {
+ ...data.recommendations[i],
+ pos: positions[component.type]++
+ };
+ }
+ return {
+ ...component,
+ data
+ };
+ };
+ const renderLayout = () => {
+ const renderedLayoutArray = [];
+ for (const row of layout.filter(r => r.components.filter(c => !filterArray.includes(c.type)).length)) {
+ let components = [];
+ renderedLayoutArray.push({
+ ...row,
+ components
+ });
+ for (const component of row.components.filter(c => !filterArray.includes(c.type))) {
+ const spocsConfig = component.spocs;
+ if (spocsConfig || component.feed) {
+ // TODO make sure this still works for different loading cases.
+ if (component.feed && !feeds.data[component.feed.url] || spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded) {
+ components.push(placeholderComponent(component));
+ return renderedLayoutArray;
+ }
+ if (component.feed) {
+ components.push(handleComponentWithFeed(component));
+ } else {
+ components.push(handleComponent(component));
+ }
+ } else {
+ components.push(component);
+ }
+ }
+ }
+ return renderedLayoutArray;
+ };
+ const layoutRender = renderLayout();
+ return {
+ layoutRender
+ };
+};
+;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+const ALLOWED_CSS_URL_PREFIXES = ["chrome://", "resource://", "https://img-getpocket.cdn.mozilla.net/"];
+const DUMMY_CSS_SELECTOR = "DUMMY#CSS.SELECTOR";
+
+/**
+ * Validate a CSS declaration. The values are assumed to be normalized by CSSOM.
+ */
+function isAllowedCSS(property, value) {
+ // Bug 1454823: INTERNAL properties, e.g., -moz-context-properties, are
+ // exposed but their values aren't resulting in getting nothing. Fortunately,
+ // we don't care about validating the values of the current set of properties.
+ if (value === undefined) {
+ return true;
+ }
+
+ // Make sure all urls are of the allowed protocols/prefixes
+ const urls = value.match(/url\("[^"]+"\)/g);
+ return !urls || urls.every(url => ALLOWED_CSS_URL_PREFIXES.some(prefix => url.slice(5).startsWith(prefix)));
+}
+class _DiscoveryStreamBase extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onStyleMount = this.onStyleMount.bind(this);
+ }
+ onStyleMount(style) {
+ // Unmounting style gets rid of old styles, so nothing else to do
+ if (!style) {
+ return;
+ }
+ const {
+ sheet
+ } = style;
+ const styles = JSON.parse(style.dataset.styles);
+ styles.forEach((row, rowIndex) => {
+ row.forEach((component, componentIndex) => {
+ // Nothing to do without optional styles overrides
+ if (!component) {
+ return;
+ }
+ Object.entries(component).forEach(([selectors, declarations]) => {
+ // Start with a dummy rule to validate declarations and selectors
+ sheet.insertRule(`${DUMMY_CSS_SELECTOR} {}`);
+ const [rule] = sheet.cssRules;
+
+ // Validate declarations and remove any offenders. CSSOM silently
+ // discards invalid entries, so here we apply extra restrictions.
+ rule.style = declarations;
+ [...rule.style].forEach(property => {
+ const value = rule.style[property];
+ if (!isAllowedCSS(property, value)) {
+ console.error(`Bad CSS declaration ${property}: ${value}`);
+ rule.style.removeProperty(property);
+ }
+ });
+
+ // Set the actual desired selectors scoped to the component
+ const prefix = `.ds-layout > .ds-column:nth-child(${rowIndex + 1}) .ds-column-grid > :nth-child(${componentIndex + 1})`;
+ // NB: Splitting on "," doesn't work with strings with commas, but
+ // we're okay with not supporting those selectors
+ rule.selectorText = selectors.split(",").map(selector => prefix + (
+ // Assume :pseudo-classes are for component instead of descendant
+ selector[0] === ":" ? "" : " ") + selector).join(",");
+
+ // CSSOM silently ignores bad selectors, so we'll be noisy instead
+ if (rule.selectorText === DUMMY_CSS_SELECTOR) {
+ console.error(`Bad CSS selector ${selectors}`);
+ }
+ });
+ });
+ });
+ }
+ renderComponent(component, embedWidth) {
+ switch (component.type) {
+ case "Highlights":
+ return /*#__PURE__*/external_React_default().createElement(Highlights, null);
+ case "TopSites":
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-top-sites"
+ }, /*#__PURE__*/external_React_default().createElement(TopSites_TopSites, {
+ isFixed: true,
+ title: component.header?.title
+ }));
+ case "TextPromo":
+ return /*#__PURE__*/external_React_default().createElement(DSTextPromo, {
+ dispatch: this.props.dispatch,
+ type: component.type,
+ data: component.data
+ });
+ case "Signup":
+ return /*#__PURE__*/external_React_default().createElement(DSSignup, {
+ dispatch: this.props.dispatch,
+ type: component.type,
+ data: component.data
+ });
+ case "Message":
+ return /*#__PURE__*/external_React_default().createElement(DSMessage, {
+ title: component.header && component.header.title,
+ subtitle: component.header && component.header.subtitle,
+ link_text: component.header && component.header.link_text,
+ link_url: component.header && component.header.link_url,
+ icon: component.header && component.header.icon,
+ essentialReadsHeader: component.essentialReadsHeader,
+ editorsPicksHeader: component.editorsPicksHeader
+ });
+ case "SectionTitle":
+ return /*#__PURE__*/external_React_default().createElement(SectionTitle, {
+ header: component.header
+ });
+ case "Navigation":
+ return /*#__PURE__*/external_React_default().createElement(Navigation, {
+ dispatch: this.props.dispatch,
+ links: component.properties.links,
+ extraLinks: component.properties.extraLinks,
+ alignment: component.properties.alignment,
+ explore_topics: component.properties.explore_topics,
+ header: component.header,
+ locale: this.props.App.locale,
+ newFooterSection: component.newFooterSection,
+ privacyNoticeURL: component.properties.privacyNoticeURL
+ });
+ case "CollectionCardGrid":
+ const {
+ DiscoveryStream
+ } = this.props;
+ return /*#__PURE__*/external_React_default().createElement(CollectionCardGrid, {
+ data: component.data,
+ feed: component.feed,
+ spocs: DiscoveryStream.spocs,
+ placement: component.placement,
+ type: component.type,
+ items: component.properties.items,
+ dismissible: this.props.DiscoveryStream.isCollectionDismissible,
+ dispatch: this.props.dispatch
+ });
+ case "CardGrid":
+ return /*#__PURE__*/external_React_default().createElement(CardGrid, {
+ title: component.header && component.header.title,
+ data: component.data,
+ feed: component.feed,
+ widgets: component.widgets,
+ type: component.type,
+ dispatch: this.props.dispatch,
+ items: component.properties.items,
+ hybridLayout: component.properties.hybridLayout,
+ hideCardBackground: component.properties.hideCardBackground,
+ fourCardLayout: component.properties.fourCardLayout,
+ compactGrid: component.properties.compactGrid,
+ essentialReadsHeader: component.properties.essentialReadsHeader,
+ onboardingExperience: component.properties.onboardingExperience,
+ ctaButtonSponsors: component.properties.ctaButtonSponsors,
+ ctaButtonVariant: component.properties.ctaButtonVariant,
+ editorsPicksHeader: component.properties.editorsPicksHeader,
+ recentSavesEnabled: this.props.DiscoveryStream.recentSavesEnabled,
+ hideDescriptions: this.props.DiscoveryStream.hideDescriptions
+ });
+ case "HorizontalRule":
+ return /*#__PURE__*/external_React_default().createElement(HorizontalRule, null);
+ case "PrivacyLink":
+ return /*#__PURE__*/external_React_default().createElement(PrivacyLink, {
+ properties: component.properties
+ });
+ default:
+ return /*#__PURE__*/external_React_default().createElement("div", null, component.type);
+ }
+ }
+ renderStyles(styles) {
+ // Use json string as both the key and styles to render so React knows when
+ // to unmount and mount a new instance for new styles.
+ const json = JSON.stringify(styles);
+ return /*#__PURE__*/external_React_default().createElement("style", {
+ key: json,
+ "data-styles": json,
+ ref: this.onStyleMount
+ });
+ }
+ render() {
+ const {
+ locale
+ } = this.props;
+ // Select layout render data by adding spocs and position to recommendations
+ const {
+ layoutRender
+ } = selectLayoutRender({
+ state: this.props.DiscoveryStream,
+ prefs: this.props.Prefs.values,
+ locale
+ });
+ const {
+ config
+ } = this.props.DiscoveryStream;
+
+ // Allow rendering without extracting special components
+ if (!config.collapsible) {
+ return this.renderLayout(layoutRender);
+ }
+
+ // Find the first component of a type and remove it from layout
+ const extractComponent = type => {
+ for (const [rowIndex, row] of Object.entries(layoutRender)) {
+ for (const [index, component] of Object.entries(row.components)) {
+ if (component.type === type) {
+ // Remove the row if it was the only component or the single item
+ if (row.components.length === 1) {
+ layoutRender.splice(rowIndex, 1);
+ } else {
+ row.components.splice(index, 1);
+ }
+ return component;
+ }
+ }
+ }
+ return null;
+ };
+
+ // Get "topstories" Section state for default values
+ const topStories = this.props.Sections.find(s => s.id === "topstories");
+ if (!topStories) {
+ return null;
+ }
+
+ // Extract TopSites to render before the rest and Message to use for header
+ const topSites = extractComponent("TopSites");
+ const sponsoredCollection = extractComponent("CollectionCardGrid");
+ const message = extractComponent("Message") || {
+ header: {
+ link_text: topStories.learnMore.link.message,
+ link_url: topStories.learnMore.link.href,
+ title: topStories.title
+ }
+ };
+ const privacyLinkComponent = extractComponent("PrivacyLink");
+ let learnMore = {
+ link: {
+ href: message.header.link_url,
+ message: message.header.link_text
+ }
+ };
+ let sectionTitle = message.header.title;
+ let subTitle = "";
+
+ // If we're in one of these experiments, override the default message.
+ // For now this is English only.
+ if (message.essentialReadsHeader || message.editorsPicksHeader) {
+ learnMore = null;
+ subTitle = "Recommended By Pocket";
+ if (message.essentialReadsHeader) {
+ sectionTitle = "Today’s Essential Reads";
+ } else if (message.editorsPicksHeader) {
+ sectionTitle = "Editor’s Picks";
+ }
+ }
+
+ // Render a DS-style TopSites then the rest if any in a collapsible section
+ return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, this.props.DiscoveryStream.isPrivacyInfoModalVisible && /*#__PURE__*/external_React_default().createElement(DSPrivacyModal, {
+ dispatch: this.props.dispatch
+ }), topSites && this.renderLayout([{
+ width: 12,
+ components: [topSites]
+ }]), sponsoredCollection && this.renderLayout([{
+ width: 12,
+ components: [sponsoredCollection]
+ }]), !!layoutRender.length && /*#__PURE__*/external_React_default().createElement(CollapsibleSection, {
+ className: "ds-layout",
+ collapsed: topStories.pref.collapsed,
+ dispatch: this.props.dispatch,
+ id: topStories.id,
+ isFixed: true,
+ learnMore: learnMore,
+ privacyNoticeURL: topStories.privacyNoticeURL,
+ showPrefName: topStories.pref.feed,
+ title: sectionTitle,
+ subTitle: subTitle,
+ eventSource: "CARDGRID"
+ }, this.renderLayout(layoutRender)), this.renderLayout([{
+ width: 12,
+ components: [{
+ type: "Highlights"
+ }]
+ }]), privacyLinkComponent && this.renderLayout([{
+ width: 12,
+ components: [privacyLinkComponent]
+ }]));
+ }
+ renderLayout(layoutRender) {
+ const styles = [];
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "discovery-stream ds-layout"
+ }, layoutRender.map((row, rowIndex) => /*#__PURE__*/external_React_default().createElement("div", {
+ key: `row-${rowIndex}`,
+ className: `ds-column ds-column-${row.width}`
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "ds-column-grid"
+ }, row.components.map((component, componentIndex) => {
+ if (!component) {
+ return null;
+ }
+ styles[rowIndex] = [...(styles[rowIndex] || []), component.styles];
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ key: `component-${componentIndex}`
+ }, this.renderComponent(component, row.width));
+ })))), this.renderStyles(styles));
+ }
+}
+const DiscoveryStreamBase = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ DiscoveryStream: state.DiscoveryStream,
+ Prefs: state.Prefs,
+ Sections: state.Sections,
+ document: __webpack_require__.g.document,
+ App: state.App
+}))(_DiscoveryStreamBase);
+;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+class BackgroundsSection extends (external_React_default()).PureComponent {
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("div", null);
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+class ContentSection extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onPreferenceSelect = this.onPreferenceSelect.bind(this);
+
+ // Refs are necessary for dynamically measuring drawer heights for slide animations
+ this.topSitesDrawerRef = /*#__PURE__*/external_React_default().createRef();
+ this.pocketDrawerRef = /*#__PURE__*/external_React_default().createRef();
+ }
+ inputUserEvent(eventSource, status) {
+ this.props.dispatch(actionCreators.UserEvent({
+ event: "PREF_CHANGED",
+ source: eventSource,
+ value: {
+ status,
+ menu_source: "CUSTOMIZE_MENU"
+ }
+ }));
+ }
+ onPreferenceSelect(e) {
+ // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS
+ const {
+ preference,
+ eventSource
+ } = e.target.dataset;
+ let value;
+ if (e.target.nodeName === "SELECT") {
+ value = parseInt(e.target.value, 10);
+ } else if (e.target.nodeName === "INPUT") {
+ value = e.target.checked;
+ if (eventSource) {
+ this.inputUserEvent(eventSource, value);
+ }
+ } else if (e.target.nodeName === "MOZ-TOGGLE") {
+ value = e.target.pressed;
+ if (eventSource) {
+ this.inputUserEvent(eventSource, value);
+ }
+ }
+ this.props.setPref(preference, value);
+ }
+ componentDidMount() {
+ this.setDrawerMargins();
+ }
+ componentDidUpdate() {
+ this.setDrawerMargins();
+ }
+ setDrawerMargins() {
+ this.setDrawerMargin(`TOP_SITES`, this.props.enabledSections.topSitesEnabled);
+ this.setDrawerMargin(`TOP_STORIES`, this.props.enabledSections.pocketEnabled);
+ }
+ setDrawerMargin(drawerID, isOpen) {
+ let drawerRef;
+ if (drawerID === `TOP_SITES`) {
+ drawerRef = this.topSitesDrawerRef.current;
+ } else if (drawerID === `TOP_STORIES`) {
+ drawerRef = this.pocketDrawerRef.current;
+ } else {
+ return;
+ }
+ let drawerHeight;
+ if (drawerRef) {
+ drawerHeight = parseFloat(window.getComputedStyle(drawerRef)?.height);
+ if (isOpen) {
+ drawerRef.style.marginTop = `0`;
+ } else {
+ drawerRef.style.marginTop = `-${drawerHeight}px`;
+ }
+ }
+ }
+ render() {
+ const {
+ enabledSections,
+ mayHaveSponsoredTopSites,
+ pocketRegion,
+ mayHaveSponsoredStories,
+ mayHaveRecentSaves,
+ openPreferences
+ } = this.props;
+ const {
+ topSitesEnabled,
+ pocketEnabled,
+ highlightsEnabled,
+ showSponsoredTopSitesEnabled,
+ showSponsoredPocketEnabled,
+ showRecentSavesEnabled,
+ topSitesRowsCount
+ } = enabledSections;
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: "home-section"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ id: "shortcuts-section",
+ className: "section"
+ }, /*#__PURE__*/external_React_default().createElement("moz-toggle", {
+ id: "shortcuts-toggle",
+ pressed: topSitesEnabled || null,
+ onToggle: this.onPreferenceSelect,
+ "data-preference": "feeds.topsites",
+ "data-eventSource": "TOP_SITES",
+ "data-l10n-id": "newtab-custom-shortcuts-toggle",
+ "data-l10n-attrs": "label, description"
+ }), /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "more-info-top-wrapper"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "more-information",
+ ref: this.topSitesDrawerRef
+ }, /*#__PURE__*/external_React_default().createElement("select", {
+ id: "row-selector",
+ className: "selector",
+ name: "row-count",
+ "data-preference": "topSitesRows",
+ value: topSitesRowsCount,
+ onChange: this.onPreferenceSelect,
+ disabled: !topSitesEnabled,
+ "aria-labelledby": "custom-shortcuts-title"
+ }, /*#__PURE__*/external_React_default().createElement("option", {
+ value: "1",
+ "data-l10n-id": "newtab-custom-row-selector",
+ "data-l10n-args": "{\"num\": 1}"
+ }), /*#__PURE__*/external_React_default().createElement("option", {
+ value: "2",
+ "data-l10n-id": "newtab-custom-row-selector",
+ "data-l10n-args": "{\"num\": 2}"
+ }), /*#__PURE__*/external_React_default().createElement("option", {
+ value: "3",
+ "data-l10n-id": "newtab-custom-row-selector",
+ "data-l10n-args": "{\"num\": 3}"
+ }), /*#__PURE__*/external_React_default().createElement("option", {
+ value: "4",
+ "data-l10n-id": "newtab-custom-row-selector",
+ "data-l10n-args": "{\"num\": 4}"
+ })), mayHaveSponsoredTopSites && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "check-wrapper",
+ role: "presentation"
+ }, /*#__PURE__*/external_React_default().createElement("input", {
+ id: "sponsored-shortcuts",
+ className: "sponsored-checkbox",
+ disabled: !topSitesEnabled,
+ checked: showSponsoredTopSitesEnabled,
+ type: "checkbox",
+ onChange: this.onPreferenceSelect,
+ "data-preference": "showSponsoredTopSites",
+ "data-eventSource": "SPONSORED_TOP_SITES"
+ }), /*#__PURE__*/external_React_default().createElement("label", {
+ className: "sponsored",
+ htmlFor: "sponsored-shortcuts",
+ "data-l10n-id": "newtab-custom-sponsored-sites"
+ })))))), pocketRegion && /*#__PURE__*/external_React_default().createElement("div", {
+ id: "pocket-section",
+ className: "section"
+ }, /*#__PURE__*/external_React_default().createElement("label", {
+ className: "switch"
+ }, /*#__PURE__*/external_React_default().createElement("moz-toggle", {
+ id: "pocket-toggle",
+ pressed: pocketEnabled || null,
+ onToggle: this.onPreferenceSelect,
+ "aria-describedby": "custom-pocket-subtitle",
+ "data-preference": "feeds.section.topstories",
+ "data-eventSource": "TOP_STORIES",
+ "data-l10n-id": "newtab-custom-stories-toggle",
+ "data-l10n-attrs": "label, description"
+ })), /*#__PURE__*/external_React_default().createElement("div", null, (mayHaveSponsoredStories || mayHaveRecentSaves) && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "more-info-pocket-wrapper"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "more-information",
+ ref: this.pocketDrawerRef
+ }, mayHaveSponsoredStories && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "check-wrapper",
+ role: "presentation"
+ }, /*#__PURE__*/external_React_default().createElement("input", {
+ id: "sponsored-pocket",
+ className: "sponsored-checkbox",
+ disabled: !pocketEnabled,
+ checked: showSponsoredPocketEnabled,
+ type: "checkbox",
+ onChange: this.onPreferenceSelect,
+ "data-preference": "showSponsored",
+ "data-eventSource": "POCKET_SPOCS"
+ }), /*#__PURE__*/external_React_default().createElement("label", {
+ className: "sponsored",
+ htmlFor: "sponsored-pocket",
+ "data-l10n-id": "newtab-custom-pocket-sponsored"
+ })), mayHaveRecentSaves && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "check-wrapper",
+ role: "presentation"
+ }, /*#__PURE__*/external_React_default().createElement("input", {
+ id: "recent-saves-pocket",
+ className: "sponsored-checkbox",
+ disabled: !pocketEnabled,
+ checked: showRecentSavesEnabled,
+ type: "checkbox",
+ onChange: this.onPreferenceSelect,
+ "data-preference": "showRecentSaves",
+ "data-eventSource": "POCKET_RECENT_SAVES"
+ }), /*#__PURE__*/external_React_default().createElement("label", {
+ className: "sponsored",
+ htmlFor: "recent-saves-pocket",
+ "data-l10n-id": "newtab-custom-pocket-show-recent-saves"
+ })))))), /*#__PURE__*/external_React_default().createElement("div", {
+ id: "recent-section",
+ className: "section"
+ }, /*#__PURE__*/external_React_default().createElement("label", {
+ className: "switch"
+ }, /*#__PURE__*/external_React_default().createElement("moz-toggle", {
+ id: "highlights-toggle",
+ pressed: highlightsEnabled || null,
+ onToggle: this.onPreferenceSelect,
+ "data-preference": "feeds.section.highlights",
+ "data-eventSource": "HIGHLIGHTS",
+ "data-l10n-id": "newtab-custom-recent-toggle",
+ "data-l10n-attrs": "label, description"
+ }))), /*#__PURE__*/external_React_default().createElement("span", {
+ className: "divider",
+ role: "separator"
+ }), /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("button", {
+ id: "settings-link",
+ className: "external-link",
+ onClick: openPreferences,
+ "data-l10n-id": "newtab-custom-settings"
+ })));
+ }
+}
+;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/CustomizeMenu.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+class _CustomizeMenu extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onEntered = this.onEntered.bind(this);
+ this.onExited = this.onExited.bind(this);
+ }
+ onEntered() {
+ if (this.closeButton) {
+ this.closeButton.focus();
+ }
+ }
+ onExited() {
+ if (this.openButton) {
+ this.openButton.focus();
+ }
+ }
+ render() {
+ return /*#__PURE__*/external_React_default().createElement("span", null, /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.CSSTransition, {
+ timeout: 300,
+ classNames: "personalize-animate",
+ in: !this.props.showing,
+ appear: true
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ className: "icon icon-settings personalize-button",
+ onClick: () => this.props.onOpen(),
+ "data-l10n-id": "newtab-personalize-icon-label",
+ ref: c => this.openButton = c
+ })), /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.CSSTransition, {
+ timeout: 250,
+ classNames: "customize-animate",
+ in: this.props.showing,
+ onEntered: this.onEntered,
+ onExited: this.onExited,
+ appear: true
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "customize-menu",
+ role: "dialog",
+ "data-l10n-id": "newtab-personalize-dialog-label"
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ onClick: () => this.props.onClose(),
+ className: "close-button",
+ "data-l10n-id": "newtab-custom-close-button",
+ ref: c => this.closeButton = c
+ }), /*#__PURE__*/external_React_default().createElement(BackgroundsSection, null), /*#__PURE__*/external_React_default().createElement(ContentSection, {
+ openPreferences: this.props.openPreferences,
+ setPref: this.props.setPref,
+ enabledSections: this.props.enabledSections,
+ pocketRegion: this.props.pocketRegion,
+ mayHaveSponsoredTopSites: this.props.mayHaveSponsoredTopSites,
+ mayHaveSponsoredStories: this.props.mayHaveSponsoredStories,
+ mayHaveRecentSaves: this.props.DiscoveryStream.recentSavesEnabled,
+ dispatch: this.props.dispatch
+ }))));
+ }
+}
+const CustomizeMenu = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ DiscoveryStream: state.DiscoveryStream
+}))(_CustomizeMenu);
+;// CONCATENATED MODULE: ./content-src/lib/constants.js
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const IS_NEWTAB = __webpack_require__.g.document && __webpack_require__.g.document.documentURI === "about:newtab";
+const NEWTAB_DARK_THEME = {
+ ntp_background: {
+ r: 42,
+ g: 42,
+ b: 46,
+ a: 1
+ },
+ ntp_card_background: {
+ r: 66,
+ g: 65,
+ b: 77,
+ a: 1
+ },
+ ntp_text: {
+ r: 249,
+ g: 249,
+ b: 250,
+ a: 1
+ },
+ sidebar: {
+ r: 56,
+ g: 56,
+ b: 61,
+ a: 1
+ },
+ sidebar_text: {
+ r: 249,
+ g: 249,
+ b: 250,
+ a: 1
+ }
+};
+;// CONCATENATED MODULE: ./content-src/components/Search/Search.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals ContentSearchUIController, ContentSearchHandoffUIController */
+
+
+
+
+
+class _Search extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.onSearchClick = this.onSearchClick.bind(this);
+ this.onSearchHandoffClick = this.onSearchHandoffClick.bind(this);
+ this.onSearchHandoffPaste = this.onSearchHandoffPaste.bind(this);
+ this.onSearchHandoffDrop = this.onSearchHandoffDrop.bind(this);
+ this.onInputMount = this.onInputMount.bind(this);
+ this.onInputMountHandoff = this.onInputMountHandoff.bind(this);
+ this.onSearchHandoffButtonMount = this.onSearchHandoffButtonMount.bind(this);
+ }
+ handleEvent(event) {
+ // Also track search events with our own telemetry
+ if (event.detail.type === "Search") {
+ this.props.dispatch(actionCreators.UserEvent({
+ event: "SEARCH"
+ }));
+ }
+ }
+ onSearchClick(event) {
+ window.gContentSearchController.search(event);
+ }
+ doSearchHandoff(text) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.HANDOFF_SEARCH_TO_AWESOMEBAR,
+ data: {
+ text
+ }
+ }));
+ this.props.dispatch({
+ type: actionTypes.FAKE_FOCUS_SEARCH
+ });
+ this.props.dispatch(actionCreators.UserEvent({
+ event: "SEARCH_HANDOFF"
+ }));
+ if (text) {
+ this.props.dispatch({
+ type: actionTypes.DISABLE_SEARCH
+ });
+ }
+ }
+ onSearchHandoffClick(event) {
+ // When search hand-off is enabled, we render a big button that is styled to
+ // look like a search textbox. If the button is clicked, we style
+ // the button as if it was a focused search box and show a fake cursor but
+ // really focus the awesomebar without the focus styles ("hidden focus").
+ event.preventDefault();
+ this.doSearchHandoff();
+ }
+ onSearchHandoffPaste(event) {
+ event.preventDefault();
+ this.doSearchHandoff(event.clipboardData.getData("Text"));
+ }
+ onSearchHandoffDrop(event) {
+ event.preventDefault();
+ let text = event.dataTransfer.getData("text");
+ if (text) {
+ this.doSearchHandoff(text);
+ }
+ }
+ componentWillUnmount() {
+ delete window.gContentSearchController;
+ }
+ onInputMount(input) {
+ if (input) {
+ // The "healthReportKey" and needs to be "newtab" or "abouthome" so that
+ // BrowserUsageTelemetry.sys.mjs knows to handle events with this name, and
+ // can add the appropriate telemetry probes for search. Without the correct
+ // name, certain tests like browser_UsageTelemetry_content.js will fail
+ // (See github ticket #2348 for more details)
+ const healthReportKey = IS_NEWTAB ? "newtab" : "abouthome";
+
+ // The "searchSource" needs to be "newtab" or "homepage" and is sent with
+ // the search data and acts as context for the search request (See
+ // nsISearchEngine.getSubmission). It is necessary so that search engine
+ // plugins can correctly atribute referrals. (See github ticket #3321 for
+ // more details)
+ const searchSource = IS_NEWTAB ? "newtab" : "homepage";
+
+ // gContentSearchController needs to exist as a global so that tests for
+ // the existing about:home can find it; and so it allows these tests to pass.
+ // In the future, when activity stream is default about:home, this can be renamed
+ window.gContentSearchController = new ContentSearchUIController(input, input.parentNode, healthReportKey, searchSource);
+ addEventListener("ContentSearchClient", this);
+ } else {
+ window.gContentSearchController = null;
+ removeEventListener("ContentSearchClient", this);
+ }
+ }
+ onInputMountHandoff(input) {
+ if (input) {
+ // The handoff UI controller helps us set the search icon and reacts to
+ // changes to default engine to keep everything in sync.
+ this._handoffSearchController = new ContentSearchHandoffUIController();
+ }
+ }
+ onSearchHandoffButtonMount(button) {
+ // Keep a reference to the button for use during "paste" event handling.
+ this._searchHandoffButton = button;
+ }
+
+ /*
+ * Do not change the ID on the input field, as legacy newtab code
+ * specifically looks for the id 'newtab-search-text' on input fields
+ * in order to execute searches in various tests
+ */
+ render() {
+ const wrapperClassName = ["search-wrapper", this.props.disable && "search-disabled", this.props.fakeFocus && "fake-focus"].filter(v => v).join(" ");
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ className: wrapperClassName
+ }, this.props.showLogo && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "logo-and-wordmark"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "logo"
+ }), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "wordmark"
+ })), !this.props.handoffEnabled && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "search-inner-wrapper"
+ }, /*#__PURE__*/external_React_default().createElement("input", {
+ id: "newtab-search-text",
+ "data-l10n-id": "newtab-search-box-input",
+ maxLength: "256",
+ ref: this.onInputMount,
+ type: "search"
+ }), /*#__PURE__*/external_React_default().createElement("button", {
+ id: "searchSubmit",
+ className: "search-button",
+ "data-l10n-id": "newtab-search-box-search-button",
+ onClick: this.onSearchClick
+ })), this.props.handoffEnabled && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "search-inner-wrapper"
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ className: "search-handoff-button",
+ ref: this.onSearchHandoffButtonMount,
+ onClick: this.onSearchHandoffClick,
+ tabIndex: "-1"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "fake-textbox"
+ }), /*#__PURE__*/external_React_default().createElement("input", {
+ type: "search",
+ className: "fake-editable",
+ tabIndex: "-1",
+ "aria-hidden": "true",
+ onDrop: this.onSearchHandoffDrop,
+ onPaste: this.onSearchHandoffPaste,
+ ref: this.onInputMountHandoff
+ }), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "fake-caret"
+ }))));
+ }
+}
+const Search_Search = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Prefs: state.Prefs
+}))(_Search);
+;// CONCATENATED MODULE: ./content-src/components/Base/Base.jsx
+function Base_extends() { Base_extends = Object.assign ? Object.assign.bind() : 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; }; return Base_extends.apply(this, arguments); }
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+
+
+
+const PrefsButton = ({
+ onClick,
+ icon
+}) => /*#__PURE__*/external_React_default().createElement("div", {
+ className: "prefs-button"
+}, /*#__PURE__*/external_React_default().createElement("button", {
+ className: `icon ${icon || "icon-settings"}`,
+ onClick: onClick,
+ "data-l10n-id": "newtab-settings-button"
+}));
+
+// Returns a function will not be continuously triggered when called. The
+// function will be triggered if called again after `wait` milliseconds.
+function debounce(func, wait) {
+ let timer;
+ return (...args) => {
+ if (timer) {
+ return;
+ }
+ let wakeUp = () => {
+ timer = null;
+ };
+ timer = setTimeout(wakeUp, wait);
+ func.apply(this, args);
+ };
+}
+class _Base extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ message: {}
+ };
+ this.notifyContent = this.notifyContent.bind(this);
+ }
+ notifyContent(state) {
+ this.setState(state);
+ }
+ componentWillUnmount() {
+ this.updateTheme();
+ }
+ componentWillUpdate() {
+ this.updateTheme();
+ }
+ updateTheme() {
+ const bodyClassName = ["activity-stream",
+ // If we skipped the about:welcome overlay and removed the CSS classes
+ // we don't want to add them back to the Activity Stream view
+ document.body.classList.contains("inline-onboarding") ? "inline-onboarding" : ""].filter(v => v).join(" ");
+ __webpack_require__.g.document.body.className = bodyClassName;
+ }
+ render() {
+ const {
+ props
+ } = this;
+ const {
+ App
+ } = props;
+ const isDevtoolsEnabled = props.Prefs.values["asrouter.devtoolsEnabled"];
+ if (!App.initialized) {
+ return null;
+ }
+ return /*#__PURE__*/external_React_default().createElement(ErrorBoundary, {
+ className: "base-content-fallback"
+ }, /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(BaseContent, Base_extends({}, this.props, {
+ adminContent: this.state
+ })), isDevtoolsEnabled ? /*#__PURE__*/external_React_default().createElement(DiscoveryStreamAdmin, {
+ notifyContent: this.notifyContent
+ }) : null));
+ }
+}
+class BaseContent extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.openPreferences = this.openPreferences.bind(this);
+ this.openCustomizationMenu = this.openCustomizationMenu.bind(this);
+ this.closeCustomizationMenu = this.closeCustomizationMenu.bind(this);
+ this.handleOnKeyDown = this.handleOnKeyDown.bind(this);
+ this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5);
+ this.setPref = this.setPref.bind(this);
+ this.state = {
+ fixedSearch: false
+ };
+ }
+ componentDidMount() {
+ __webpack_require__.g.addEventListener("scroll", this.onWindowScroll);
+ __webpack_require__.g.addEventListener("keydown", this.handleOnKeyDown);
+ }
+ componentWillUnmount() {
+ __webpack_require__.g.removeEventListener("scroll", this.onWindowScroll);
+ __webpack_require__.g.removeEventListener("keydown", this.handleOnKeyDown);
+ }
+ onWindowScroll() {
+ const prefs = this.props.Prefs.values;
+ const SCROLL_THRESHOLD = prefs["logowordmark.alwaysVisible"] ? 179 : 34;
+ if (__webpack_require__.g.scrollY > SCROLL_THRESHOLD && !this.state.fixedSearch) {
+ this.setState({
+ fixedSearch: true
+ });
+ } else if (__webpack_require__.g.scrollY <= SCROLL_THRESHOLD && this.state.fixedSearch) {
+ this.setState({
+ fixedSearch: false
+ });
+ }
+ }
+ openPreferences() {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.SETTINGS_OPEN
+ }));
+ this.props.dispatch(actionCreators.UserEvent({
+ event: "OPEN_NEWTAB_PREFS"
+ }));
+ }
+ openCustomizationMenu() {
+ this.props.dispatch({
+ type: actionTypes.SHOW_PERSONALIZE
+ });
+ this.props.dispatch(actionCreators.UserEvent({
+ event: "SHOW_PERSONALIZE"
+ }));
+ }
+ closeCustomizationMenu() {
+ if (this.props.App.customizeMenuVisible) {
+ this.props.dispatch({
+ type: actionTypes.HIDE_PERSONALIZE
+ });
+ this.props.dispatch(actionCreators.UserEvent({
+ event: "HIDE_PERSONALIZE"
+ }));
+ }
+ }
+ handleOnKeyDown(e) {
+ if (e.key === "Escape") {
+ this.closeCustomizationMenu();
+ }
+ }
+ setPref(pref, value) {
+ this.props.dispatch(actionCreators.SetPref(pref, value));
+ }
+ render() {
+ const {
+ props
+ } = this;
+ const {
+ App
+ } = props;
+ const {
+ initialized,
+ customizeMenuVisible
+ } = App;
+ const prefs = props.Prefs.values;
+ const isDiscoveryStream = props.DiscoveryStream.config && props.DiscoveryStream.config.enabled;
+ let filteredSections = props.Sections.filter(section => section.id !== "topstories");
+ const pocketEnabled = prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"];
+ const noSectionsEnabled = !prefs["feeds.topsites"] && !pocketEnabled && filteredSections.filter(section => section.enabled).length === 0;
+ const searchHandoffEnabled = prefs["improvesearch.handoffToAwesomebar"];
+ const enabledSections = {
+ topSitesEnabled: prefs["feeds.topsites"],
+ pocketEnabled: prefs["feeds.section.topstories"],
+ highlightsEnabled: prefs["feeds.section.highlights"],
+ showSponsoredTopSitesEnabled: prefs.showSponsoredTopSites,
+ showSponsoredPocketEnabled: prefs.showSponsored,
+ showRecentSavesEnabled: prefs.showRecentSaves,
+ topSitesRowsCount: prefs.topSitesRows
+ };
+ const pocketRegion = prefs["feeds.system.topstories"];
+ const mayHaveSponsoredStories = prefs["system.showSponsored"];
+ const {
+ mayHaveSponsoredTopSites
+ } = prefs;
+ const outerClassName = ["outer-wrapper", isDiscoveryStream && pocketEnabled && "ds-outer-wrapper-search-alignment", isDiscoveryStream && "ds-outer-wrapper-breakpoint-override", prefs.showSearch && this.state.fixedSearch && !noSectionsEnabled && "fixed-search", prefs.showSearch && noSectionsEnabled && "only-search", prefs["logowordmark.alwaysVisible"] && "visible-logo"].filter(v => v).join(" ");
+ return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement(CustomizeMenu, {
+ onClose: this.closeCustomizationMenu,
+ onOpen: this.openCustomizationMenu,
+ openPreferences: this.openPreferences,
+ setPref: this.setPref,
+ enabledSections: enabledSections,
+ pocketRegion: pocketRegion,
+ mayHaveSponsoredTopSites: mayHaveSponsoredTopSites,
+ mayHaveSponsoredStories: mayHaveSponsoredStories,
+ showing: customizeMenuVisible
+ }), /*#__PURE__*/external_React_default().createElement("div", {
+ className: outerClassName,
+ onClick: this.closeCustomizationMenu
+ }, /*#__PURE__*/external_React_default().createElement("main", null, prefs.showSearch && /*#__PURE__*/external_React_default().createElement("div", {
+ className: "non-collapsible-section"
+ }, /*#__PURE__*/external_React_default().createElement(ErrorBoundary, null, /*#__PURE__*/external_React_default().createElement(Search_Search, Base_extends({
+ showLogo: noSectionsEnabled || prefs["logowordmark.alwaysVisible"],
+ handoffEnabled: searchHandoffEnabled
+ }, props.Search)))), /*#__PURE__*/external_React_default().createElement("div", {
+ className: `body-wrapper${initialized ? " on" : ""}`
+ }, isDiscoveryStream ? /*#__PURE__*/external_React_default().createElement(ErrorBoundary, {
+ className: "borderless-error"
+ }, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamBase, {
+ locale: props.App.locale
+ })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null))));
+ }
+}
+const Base = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ App: state.App,
+ Prefs: state.Prefs,
+ Sections: state.Sections,
+ DiscoveryStream: state.DiscoveryStream,
+ Search: state.Search
+}))(_Base);
+;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.js
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+const detect_user_session_start_VISIBLE = "visible";
+const detect_user_session_start_VISIBILITY_CHANGE_EVENT = "visibilitychange";
+class DetectUserSessionStart {
+ constructor(store, options = {}) {
+ this._store = store;
+ // Overrides for testing
+ this.document = options.document || __webpack_require__.g.document;
+ this._perfService = options.perfService || perfService;
+ this._onVisibilityChange = this._onVisibilityChange.bind(this);
+ }
+
+ /**
+ * sendEventOrAddListener - Notify immediately if the page is already visible,
+ * or else set up a listener for when visibility changes.
+ * This is needed for accurate session tracking for telemetry,
+ * because tabs are pre-loaded.
+ */
+ sendEventOrAddListener() {
+ if (this.document.visibilityState === detect_user_session_start_VISIBLE) {
+ // If the document is already visible, to the user, send a notification
+ // immediately that a session has started.
+ this._sendEvent();
+ } else {
+ // If the document is not visible, listen for when it does become visible.
+ this.document.addEventListener(detect_user_session_start_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+
+ /**
+ * _sendEvent - Sends a message to the main process to indicate the current
+ * tab is now visible to the user, includes the
+ * visibility_event_rcvd_ts time in ms from the UNIX epoch.
+ */
+ _sendEvent() {
+ this._perfService.mark("visibility_event_rcvd_ts");
+ try {
+ let visibility_event_rcvd_ts = this._perfService.getMostRecentAbsMarkStartByName("visibility_event_rcvd_ts");
+ this._store.dispatch(actionCreators.AlsoToMain({
+ type: actionTypes.SAVE_SESSION_PERF_DATA,
+ data: {
+ visibility_event_rcvd_ts
+ }
+ }));
+ } catch (ex) {
+ // If this failed, it's likely because the `privacy.resistFingerprinting`
+ // pref is true. We should at least not blow up.
+ }
+ }
+
+ /**
+ * _onVisibilityChange - If the visibility has changed to visible, sends a notification
+ * and removes the event listener. This should only be called once per tab.
+ */
+ _onVisibilityChange() {
+ if (this.document.visibilityState === detect_user_session_start_VISIBLE) {
+ this._sendEvent();
+ this.document.removeEventListener(detect_user_session_start_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+}
+;// CONCATENATED MODULE: external "Redux"
+const external_Redux_namespaceObject = Redux;
+;// CONCATENATED MODULE: ./content-src/lib/init-store.js
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/remote-page */
+
+
+
+const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
+const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
+const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
+
+/**
+ * A higher-order function which returns a reducer that, on MERGE_STORE action,
+ * will return the action.data object merged into the previous state.
+ *
+ * For all other actions, it merely calls mainReducer.
+ *
+ * Because we want this to merge the entire state object, it's written as a
+ * higher order function which takes the main reducer (itself often a call to
+ * combineReducers) as a parameter.
+ *
+ * @param {function} mainReducer reducer to call if action != MERGE_STORE_ACTION
+ * @return {function} a reducer that, on MERGE_STORE_ACTION action,
+ * will return the action.data object merged
+ * into the previous state, and the result
+ * of calling mainReducer otherwise.
+ */
+function mergeStateReducer(mainReducer) {
+ return (prevState, action) => {
+ if (action.type === MERGE_STORE_ACTION) {
+ return {
+ ...prevState,
+ ...action.data
+ };
+ }
+ return mainReducer(prevState, action);
+ };
+}
+
+/**
+ * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary
+ */
+const messageMiddleware = store => next => action => {
+ const skipLocal = action.meta && action.meta.skipLocal;
+ if (actionUtils.isSendToMain(action)) {
+ RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
+ }
+ if (!skipLocal) {
+ next(action);
+ }
+};
+const rehydrationMiddleware = ({
+ getState
+}) => {
+ // NB: The parameter here is MiddlewareAPI which looks like a Store and shares
+ // the same getState, so attached properties are accessible from the store.
+ getState.didRehydrate = false;
+ getState.didRequestInitialState = false;
+ return next => action => {
+ if (getState.didRehydrate || window.__FROM_STARTUP_CACHE__) {
+ // Startup messages can be safely ignored by the about:home document
+ // stored in the startup cache.
+ if (window.__FROM_STARTUP_CACHE__ && action.meta && action.meta.isStartup) {
+ return null;
+ }
+ return next(action);
+ }
+ const isMergeStoreAction = action.type === MERGE_STORE_ACTION;
+ const isRehydrationRequest = action.type === actionTypes.NEW_TAB_STATE_REQUEST;
+ if (isRehydrationRequest) {
+ getState.didRequestInitialState = true;
+ return next(action);
+ }
+ if (isMergeStoreAction) {
+ getState.didRehydrate = true;
+ return next(action);
+ }
+
+ // If init happened after our request was made, we need to re-request
+ if (getState.didRequestInitialState && action.type === actionTypes.INIT) {
+ return next(actionCreators.AlsoToMain({
+ type: actionTypes.NEW_TAB_STATE_REQUEST
+ }));
+ }
+ if (actionUtils.isBroadcastToContent(action) || actionUtils.isSendToOneContent(action) || actionUtils.isSendToPreloaded(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
+ */
+function initStore(reducers, initialState) {
+ const store = (0,external_Redux_namespaceObject.createStore)(mergeStateReducer((0,external_Redux_namespaceObject.combineReducers)(reducers)), initialState, __webpack_require__.g.RPMAddMessageListener && (0,external_Redux_namespaceObject.applyMiddleware)(rehydrationMiddleware, messageMiddleware));
+ if (__webpack_require__.g.RPMAddMessageListener) {
+ __webpack_require__.g.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => {
+ try {
+ store.dispatch(msg.data);
+ } catch (ex) {
+ console.error("Content msg:", msg, "Dispatch error: ", ex);
+ dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`);
+ }
+ });
+ }
+ return store;
+}
+;// CONCATENATED MODULE: external "ReactDOM"
+const external_ReactDOM_namespaceObject = ReactDOM;
+var external_ReactDOM_default = /*#__PURE__*/__webpack_require__.n(external_ReactDOM_namespaceObject);
+;// CONCATENATED MODULE: ./content-src/activity-stream.jsx
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+
+const NewTab = ({
+ store
+}) => /*#__PURE__*/external_React_default().createElement(external_ReactRedux_namespaceObject.Provider, {
+ store: store
+}, /*#__PURE__*/external_React_default().createElement(Base, null));
+function renderWithoutState() {
+ const store = initStore(reducers);
+ new DetectUserSessionStart(store).sendEventOrAddListener();
+
+ // If this document has already gone into the background by the time we've reached
+ // here, we can deprioritize requesting the initial state until the event loop
+ // frees up. If, however, the visibility changes, we then send the request.
+ let didRequest = false;
+ let requestIdleCallbackId = 0;
+ function doRequest() {
+ if (!didRequest) {
+ if (requestIdleCallbackId) {
+ cancelIdleCallback(requestIdleCallbackId);
+ }
+ didRequest = true;
+ store.dispatch(actionCreators.AlsoToMain({
+ type: actionTypes.NEW_TAB_STATE_REQUEST
+ }));
+ }
+ }
+ if (document.hidden) {
+ requestIdleCallbackId = requestIdleCallback(doRequest);
+ addEventListener("visibilitychange", doRequest, {
+ once: true
+ });
+ } else {
+ doRequest();
+ }
+ external_ReactDOM_default().hydrate( /*#__PURE__*/external_React_default().createElement(NewTab, {
+ store: store
+ }), document.getElementById("root"));
+}
+function renderCache(initialState) {
+ const store = initStore(reducers, initialState);
+ new DetectUserSessionStart(store).sendEventOrAddListener();
+ external_ReactDOM_default().hydrate( /*#__PURE__*/external_React_default().createElement(NewTab, {
+ store: store
+ }), document.getElementById("root"));
+}
+NewtabRenderUtils = __webpack_exports__;
+/******/ })()
+; \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/firefox.svg b/browser/components/newtab/data/content/assets/firefox.svg
new file mode 100644
index 0000000000..0587828f60
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/firefox.svg
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1856 1847.5" style="enable-background:new 0 0 1856 1847.5;" xml:space="preserve">
+<style type="text/css">
+ .st0{display:none;}
+ .st1{fill:url(#SVGID_1_);}
+ .st2{opacity:0.67;}
+ .st3{fill:url(#SVGID_2_);}
+ .st4{fill:url(#SVGID_3_);}
+ .st5{fill:url(#SVGID_4_);}
+ .st6{fill:url(#SVGID_5_);}
+ .st7{fill:url(#SVGID_6_);}
+ .st8{fill:url(#SVGID_7_);}
+ .st9{fill:url(#SVGID_8_);}
+ .st10{opacity:0.53;fill:url(#SVGID_9_);enable-background:new ;}
+ .st11{opacity:0.53;fill:url(#SVGID_10_);enable-background:new ;}
+ .st12{fill:url(#SVGID_11_);}
+ .st13{fill:url(#SVGID_12_);}
+ .st14{fill:url(#SVGID_13_);}
+ .st15{fill:url(#SVGID_14_);}
+ .st16{fill:none;}
+</style>
+<g id="LiveType" class="st0">
+</g>
+<g id="Outlined">
+ <g>
+ <g>
+
+ <radialGradient id="SVGID_1_" cx="321.9653" cy="2631.8848" r="1876.7874" fx="389.093" fy="2598.7063" gradientTransform="matrix(1 0 0 -1 1258.4413 3044.8896)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#FFF44F"/>
+ <stop offset="0.2949" style="stop-color:#FF980E"/>
+ <stop offset="0.4315" style="stop-color:#FF5D36"/>
+ <stop offset="0.5302" style="stop-color:#FF3750"/>
+ <stop offset="0.7493" style="stop-color:#F5156C"/>
+ <stop offset="0.7648" style="stop-color:#F1136E"/>
+ <stop offset="0.8801" style="stop-color:#DA057A"/>
+ <stop offset="0.9528" style="stop-color:#D2007F"/>
+ </radialGradient>
+ <path class="st1" d="M1588.9,424.3c-149.5-196.4-382.5-318.8-627.6-323.6c-192-3.8-324.7,53.7-399.7,100 c100.4-58.1,245.7-91.1,373-89.4c327.3,4.2,678.8,226.5,731,627.3c59.9,460.1-261.3,844-713.1,845.2 C455.4,1585,153,1145.8,232,751.4c3.9-19.7,2-38.9,8.6-57.5c3.8-69.4,30-178.1,86.7-289.1c-57.2,29.5-130.1,123-166.1,209.6 c-51.9,124.8-70.2,274.1-53.7,416.1c1.2,10.7,2.4,21.3,3.8,31.9c66.6,390.6,406.6,688,816.2,688c457.3,0,828.1-370.7,828.1-828.1 C1755.5,735.4,1693.5,562.9,1588.9,424.3z M278.3,496.2L278.3,496.2L278.3,496.2z"/>
+ <g class="st2">
+
+ <radialGradient id="SVGID_2_" cx="-1019.8155" cy="2554.2456" r="1110.733" fx="-980.0875" fy="2534.6096" gradientTransform="matrix(1 0 0 -1 1258.4413 3044.8896)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#B5007F"/>
+ <stop offset="1" style="stop-color:#F5156C;stop-opacity:0"/>
+ </radialGradient>
+ <path class="st3" d="M1588.9,424.3c-149.5-196.4-382.5-318.8-627.6-323.6c-192-3.8-324.7,53.7-399.7,100 c100.4-58.1,245.7-91.1,373-89.4c327.3,4.2,678.8,226.5,731,627.3c59.9,460.1-261.3,844-713.1,845.2 C455.4,1585,153,1145.8,232,751.4c3.9-19.7,2-38.9,8.6-57.5c3.8-69.4,30-178.1,86.7-289.1c-57.2,29.5-130.1,123-166.1,209.6 c-51.9,124.8-70.2,274.1-53.7,416.1c1.2,10.7,2.4,21.3,3.8,31.9c66.6,390.6,406.6,688,816.2,688 c457.3,0,828.1-370.7,828.1-828.1C1755.5,735.4,1693.5,562.9,1588.9,424.3z M278.3,496.2L278.3,496.2L278.3,496.2z"/>
+ </g>
+
+ <radialGradient id="SVGID_3_" cx="482.4009" cy="2738.6748" r="2203.8347" fx="561.2262" fy="2699.7146" gradientTransform="matrix(1 0 0 -1 1258.4413 3044.8896)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#FFDD00;stop-opacity:0.6"/>
+ <stop offset="8.366719e-02" style="stop-color:#FFD801;stop-opacity:0.5244"/>
+ <stop offset="0.1822" style="stop-color:#FECA05;stop-opacity:0.4353"/>
+ <stop offset="0.2884" style="stop-color:#FEB20C;stop-opacity:0.3394"/>
+ <stop offset="0.3998" style="stop-color:#FD9115;stop-opacity:0.2388"/>
+ <stop offset="0.5154" style="stop-color:#FB6621;stop-opacity:0.1343"/>
+ <stop offset="0.6329" style="stop-color:#F9332F;stop-opacity:2.816400e-02"/>
+ <stop offset="0.664" style="stop-color:#F92433;stop-opacity:0"/>
+ </radialGradient>
+ <path class="st4" d="M1588.9,424.3c-149.5-196.4-382.5-318.8-627.6-323.6c-192-3.8-324.7,53.7-399.7,100 c100.4-58.1,245.7-91.1,373-89.4c327.3,4.2,678.8,226.5,731,627.3c59.9,460.1-261.3,844-713.1,845.2 C455.4,1585,153,1145.8,232,751.4c3.9-19.7,2-38.9,8.6-57.5c3.8-69.4,30-178.1,86.7-289.1c-57.2,29.5-130.1,123-166.1,209.6 c-51.9,124.8-70.2,274.1-53.7,416.1c1.2,10.7,2.4,21.3,3.8,31.9c66.6,390.6,406.6,688,816.2,688c457.3,0,828.1-370.7,828.1-828.1 C1755.5,735.4,1693.5,562.9,1588.9,424.3z M278.3,496.2L278.3,496.2L278.3,496.2z"/>
+ </g>
+ <g>
+
+ <radialGradient id="SVGID_4_" cx="975.7665" cy="1493.5381" r="2843.1211" gradientTransform="matrix(1 0 0 -1 0 2512)" gradientUnits="userSpaceOnUse">
+ <stop offset="0.1528" style="stop-color:#960E18"/>
+ <stop offset="0.2061" style="stop-color:#CC2335;stop-opacity:0.5541"/>
+ <stop offset="0.2495" style="stop-color:#F13148;stop-opacity:0.1914"/>
+ <stop offset="0.2724" style="stop-color:#FF3750;stop-opacity:0"/>
+ </radialGradient>
+ <path class="st5" d="M1588.9,424.3c-149.5-196.4-382.5-318.8-627.6-323.6c-192-3.8-324.7,53.7-399.7,100 c100.4-58.1,245.7-91.1,373-89.4c327.3,4.2,678.8,226.5,731,627.3c59.9,460.1-261.3,844-713.1,845.2 C455.4,1585,153,1145.8,232,751.4c3.9-19.7,2-38.9,8.6-57.5c3.8-69.4,30-178.1,86.7-289.1c-57.2,29.5-130.1,123-166.1,209.6 c-51.9,124.8-70.2,274.1-53.7,416.1c1.2,10.7,2.4,21.3,3.8,31.9c66.6,390.6,406.6,688,816.2,688c457.3,0,828.1-370.7,828.1-828.1 C1755.5,735.4,1693.5,562.9,1588.9,424.3z M278.3,496.2L278.3,496.2L278.3,496.2z"/>
+
+ <radialGradient id="SVGID_5_" cx="760.6194" cy="1529.2881" r="2843.1211" gradientTransform="matrix(1 0 0 -1 0 2512)" gradientUnits="userSpaceOnUse">
+ <stop offset="0.1129" style="stop-color:#960E18"/>
+ <stop offset="0.1893" style="stop-color:#CC2335;stop-opacity:0.5541"/>
+ <stop offset="0.2515" style="stop-color:#F13148;stop-opacity:0.1914"/>
+ <stop offset="0.2843" style="stop-color:#FF3750;stop-opacity:0"/>
+ </radialGradient>
+ <path class="st6" d="M1588.9,424.3c-149.5-196.4-382.5-318.8-627.6-323.6c-192-3.8-324.7,53.7-399.7,100 c100.4-58.1,245.7-91.1,373-89.4c327.3,4.2,678.8,226.5,731,627.3c59.9,460.1-261.3,844-713.1,845.2 C455.4,1585,153,1145.8,232,751.4c3.9-19.7,2-38.9,8.6-57.5c3.8-69.4,30-178.1,86.7-289.1c-57.2,29.5-130.1,123-166.1,209.6 c-51.9,124.8-70.2,274.1-53.7,416.1c1.2,10.7,2.4,21.3,3.8,31.9c66.6,390.6,406.6,688,816.2,688c457.3,0,828.1-370.7,828.1-828.1 C1755.5,735.4,1693.5,562.9,1588.9,424.3z M278.3,496.2L278.3,496.2L278.3,496.2z"/>
+ </g>
+
+ <linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-209.3687" y1="2784.0808" x2="277.0962" y2="1941.499" gradientTransform="matrix(1 0 0 -1 1258.4413 3044.8896)">
+ <stop offset="0" style="stop-color:#FFBC04"/>
+ <stop offset="0.2597" style="stop-color:#FFA202;stop-opacity:0.4886"/>
+ <stop offset="0.5078" style="stop-color:#FF8E00;stop-opacity:0"/>
+ </linearGradient>
+ <path class="st7" d="M1665.6,738.5c6.6,50.8,8.6,100.8,6.2,149.5c27.4-4.1,54.9-7.7,82.4-10.9c-9.2-169.5-69.2-325.4-165.3-452.8 c-149.5-196.4-382.5-318.8-627.6-323.6c-192-3.8-324.7,53.7-399.7,100c100.4-58.1,245.7-91.1,373-89.4 C1261.9,115.4,1613.4,337.7,1665.6,738.5z"/>
+ <g>
+
+ <radialGradient id="SVGID_7_" cx="244.0767" cy="2202.7847" r="1837.1556" fx="309.7869" fy="2170.3069" gradientTransform="matrix(0.9589 0 0 -0.9589 1306.9894 2461.7681)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#FF980E"/>
+ <stop offset="0.295" style="stop-color:#FF7139"/>
+ <stop offset="0.4846" style="stop-color:#FF5B51"/>
+ <stop offset="0.626" style="stop-color:#FF4F5E"/>
+ <stop offset="0.7365" style="stop-color:#FF4055"/>
+ <stop offset="0.8428" style="stop-color:#FF3750"/>
+ </radialGradient>
+ <path class="st8" d="M1685.7,715.6c-46.5-418.8-421-607.1-751.1-604.4c-127.2,1-272.5,31.3-373,89.4 c-46.9,28.9-71.3,53.4-73.6,55.8c2.7-2.2,10.6-8.7,23.8-17.8c0.4-0.3,0.9-0.6,1.3-0.9c0.4-0.3,0.8-0.5,1.2-0.8 c47.9-33,101.9-57,159-73.7c87.3-25.5,181.7-34.1,272.6-31.8c373.4,21.5,636.9,330.7,646.1,659.8 c7.6,272.4-215.9,489.6-473.5,502.2c-187.3,9.1-363.8-81.3-450.1-262.2c-19.2-40.3-33.3-81.2-40.6-130.6 C587,625.2,772.3,390.4,942.3,332.3c-91.7-79.9-321.4-74.5-492.4,51c-123.1,90.4-203,227.9-229.5,391.9 c-20.2,125.1-1.2,255.2,48.4,371.3c50.5,118.4,132.6,222.7,235.8,299.6c112.1,83.4,247.1,132,386.2,142.6 c20.5,1.6,41.1,2.3,61.7,2.3C1499.6,1591,1736.9,1176.7,1685.7,715.6z"/>
+
+ <radialGradient id="SVGID_8_" cx="244.0767" cy="2202.7847" r="1837.1556" fx="309.7869" fy="2170.3069" gradientTransform="matrix(0.9589 0 0 -0.9589 1306.9894 2461.7681)" gradientUnits="userSpaceOnUse">
+ <stop offset="8.407450e-02" style="stop-color:#FFDE08"/>
+ <stop offset="0.2081" style="stop-color:#FFD609;stop-opacity:0.832"/>
+ <stop offset="0.4033" style="stop-color:#FFBF0B;stop-opacity:0.5677"/>
+ <stop offset="0.6437" style="stop-color:#FF9B0F;stop-opacity:0.242"/>
+ <stop offset="0.8224" style="stop-color:#FF7B12;stop-opacity:0"/>
+ </radialGradient>
+ <path class="st9" d="M1685.7,715.6c-46.5-418.8-421-607.1-751.1-604.4c-127.2,1-272.5,31.3-373,89.4 c-46.9,28.9-71.3,53.4-73.6,55.8c2.7-2.2,10.6-8.7,23.8-17.8c0.4-0.3,0.9-0.6,1.3-0.9c0.4-0.3,0.8-0.5,1.2-0.8 c47.9-33,101.9-57,159-73.7c87.3-25.5,181.7-34.1,272.6-31.8c373.4,21.5,636.9,330.7,646.1,659.8 c7.6,272.4-215.9,489.6-473.5,502.2c-187.3,9.1-363.8-81.3-450.1-262.2c-19.2-40.3-33.3-81.2-40.6-130.6 C587,625.2,772.3,390.4,942.3,332.3c-91.7-79.9-321.4-74.5-492.4,51c-123.1,90.4-203,227.9-229.5,391.9 c-20.2,125.1-1.2,255.2,48.4,371.3c50.5,118.4,132.6,222.7,235.8,299.6c112.1,83.4,247.1,132,386.2,142.6 c20.5,1.6,41.1,2.3,61.7,2.3C1499.6,1591,1736.9,1176.7,1685.7,715.6z"/>
+ </g>
+ <g>
+
+ <radialGradient id="SVGID_9_" cx="538.6154" cy="664.721" r="863.9618" gradientTransform="matrix(0.2472 0.969 1.0112 -0.258 328.7156 547.2202)" gradientUnits="userSpaceOnUse">
+ <stop offset="0.3634" style="stop-color:#FF3750"/>
+ <stop offset="0.4111" style="stop-color:#FF444B;stop-opacity:0.7895"/>
+ <stop offset="0.5902" style="stop-color:#FF7139;stop-opacity:0"/>
+ </radialGradient>
+ <path class="st10" d="M1685.7,715.6c-46.5-418.8-421-607.1-751.1-604.4c-127.2,1-272.5,31.3-373,89.4 c-46.9,28.9-71.3,53.4-73.6,55.8c2.7-2.2,10.6-8.7,23.8-17.8c0.4-0.3,0.9-0.6,1.3-0.9c0.4-0.3,0.8-0.5,1.2-0.8 c47.9-33,101.9-57,159-73.7c87.3-25.5,181.7-34.1,272.6-31.8c373.4,21.5,636.9,330.7,646.1,659.8 c7.6,272.4-215.9,489.6-473.5,502.2c-187.3,9.1-363.8-81.3-450.1-262.2c-19.2-40.3-33.3-81.2-40.6-130.6 C587,625.2,772.3,390.4,942.3,332.3c-91.7-79.9-321.4-74.5-492.4,51c-123.1,90.4-203,227.9-229.5,391.9 c-20.2,125.1-1.2,255.2,48.4,371.3c50.5,118.4,132.6,222.7,235.8,299.6c112.1,83.4,247.1,132,386.2,142.6 c20.5,1.6,41.1,2.3,61.7,2.3C1499.6,1591,1736.9,1176.7,1685.7,715.6z"/>
+
+ <radialGradient id="SVGID_10_" cx="389.6962" cy="589.2068" r="862.9537" gradientTransform="matrix(0.2472 0.969 0.9698 -0.2474 318.7961 738.6102)" gradientUnits="userSpaceOnUse">
+ <stop offset="0.2159" style="stop-color:#FF3750;stop-opacity:0.8"/>
+ <stop offset="0.2702" style="stop-color:#FF444B;stop-opacity:0.6316"/>
+ <stop offset="0.4739" style="stop-color:#FF7139;stop-opacity:0"/>
+ </radialGradient>
+ <path class="st11" d="M1685.7,715.6c-46.5-418.8-421-607.1-751.1-604.4c-127.2,1-272.5,31.3-373,89.4 c-46.9,28.9-71.3,53.4-73.6,55.8c2.7-2.2,10.6-8.7,23.8-17.8c0.4-0.3,0.9-0.6,1.3-0.9c0.4-0.3,0.8-0.5,1.2-0.8 c47.9-33,101.9-57,159-73.7c87.3-25.5,181.7-34.1,272.6-31.8c373.4,21.5,636.9,330.7,646.1,659.8 c7.6,272.4-215.9,489.6-473.5,502.2c-187.3,9.1-363.8-81.3-450.1-262.2c-19.2-40.3-33.3-81.2-40.6-130.6 C587,625.2,772.3,390.4,942.3,332.3c-91.7-79.9-321.4-74.5-492.4,51c-123.1,90.4-203,227.9-229.5,391.9 c-20.2,125.1-1.2,255.2,48.4,371.3c50.5,118.4,132.6,222.7,235.8,299.6c112.1,83.4,247.1,132,386.2,142.6 c20.5,1.6,41.1,2.3,61.7,2.3C1499.6,1591,1736.9,1176.7,1685.7,715.6z"/>
+ </g>
+ <g>
+
+ <radialGradient id="SVGID_11_" cx="610.7241" cy="2410.3098" r="3105.1294" gradientTransform="matrix(0.9589 0 0 -0.9589 1306.9894 2461.7681)" gradientUnits="userSpaceOnUse">
+ <stop offset="5.356570e-02" style="stop-color:#FFF44F"/>
+ <stop offset="0.4573" style="stop-color:#FF980E"/>
+ <stop offset="0.5211" style="stop-color:#FF8424"/>
+ <stop offset="0.5871" style="stop-color:#FF7634"/>
+ <stop offset="0.6393" style="stop-color:#FF7139"/>
+ </radialGradient>
+ <path class="st12" d="M1118.5,1293.4c353.6-21.5,504.9-313.4,514.3-520.7c14.8-323.7-177.7-672.7-686.9-641.2 c-90.8-2.3-185.3,6.3-272.6,31.8c-76.5,23.3-129.6,53.8-159,73.7c-0.4,0.3-0.8,0.5-1.2,0.8c-0.5,0.3-0.9,0.6-1.3,0.9 c-7.9,5.5-15.7,11.3-23.2,17.4c14.2-9.7,189.8-113.1,433.8-81.2C1214.7,213.1,1482,440,1482,739.4 c0,230.4-178.4,406.2-387.4,393.7C784.2,1114.4,706,796.8,867.4,659.6c-43.5-9.4-125.4,9-182.3,93.9 c-51.1,76.3-48.2,194.1-16.7,277.6C754.7,1212.1,931.3,1304.8,1118.5,1293.4z"/>
+ </g>
+ <g>
+
+ <linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="1321.7657" y1="2265.9978" x2="477.6807" y2="803.9998" gradientTransform="matrix(1 0 0 -1 0 2512)">
+ <stop offset="0" style="stop-color:#FFF44F;stop-opacity:0.8"/>
+ <stop offset="0.75" style="stop-color:#FFF44F;stop-opacity:0"/>
+ </linearGradient>
+ <path class="st13" d="M1588.9,424.3c-22.2-29.1-46.4-56.4-72-82.1c-20.5-21.7-42.6-41.8-65.7-60.3c13.3,11.6,26.1,23.9,38.2,36.9 c44.8,48.3,80.2,105.4,100.9,168c43.2,130.5,40.4,293.9-42.1,422.2c-98.3,152.9-258.2,228.4-431.2,224.7c-7.5,0-15-0.1-22.6-0.6 C784.2,1114.4,706,796.8,867.5,659.6c-43.5-9.4-125.4,9-182.4,93.9c-51.1,76.3-48.2,194.1-16.7,277.6 c-19.2-40.2-33.3-81.2-40.6-130.6C587,625.2,772.3,390.4,942.3,332.3c-91.7-79.9-321.4-74.5-492.4,51 c-99.6,73.1-170.9,177.1-208.5,301.1c5.5-69.5,31.9-173.4,86-279.5c-57.2,29.5-130.1,123-166.1,209.6 c-51.9,124.8-70.2,274.1-53.7,416.1c1.2,10.7,2.4,21.3,3.8,31.9c66.6,390.6,406.6,688,816.2,688c457.3,0,828.1-370.7,828.1-828.1 C1755.5,735.4,1693.5,562.9,1588.9,424.3z"/>
+ </g>
+
+ <linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="-205.4111" y1="1906.6858" x2="-205.4111" y2="2933.979" gradientTransform="matrix(1 0 0 -1 1258.4413 3044.8896)">
+ <stop offset="0" style="stop-color:#3A8EE6"/>
+ <stop offset="0.2359" style="stop-color:#5C79F0"/>
+ <stop offset="0.6293" style="stop-color:#9059FF"/>
+ <stop offset="1" style="stop-color:#C139E6"/>
+ </linearGradient>
+ <path class="st14" d="M1590.4,486.7c-20.7-62.5-56.1-119.6-100.9-168c-52.8-56.9-118.5-100.2-188-133.9 c-59.2-28.7-121-51.3-184.8-65.4c-114.4-25.2-234.4-24.7-342.5-2C656.7,142.2,553.4,192.9,488,256.3c49-29.7,117.3-53.8,166-66.1 c226-57,474.8,4.7,644.5,167.1c34.1,32.6,64.7,69.1,89.5,109.4c101.2,164.2,91.6,370.5,12.7,492.2 c-58.6,90.4-184.1,175.2-301.3,174.3c179.7,9.4,347.2-66.2,448.9-224.3C1630.8,780.6,1633.6,617.2,1590.4,486.7z"/>
+
+ <linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="-583.4494" y1="2938.6887" x2="250.1202" y2="2105.1191" gradientTransform="matrix(1 0 0 -1 1258.4413 3044.8896)">
+ <stop offset="0.8054" style="stop-color:#9059FF;stop-opacity:0"/>
+ <stop offset="1" style="stop-color:#6E008B;stop-opacity:0.5"/>
+ </linearGradient>
+ <path class="st15" d="M1590.4,486.7c-20.7-62.5-56.1-119.6-100.9-168c-52.8-56.9-118.5-100.2-188-133.9 c-59.2-28.7-121-51.3-184.8-65.4c-114.4-25.2-234.4-24.7-342.5-2C656.7,142.2,553.4,192.9,488,256.3c49-29.7,117.3-53.8,166-66.1 c226-57,474.8,4.7,644.5,167.1c34.1,32.6,64.7,69.1,89.5,109.4c101.2,164.2,91.6,370.5,12.7,492.2 c-58.6,90.4-184.1,175.2-301.3,174.3c179.7,9.4,347.2-66.2,448.9-224.3C1630.8,780.6,1633.6,617.2,1590.4,486.7z"/>
+ </g>
+ <rect x="745.5" y="50.5" class="st16" width="50" height="50"/>
+</g>
+<g id="Layer_3">
+</g>
+<g id="Layer_4">
+</g>
+<g id="Layer_5">
+</g>
+</svg>
diff --git a/browser/components/newtab/data/content/assets/glyph-cfr-feature-16.svg b/browser/components/newtab/data/content/assets/glyph-cfr-feature-16.svg
new file mode 100644
index 0000000000..871b48ca45
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-cfr-feature-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" fill-opacity="context-fill-opacity" d="M10 13H6a1 1 0 0 1-1-1 4.552 4.552 0 0 0-1.1-1.87A7.017 7.017 0 0 1 2 6a6 6 0 1 1 12 0 7.017 7.017 0 0 1-1.9 4.13A4.552 4.552 0 0 0 11 12a1 1 0 0 1-1 1zm-3.188-2h2.376a8.489 8.489 0 0 1 1.328-2.093A5.415 5.415 0 0 0 12 6a4.054 4.054 0 0 0-4-4 4.054 4.054 0 0 0-4 4 5.415 5.415 0 0 0 1.484 2.907c.543.629.99 1.334 1.328 2.093zM10 16H6a1 1 0 0 1 0-2h4a1 1 0 0 1 0 2z"/></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/glyph-mail-16.svg b/browser/components/newtab/data/content/assets/glyph-mail-16.svg
new file mode 100644
index 0000000000..8c211c5567
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-mail-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" d="M13 2H3C1.3 2 0 3.3 0 5v6c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3V5c0-1.7-1.3-3-3-3zm0 2c.1 0 .2 0 .3.1h-.1L8 7.9 2.8 4.1h-.1c.1-.1.2-.1.3-.1h10zm1 7c0 .6-.4 1-1 1H3c-.6 0-1-.4-1-1V4.8c0 .1.1.1.2.1l5.5 4c.1.1.2.1.3.1.1 0 .2 0 .3-.1l5.5-4c.1 0 .1-.1.1-.2.1.1.1.2.1.3v6z"></path></svg>
diff --git a/browser/components/newtab/data/content/assets/glyph-maximize-16.svg b/browser/components/newtab/data/content/assets/glyph-maximize-16.svg
new file mode 100644
index 0000000000..2f45557cfa
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-maximize-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" d="M14 1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zm-1 12H3V3h10z"/><path fill="context-fill" d="M5 9h2v2a1 1 0 0 0 2 0V9h2a1 1 0 0 0 0-2H9V5a1 1 0 0 0-2 0v2H5a1 1 0 0 0 0 2z"/></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/glyph-minimize-16.svg b/browser/components/newtab/data/content/assets/glyph-minimize-16.svg
new file mode 100644
index 0000000000..6bc93fa5e0
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-minimize-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" d="M14 1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zm-1 12H3V3h10z"/><path fill="context-fill" d="M5 9h6a1 1 0 0 0 0-2H5a1 1 0 0 0 0 2z"/></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/glyph-modal-delete-20.svg b/browser/components/newtab/data/content/assets/glyph-modal-delete-20.svg
new file mode 100644
index 0000000000..592f9569a1
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-modal-delete-20.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20" fill="context-fill" fill-opacity="context-fill-opacity">
+ <path d="M17.2 4 14 4l0-1.5C14 1.1 12.9 0 11.5 0l-3 0C7.1 0 6 1.1 6 2.5L6 4 2.8 4c-.5 0-.8.3-.8.8s.3.8.8.8l.2 0 0 12C3 18.9 4.1 20 5.5 20l9 0c1.4 0 2.5-1.1 2.5-2.5l0-12 .3 0c.4 0 .8-.3.8-.8s-.4-.7-.9-.7zM7.5 2.3l.8-.8 3.4 0 .8.8 0 1.7-5 0 0-1.7zm8 15.4-.8.8-9.4 0-.8-.8 0-12.2 11 0 0 12.2z"/>
+ <path d="M7.8 16c.4 0 .8-.3.8-.8l0-6.5c0-.4-.3-.8-.8-.8s-.8.4-.8.9l0 6.5c0 .4.3.7.8.7z"/>
+ <path d="M12.2 16c.4 0 .8-.3.8-.8l0-6.5c0-.4-.3-.8-.8-.8s-.8.3-.8.8l0 6.5c.1.5.4.8.8.8z"/>
+</svg>
diff --git a/browser/components/newtab/data/content/assets/glyph-newWindow-16.svg b/browser/components/newtab/data/content/assets/glyph-newWindow-16.svg
new file mode 100644
index 0000000000..0b09bfde5a
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-newWindow-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="context-fill"><path d="M14.923 1.618A1 1 0 0 0 14 1H9a1 1 0 0 0 0 2h2.586L8.293 6.293a1 1 0 1 0 1.414 1.414L13 4.414V7a1 1 0 0 0 2 0V2a1 1 0 0 0-.077-.382z"/><path d="M14 10a1 1 0 0 0-1 1v2H3V3h2a1 1 0 0 0 0-2H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1z"/></g></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/glyph-open-file-16.svg b/browser/components/newtab/data/content/assets/glyph-open-file-16.svg
new file mode 100644
index 0000000000..a2a23f09eb
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-open-file-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" d="M14.859 3.2a1.335 1.335 0 0 1-1.217.8H13v1h1v8H2V5h8V4h-.642a1.365 1.365 0 0 1-1.325-1.11L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-1.141-1.8zM2 3h3.219l1.072 1H2zm7.854-.146L11 1.707V8.5a.5.5 0 0 0 1 0V1.707l1.146 1.146a.5.5 0 1 0 .707-.707l-2-2a.5.5 0 0 0-.707 0l-2 2a.5.5 0 0 0 .707.707z"/></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/glyph-pin-16.svg b/browser/components/newtab/data/content/assets/glyph-pin-16.svg
new file mode 100644
index 0000000000..c951bc1c9d
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-pin-16.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+ <path d="m15.817 13.933-4.092-4.092 2.057-2.057a1.622 1.622 0 0 0 .352-1.771 1.62 1.62 0 0 0-1.501-1.003l-1.851 0-3.175-3.177c-.021-.021-.049-.03-.072-.048l.719-.719A.625.625 0 0 0 7.37.182L1.183 6.37a.625.625 0 0 0 .884.884l.719-.719c.017.023.026.05.047.072L6.01 9.783l0 1.85c0 .66.394 1.249 1.003 1.501a1.623 1.623 0 0 0 1.771-.352l2.057-2.057 4.092 4.092a.625.625 0 0 0 .884-.884zM6.944 2.94l3.079 3.078.623.242 1.987 0c.226 0 .317.162.347.231a.363.363 0 0 1-.082.408L7.9 11.897a.362.362 0 0 1-.408.082.363.363 0 0 1-.231-.347l0-1.992-.25-.625L3.94 5.944l0-.567 2.51-2.509.494.072z"/>
+</svg>
diff --git a/browser/components/newtab/data/content/assets/glyph-pocket-archive-16.svg b/browser/components/newtab/data/content/assets/glyph-pocket-archive-16.svg
new file mode 100644
index 0000000000..10cf13c4d2
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-pocket-archive-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" fill-opacity="context-fill-opacity" d="M1 14.667V4h14v10.667c0 .736-.597 1.333-1.333 1.333H2.333A1.333 1.333 0 0 1 1 14.667zM1 0h14a1 1 0 0 1 0 2H1a1 1 0 1 1 0-2zm9.341 7.247l-3.295 2.884-.839-.838a1 1 0 1 0-1.414 1.414l1.5 1.5a1 1 0 0 0 1.366.046l4-3.5a1 1 0 0 0-1.318-1.506z"/></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/glyph-pocket-delete-16.svg b/browser/components/newtab/data/content/assets/glyph-pocket-delete-16.svg
new file mode 100644
index 0000000000..95bb4d3edb
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-pocket-delete-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" fill-opacity="context-fill-opacity" d="M4 2v-.667C4 .597 4.597 0 5.333 0h5.334C11.403 0 12 .597 12 1.333V2h3.02a1 1 0 0 1 0 2H1.003a1 1 0 1 1 0-2H4zM2 14.667V6h12v8.667c0 .736-.597 1.333-1.333 1.333H3.333A1.333 1.333 0 0 1 2 14.667z"/></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/glyph-unpin-16.svg b/browser/components/newtab/data/content/assets/glyph-unpin-16.svg
new file mode 100644
index 0000000000..2352839340
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-unpin-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" d="M11.414 10l2.293-2.293a1 1 0 0 0 0-1.414 4.418 4.418 0 0 0-.8-.622L11.425 7.15h.008l-4.3 4.3v-.017l-1.48 1.476a3.865 3.865 0 0 0 .692.834 1 1 0 0 0 1.37-.042L10 11.414l3.293 3.293a1 1 0 0 0 1.414-1.414zm3.293-8.707a1 1 0 0 0-1.414 0L9.7 4.882A2.382 2.382 0 0 1 8 2.586V2a1 1 0 0 0-1.707-.707l-5 5A1 1 0 0 0 2 8h.586a2.382 2.382 0 0 1 2.3 1.7l-3.593 3.593a1 1 0 1 0 1.414 1.414l12-12a1 1 0 0 0 0-1.414zm-9 6a4.414 4.414 0 0 0-1.571-1.015l2.143-2.142a4.4 4.4 0 0 0 1.013 1.571 4.191 4.191 0 0 0 .9.684l-1.8 1.8a4.2 4.2 0 0 0-.684-.898z"/></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/glyph-webextension-16.svg b/browser/components/newtab/data/content/assets/glyph-webextension-16.svg
new file mode 100644
index 0000000000..b29ea04bf2
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-webextension-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5 8c-.971 0-1 1-1.75 1a.765.765 0 0 1-.75-.75V5a1 1 0 0 0-1-1H7.75A.765.765 0 0 1 7 3.25c0-.75 1-.779 1-1.75C8 .635 7.1 0 6 0S4 .635 4 1.5c0 .971 1 1 1 1.75a.765.765 0 0 1-.75.75H1a1 1 0 0 0-1 1v2.25A.765.765 0 0 0 .75 8c.75 0 .779-1 1.75-1C3.365 7 4 7.9 4 9s-.635 2-1.5 2c-.971 0-1-1-1.75-1a.765.765 0 0 0-.75.75V15a1 1 0 0 0 1 1h3.25a.765.765 0 0 0 .75-.75c0-.75-1-.779-1-1.75 0-.865.9-1.5 2-1.5s2 .635 2 1.5c0 .971-1 1-1 1.75a.765.765 0 0 0 .75.75H11a1 1 0 0 0 1-1v-3.25a.765.765 0 0 1 .75-.75c.75 0 .779 1 1.75 1 .865 0 1.5-.9 1.5-2s-.635-2-1.5-2z"/></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/icon-removed-bookmark.svg b/browser/components/newtab/data/content/assets/icon-removed-bookmark.svg
new file mode 100644
index 0000000000..e222da3bfd
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/icon-removed-bookmark.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M15.845 6.063A1.1 1.1 0 0 0 15 5.331l-1.125-.2-1.729 1.723.872.156-2.45 2.635.5 3.572L8 11.618l-1.291.673-3.468 3.468a1.057 1.057 0 0 0 1.066.038L8 13.874l3.688 1.921a1.1 1.1 0 0 0 1.6-1.126l-.609-4.358 2.926-3.147a1.1 1.1 0 0 0 .24-1.101zm-1.138-4.77a1 1 0 0 0-1.414 0L10.6 3.983 8.984.733A1.093 1.093 0 0 0 8 .124a1.1 1.1 0 0 0-.985.609L5.089 4.6l-4.082.729a1.1 1.1 0 0 0-.614 1.833L3.32 10.31l-.155 1.111-1.872 1.872a1 1 0 1 0 1.414 1.414l12-12a1 1 0 0 0 0-1.414zM2.981 7.01l3.449-.617L8 3.243l1.111 2.232L5.2 9.391z"></path></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/pocket-onboarding.avif b/browser/components/newtab/data/content/assets/pocket-onboarding.avif
new file mode 100644
index 0000000000..9bd1b3e524
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/pocket-onboarding.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/pocket-onboarding@2x.avif b/browser/components/newtab/data/content/assets/pocket-onboarding@2x.avif
new file mode 100644
index 0000000000..6817e85f2f
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/pocket-onboarding@2x.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/pocket-swoosh.svg b/browser/components/newtab/data/content/assets/pocket-swoosh.svg
new file mode 100644
index 0000000000..0d81c7c453
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/pocket-swoosh.svg
@@ -0,0 +1,11 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="272" height="221" viewBox="0 0 272 221" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M198.149 217.506C197.027 216.807 149.258 224.525 138.051 218.986C128.922 214.474 157.583 197.26 145.113 191.397C117.229 178.287 84.755 163.892 64.5172 157.556C4.66231 138.817 -3.24386 110.107 0.935229 88.348C5.11432 66.5887 18.2486 80.7262 34.9152 65.6889C51.5819 50.6516 104.716 18.9837 151.432 20.6766C198.149 22.3695 221.034 23.4152 223.074 41.0915C225.114 58.7678 212.726 69.4233 186.059 64.4441C159.393 59.4649 125.96 54.8342 131.034 48.6101C136.109 42.3363 137.092 40.245 145.898 41.0915C149.707 41.4576 140.907 45.1279 146.728 43.0367C152.549 40.9454 143.027 37.6002 138.051 37.1521C133.076 36.7537 146.728 39.6303 120.639 48.6101C93.2259 62.3528 76.5072 66.4358 72.7758 83.166C69.0445 99.8963 94.0196 132.46 127.85 149.838C161.681 167.216 210.686 192.51 208.547 196.742C206.457 200.975 198.149 217.506 198.149 217.506Z" fill="#F9BFD1"/>
+<path d="M205.746 200.389C205.945 200.041 90.7455 153.648 52.4781 140.797C14.2106 127.945 -1.18287 102.054 4.62425 81.3135C10.4314 60.573 24.4848 48.7046 69.0061 31.8343C113.527 14.9639 191.105 8.46391 207.781 22.7044C224.458 36.945 242.624 58.1321 222.324 66.4184C222.324 66.4184 153.234 65.6741 144.201 60.2161C135.168 54.8077 122.459 48.2869 127.522 44.5655C132.584 40.8441 161.225 49.3992 180.582 50.6397C199.939 51.8802 214.929 45.3801 191.253 38.4335C167.578 31.4869 141.024 31.8839 119.682 38.4335C103.7 43.3458 89.8521 52.2771 74.9125 61.8535C59.9728 71.4299 45.4301 82.1475 47.9118 100.01C50.3935 117.873 94.7162 117.923 122.61 128.342C150.504 138.762 213.291 185.404 213.291 185.404L205.746 200.389Z" fill="#EF4056"/>
+<path d="M209.225 191.228C207.134 188.504 145.073 153.09 82.9625 136.052C20.8518 119.064 -2.24359 101.58 8.1082 70.5247C15.8223 47.3945 10.0023 68.8903 27.9189 55.2201C45.8354 41.55 83.0621 7.86999 143.929 10.4455C204.795 13.021 229.132 17.4787 236.647 29.5144C244.162 41.55 253.717 58.9348 236.647 62.2533C219.576 65.5718 183.594 68.1473 163.438 63.8878C140.096 58.9844 114.225 47.2717 117.211 45.5877C117.211 45.5877 160.932 53.89 169.509 55.2021C178.087 56.5142 210.369 60.5693 215.794 52.2979C221.219 44.0265 207.035 31.9908 183.295 29.5144C159.556 27.0379 131.238 22.7783 95.9023 37.6867C73.208 47.1964 56.2868 60.5693 41.7047 70.5247C23.7383 82.808 25.4305 101.481 48.3736 100.738C71.3168 99.9947 145.72 133.576 163.488 138.974C181.255 144.373 218.929 173.546 218.929 173.546L209.225 191.228Z" fill="#FCB643"/>
+<path d="M213.615 176.342C213.615 176.342 159 138.263 118.647 129.929C78.2938 121.594 19.2055 116.205 10.5087 89.3049C1.21562 60.5587 20.8951 78.0759 20.8951 78.0759C23.9266 76.429 20.8951 66.4976 43.4073 48.3316C66.9134 29.3671 94.3952 17.8886 136.14 17.0901C177.884 16.2916 210.634 18.737 223.654 29.6166C233.692 38.0009 241.942 60.5587 210.335 61.8064C178.729 63.054 133.624 51.4307 123.139 45.9908C112.603 40.551 109.227 51.1025 155.819 67.6454C179.574 73.4845 232.053 80.6211 243.681 66.3978C255.31 52.1744 267.784 21.6815 218.684 11.251C169.585 0.820553 107.266 -2.12393 69.597 11.251C31.9276 24.626 17.5158 48.0322 15.8759 65.1501C14.2359 82.2681 27.6538 98.9368 41.7674 101.632C55.9307 104.327 121.181 104.826 139.519 110.266C157.857 115.705 199.353 147.745 205.515 155.88C211.677 164.015 218.635 173.547 218.635 173.547L213.615 176.342Z" fill="#1CB0A8"/>
+<path d="M202.82 146.773C190.024 133.777 166.02 102.605 122.971 90.7038C80.7161 79.0517 51.7524 81.9896 39.6015 81.5414C-11.5313 79.5496 2.55382 51.0168 9.29879 41.5059C16.0438 31.995 44.2139 5.20501 128.476 0.673622C213.185 -3.90756 273.791 15.2637 271.311 46.4356C268.831 77.6076 212.342 88.214 176.633 80.2966C122.971 68.3983 104.917 48.7908 107.149 45.9026C109.331 42.9647 121.033 49.2374 138.241 57.6725C155.449 66.1076 244.331 84.8777 250.134 51.5148C255.936 18.1518 198.654 9.83599 147.62 8.98946C100.604 8.24253 54.5793 27.3142 31.3191 44.4438C21.1025 51.9629 -8.55554 95.9821 54.5793 106.987C117.714 117.992 137.255 109.078 169.145 130.341C201.034 151.604 215.891 172.154 217.153 170.526C218.415 168.898 215.615 159.77 202.82 146.773Z" fill="#95D2FF"/>
+<path d="M138.128 44.9027C124.799 45.702 103.087 55.76 93.8965 60.689C93.8965 58.4909 95.1659 53.0556 100.244 48.8992C106.591 43.7037 120.079 35.1111 138.128 35.5108C156.178 35.9104 154.79 43.9035 138.128 44.9027Z" fill="#FFC0D1"/>
+</svg>
diff --git a/browser/components/newtab/data/content/assets/remote/mountain.svg b/browser/components/newtab/data/content/assets/remote/mountain.svg
new file mode 100644
index 0000000000..4511148820
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/remote/mountain.svg
@@ -0,0 +1,12 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="115" height="89" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M86.676 83.103c15.377 0 27.842-12.465 27.842-27.842 0-15.376-12.465-27.842-27.842-27.842-15.377 0-27.842 12.466-27.842 27.842 0 15.377 12.465 27.842 27.842 27.842z" fill="#FFEA80"/>
+ <path d="M84.457 41.839l-25.25 43.159a2.59 2.59 0 002.232 3.898h50.484a2.589 2.589 0 002.591-2.6 2.594 2.594 0 00-.354-1.298L88.914 41.84a2.586 2.586 0 00-4.457 0z" fill="#7542E5"/>
+ <path d="M114.16 85.031L95.432 52.967l-4.084 4.084-4.653-4.657-4.656 4.657-4.085-4.085L59.21 85.03a2.591 2.591 0 002.233 3.904h50.48a2.59 2.59 0 002.236-3.904z" fill="#7542E5"/>
+ <path d="M86.68 52.394l3.923 3.922a1.037 1.037 0 001.465 0l3.34-3.34-6.493-11.142a2.587 2.587 0 00-4.467 0l-6.509 11.132 3.34 3.34a1.037 1.037 0 001.466 0l3.936-3.912z" fill="#B48EFF"/>
+ <path d="M41.036 17.67L.368 85.014a2.586 2.586 0 002.3 3.893h81.346a2.586 2.586 0 002.295-3.893L45.636 17.67a2.706 2.706 0 00-4.6 0z" fill="#AB71FF"/>
+ <path d="M56.329 35.373L45.635 17.671a2.706 2.706 0 00-4.594 0L30.347 35.373l-.954 1.58 5.56 7.635a1.032 1.032 0 001.67 0l6.68-9.214 6.704 9.214a1.03 1.03 0 001.67 0l5.583-7.663-.931-1.551z" fill="#D9BFFF"/>
+ <path d="M41.086 1.131v14.41a1.13 1.13 0 001.13 1.131h26.507a1.12 1.12 0 001.044-.697 1.14 1.14 0 00-.242-1.236l-6.409-6.403 6.39-6.404c.322-.325.417-.811.243-1.235A1.135 1.135 0 0068.723 0H42.216c-.624 0-1.13.507-1.13 1.131z" fill="#FFA266"/>
+</svg>
diff --git a/browser/components/newtab/data/content/assets/remote/umbrella.png b/browser/components/newtab/data/content/assets/remote/umbrella.png
new file mode 100644
index 0000000000..3488d135c6
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/remote/umbrella.png
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/spinner.svg b/browser/components/newtab/data/content/assets/spinner.svg
new file mode 100644
index 0000000000..2964a31731
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/spinner.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="73" height="73" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="93.093%" y1="52.773%" x2="68.513%" y2="119.326%" id="a"><stop stop-color="#FFF" stop-opacity="0" offset="0%"/><stop stop-color="#FFF" offset="69.37%"/><stop stop-color="#FFF" offset="100%"/><stop stop-color="#FFF" stop-opacity=".005" offset="100%"/><stop stop-color="#FFF" stop-opacity="0" offset="100%"/><stop stop-color="#FFF" stop-opacity="0" offset="100%"/></linearGradient><path id="b" d="M0 0h48v60H0z"/></defs><g transform="translate(-5 -1)" fill="none" fill-rule="evenodd"><path d="M41.8 73.8c-19.9 0-36-16.1-36-36 0-19.7 15.8-35.6 35.3-36h.7c2.8.4 5 2.7 5 5.5s-2.2 5.2-5 5.4c-13.8.1-25 11.3-25 25.1s11.2 25 25 25 25-11.2 25-25h11c0 19.9-16.1 36-36 36z" fill="url(#a)"/><mask id="c" fill="#fff"><use href="#b"/></mask><path d="M41.8 73.8c-19.9 0-36-16.1-36-36 0-19.7 15.8-35.6 35.3-36h.7c2.8.4 5 2.7 5 5.5s-2.2 5.2-5 5.4c-13.8.1-25 11.3-25 25.1s11.2 25 25 25 25-11.2 25-25h11c0 19.9-16.1 36-36 36z" fill="#FFF" mask="url(#c)"/></g></svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/newtab-render.js b/browser/components/newtab/data/content/newtab-render.js
new file mode 100644
index 0000000000..4fd46fad03
--- /dev/null
+++ b/browser/components/newtab/data/content/newtab-render.js
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// exported by activity-stream.bundle.js
+if (window.__FROM_STARTUP_CACHE__) {
+ window.NewtabRenderUtils.renderCache(window.__STARTUP_STATE__);
+} else {
+ window.NewtabRenderUtils.renderWithoutState();
+}
diff --git a/browser/components/newtab/data/content/tippytop/favicons/adidas.png b/browser/components/newtab/data/content/tippytop/favicons/adidas.png
new file mode 100644
index 0000000000..fd7123958c
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/adidas.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/aliexpress-com.ico b/browser/components/newtab/data/content/tippytop/favicons/aliexpress-com.ico
new file mode 100644
index 0000000000..99b86e13aa
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/aliexpress-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/allegro-pl.ico b/browser/components/newtab/data/content/tippytop/favicons/allegro-pl.ico
new file mode 100644
index 0000000000..42b4f90149
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/allegro-pl.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/amazon.ico b/browser/components/newtab/data/content/tippytop/favicons/amazon.ico
new file mode 100644
index 0000000000..1c39eaf8fe
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/amazon.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/avito-ru.ico b/browser/components/newtab/data/content/tippytop/favicons/avito-ru.ico
new file mode 100644
index 0000000000..c41847b27a
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/avito-ru.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/baidu-com.png b/browser/components/newtab/data/content/tippytop/favicons/baidu-com.png
new file mode 100644
index 0000000000..e63737eb30
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/baidu-com.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/bbc-uk.ico b/browser/components/newtab/data/content/tippytop/favicons/bbc-uk.ico
new file mode 100644
index 0000000000..8f62b07af8
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/bbc-uk.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/bing-com.ico b/browser/components/newtab/data/content/tippytop/favicons/bing-com.ico
new file mode 100644
index 0000000000..fdc021cfeb
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/bing-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/ctrip-com.ico b/browser/components/newtab/data/content/tippytop/favicons/ctrip-com.ico
new file mode 100644
index 0000000000..fa44291d84
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/ctrip-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/duckduckgo-com.ico b/browser/components/newtab/data/content/tippytop/favicons/duckduckgo-com.ico
new file mode 100644
index 0000000000..3ad20825c1
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/duckduckgo-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/ebay.ico b/browser/components/newtab/data/content/tippytop/favicons/ebay.ico
new file mode 100644
index 0000000000..3af7a36484
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/ebay.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/etsy.ico b/browser/components/newtab/data/content/tippytop/favicons/etsy.ico
new file mode 100644
index 0000000000..a94f3efd4f
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/etsy.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/facebook-com.ico b/browser/components/newtab/data/content/tippytop/favicons/facebook-com.ico
new file mode 100644
index 0000000000..8ce319b8f7
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/facebook-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/geico.png b/browser/components/newtab/data/content/tippytop/favicons/geico.png
new file mode 100644
index 0000000000..3f61497dd8
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/geico.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/google-com.ico b/browser/components/newtab/data/content/tippytop/favicons/google-com.ico
new file mode 100644
index 0000000000..82339b3b1d
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/google-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/hrblock.ico b/browser/components/newtab/data/content/tippytop/favicons/hrblock.ico
new file mode 100644
index 0000000000..e0d7be35e0
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/hrblock.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/ifeng-com.ico b/browser/components/newtab/data/content/tippytop/favicons/ifeng-com.ico
new file mode 100644
index 0000000000..b0003e058f
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/ifeng-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/iqiyi-com.ico b/browser/components/newtab/data/content/tippytop/favicons/iqiyi-com.ico
new file mode 100644
index 0000000000..4b179bf4d5
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/iqiyi-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/leboncoin-fr.png b/browser/components/newtab/data/content/tippytop/favicons/leboncoin-fr.png
new file mode 100644
index 0000000000..e23e2a34b0
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/leboncoin-fr.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/nike.ico b/browser/components/newtab/data/content/tippytop/favicons/nike.ico
new file mode 100644
index 0000000000..7788d580af
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/nike.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/ok-ru.ico b/browser/components/newtab/data/content/tippytop/favicons/ok-ru.ico
new file mode 100644
index 0000000000..7db8914287
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/ok-ru.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/olx-pl.ico b/browser/components/newtab/data/content/tippytop/favicons/olx-pl.ico
new file mode 100644
index 0000000000..b2a28638f8
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/olx-pl.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/reddit-com.png b/browser/components/newtab/data/content/tippytop/favicons/reddit-com.png
new file mode 100644
index 0000000000..3c09931835
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/reddit-com.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/samsung.ico b/browser/components/newtab/data/content/tippytop/favicons/samsung.ico
new file mode 100644
index 0000000000..eb8c814256
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/samsung.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/turbotax.png b/browser/components/newtab/data/content/tippytop/favicons/turbotax.png
new file mode 100644
index 0000000000..c1d52f99fb
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/turbotax.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/twitter-com.ico b/browser/components/newtab/data/content/tippytop/favicons/twitter-com.ico
new file mode 100644
index 0000000000..e5aaff4379
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/twitter-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/vk-com.ico b/browser/components/newtab/data/content/tippytop/favicons/vk-com.ico
new file mode 100644
index 0000000000..0066072c39
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/vk-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/vodafone.png b/browser/components/newtab/data/content/tippytop/favicons/vodafone.png
new file mode 100644
index 0000000000..1a4ba0089e
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/vodafone.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/weibo-com.ico b/browser/components/newtab/data/content/tippytop/favicons/weibo-com.ico
new file mode 100644
index 0000000000..11a88045ec
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/weibo-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/wikipedia-org.ico b/browser/components/newtab/data/content/tippytop/favicons/wikipedia-org.ico
new file mode 100644
index 0000000000..e70021849b
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/wikipedia-org.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/wix.ico b/browser/components/newtab/data/content/tippytop/favicons/wix.ico
new file mode 100644
index 0000000000..cabcb650a7
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/wix.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/wykop-pl.png b/browser/components/newtab/data/content/tippytop/favicons/wykop-pl.png
new file mode 100644
index 0000000000..5aae5b17f2
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/wykop-pl.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/yandex-com.png b/browser/components/newtab/data/content/tippytop/favicons/yandex-com.png
new file mode 100644
index 0000000000..d1c3f3f8b1
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/yandex-com.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/yandex-ru.png b/browser/components/newtab/data/content/tippytop/favicons/yandex-ru.png
new file mode 100644
index 0000000000..eb187398c7
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/yandex-ru.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/youtube-com.png b/browser/components/newtab/data/content/tippytop/favicons/youtube-com.png
new file mode 100644
index 0000000000..b0c05d0716
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/youtube-com.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/favicons/zhihu-com.ico b/browser/components/newtab/data/content/tippytop/favicons/zhihu-com.ico
new file mode 100644
index 0000000000..c83d0e6d86
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/favicons/zhihu-com.ico
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/adidas@2x.png b/browser/components/newtab/data/content/tippytop/images/adidas@2x.png
new file mode 100644
index 0000000000..f07c17a9a8
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/adidas@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/aliexpress-com@2x.png b/browser/components/newtab/data/content/tippytop/images/aliexpress-com@2x.png
new file mode 100644
index 0000000000..76fac2e935
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/aliexpress-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/allegro-pl@2x.png b/browser/components/newtab/data/content/tippytop/images/allegro-pl@2x.png
new file mode 100644
index 0000000000..7aa6ffd4b3
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/allegro-pl@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/amazon@2x.png b/browser/components/newtab/data/content/tippytop/images/amazon@2x.png
new file mode 100644
index 0000000000..fb20eea921
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/amazon@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/avito-ru@2x.png b/browser/components/newtab/data/content/tippytop/images/avito-ru@2x.png
new file mode 100644
index 0000000000..9ba32a8d96
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/avito-ru@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/baidu-com@2x.png b/browser/components/newtab/data/content/tippytop/images/baidu-com@2x.png
new file mode 100644
index 0000000000..b7662dd21f
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/baidu-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/bbc-uk@2x.png b/browser/components/newtab/data/content/tippytop/images/bbc-uk@2x.png
new file mode 100644
index 0000000000..e019ac3de6
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/bbc-uk@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/bing-com@2x.svg b/browser/components/newtab/data/content/tippytop/images/bing-com@2x.svg
new file mode 100644
index 0000000000..1afdee989a
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/bing-com@2x.svg
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+ viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
+<g>
+
+ <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="20.4168" y1="-3.0325" x2="39.7202" y2="-14.1773" gradientTransform="matrix(1 0 0 -1 0 18)">
+ <stop offset="0" style="stop-color:#37BDFF"/>
+ <stop offset="0.1832" style="stop-color:#33BFFD"/>
+ <stop offset="0.3576" style="stop-color:#28C5F5"/>
+ <stop offset="0.528" style="stop-color:#15D0E9"/>
+ <stop offset="0.5468" style="stop-color:#12D1E7"/>
+ <stop offset="0.5903" style="stop-color:#1CD2E5"/>
+ <stop offset="0.7679" style="stop-color:#42D8DC"/>
+ <stop offset="0.9107" style="stop-color:#59DBD6"/>
+ <stop offset="1" style="stop-color:#62DCD4"/>
+ </linearGradient>
+ <path fill="url(#SVGID_1_)" d="M39.002,30.234c0,0.437-0.024,0.871-0.071,1.299c-0.282,2.61-1.412,4.969-3.112,6.795
+ c0.214-0.238,0.407-0.493,0.577-0.767c0.131-0.208,0.247-0.425,0.345-0.651c0.036-0.074,0.068-0.152,0.095-0.229
+ c0.032-0.074,0.059-0.152,0.083-0.229c0.027-0.071,0.051-0.146,0.071-0.22c0.021-0.077,0.042-0.154,0.059-0.232
+ c0.003-0.009,0.006-0.018,0.009-0.027c0.018-0.077,0.032-0.154,0.047-0.232c0.015-0.08,0.03-0.161,0.042-0.24
+ c0-0.003,0-0.003,0-0.006c0.012-0.074,0.021-0.149,0.027-0.226c0.018-0.176,0.027-0.35,0.027-0.529
+ c0-1.004-0.277-1.947-0.761-2.749c-0.11-0.187-0.232-0.365-0.365-0.535c-0.157-0.202-0.327-0.392-0.511-0.568
+ c-0.458-0.443-0.993-0.806-1.584-1.061c-0.255-0.113-0.523-0.205-0.796-0.274c-0.003,0-0.009-0.003-0.012-0.003l-0.095-0.032
+ l-1.385-0.475V29.04l-3.623-1.245c-0.012-0.003-0.027-0.003-0.036-0.006l-0.226-0.083c-0.728-0.285-1.332-0.82-1.709-1.501
+ l-1.322-3.374l-1.516-3.864l-0.291-0.746l-0.074-0.152c-0.083-0.202-0.128-0.422-0.128-0.651c0-0.059,0-0.119,0.006-0.172
+ c0.086-0.85,0.808-1.516,1.682-1.516c0.232,0,0.455,0.047,0.657,0.134l6.751,3.462l1.332,0.681
+ c0.705,0.419,1.361,0.913,1.961,1.468c2.176,1.998,3.602,4.797,3.816,7.927C38.99,29.679,39.002,29.955,39.002,30.234z"/>
+
+ <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="9.1031" y1="-19.7528" x2="37.1978" y2="-19.7528" gradientTransform="matrix(1 0 0 -1 0 18)">
+ <stop offset="0" style="stop-color:#39D2FF"/>
+ <stop offset="0.1501" style="stop-color:#38CEFE"/>
+ <stop offset="0.2931" style="stop-color:#35C3FA"/>
+ <stop offset="0.4327" style="stop-color:#2FB0F3"/>
+ <stop offset="0.5468" style="stop-color:#299AEB"/>
+ <stop offset="0.5827" style="stop-color:#2692EC"/>
+ <stop offset="0.7635" style="stop-color:#1A6CF1"/>
+ <stop offset="0.909" style="stop-color:#1355F4"/>
+ <stop offset="1" style="stop-color:#104CF5"/>
+ </linearGradient>
+ <path fill="url(#SVGID_2_)" d="M37.198,34.74c0,0.345-0.032,0.678-0.092,1.002c-0.018,0.086-0.036,0.172-0.056,0.259
+ c-0.039,0.154-0.08,0.303-0.131,0.452c-0.027,0.077-0.054,0.154-0.083,0.229c-0.03,0.077-0.062,0.152-0.095,0.229
+ c-0.098,0.226-0.214,0.443-0.345,0.651c-0.169,0.274-0.363,0.529-0.577,0.767c-0.984,1.088-4.325,3.028-5.556,3.816l-2.733,1.67
+ c-2.003,1.234-3.896,2.107-6.283,2.167c-0.113,0.003-0.223,0.006-0.333,0.006c-0.154,0-0.306-0.003-0.458-0.009
+ c-4.042-0.154-7.567-2.324-9.6-5.531c-0.93-1.465-1.545-3.147-1.753-4.954c0.437,2.47,2.589,4.342,5.184,4.342
+ c0.909,0,1.763-0.229,2.508-0.633c0.006-0.003,0.012-0.006,0.018-0.009l0.267-0.161l1.088-0.642l1.385-0.82v-0.039l0.179-0.107
+ l12.391-7.341l0.954-0.565l0.095,0.032c0.003,0,0.009,0.003,0.012,0.003c0.274,0.068,0.541,0.161,0.796,0.274
+ c0.592,0.255,1.126,0.618,1.584,1.061c0.184,0.176,0.354,0.365,0.511,0.568c0.134,0.169,0.255,0.348,0.365,0.535
+ C36.921,32.793,37.198,33.736,37.198,34.74L37.198,34.74z"/>
+
+ <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="14.2883" y1="-22.2219" x2="14.2883" y2="15.3467" gradientTransform="matrix(1 0 0 -1 0 18)">
+ <stop offset="0" style="stop-color:#1B48EF"/>
+ <stop offset="0.1221" style="stop-color:#1C51F0"/>
+ <stop offset="0.3212" style="stop-color:#1E69F5"/>
+ <stop offset="0.5676" style="stop-color:#2190FB"/>
+ <stop offset="1" style="stop-color:#26B8F4"/>
+ </linearGradient>
+ <path fill="url(#SVGID_3_)" d="M19.557,10.241l-0.004,27.328l-1.385,0.821l-1.089,0.641l-0.268,0.162
+ c-0.004,0-0.012,0.004-0.016,0.008c-0.747,0.402-1.6,0.634-2.51,0.634c-2.595,0-4.744-1.873-5.183-4.342
+ c-0.021-0.114-0.036-0.232-0.049-0.345c-0.016-0.215-0.028-0.427-0.032-0.642V3.749c0-0.971,0.788-1.763,1.763-1.763
+ c0.365,0,0.706,0.114,0.987,0.3l5.39,3.522c0.029,0.024,0.061,0.045,0.094,0.065C18.647,6.824,19.557,8.425,19.557,10.241z"/>
+
+ <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="14.6872" y1="-26.6866" x2="32.0255" y2="-9.3483" gradientTransform="matrix(1 0 0 -1 0 18)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.3726" style="stop-color:#FDFDFD"/>
+ <stop offset="0.5069" style="stop-color:#F6F6F6"/>
+ <stop offset="0.6026" style="stop-color:#EBEBEB"/>
+ <stop offset="0.68" style="stop-color:#DADADA"/>
+ <stop offset="0.7463" style="stop-color:#C4C4C4"/>
+ <stop offset="0.805" style="stop-color:#A8A8A8"/>
+ <stop offset="0.8581" style="stop-color:#888888"/>
+ <stop offset="0.9069" style="stop-color:#626262"/>
+ <stop offset="0.9523" style="stop-color:#373737"/>
+ <stop offset="0.9926" style="stop-color:#090909"/>
+ <stop offset="1" style="stop-color:#000000"/>
+ </linearGradient>
+ <path opacity="0.15" fill="url(#SVGID_4_)" enable-background="new " d="M37.198,34.74c0,0.345-0.032,0.678-0.092,1.002
+ c-0.018,0.086-0.036,0.172-0.056,0.259c-0.039,0.154-0.08,0.303-0.131,0.452c-0.027,0.077-0.054,0.154-0.083,0.229
+ c-0.03,0.077-0.062,0.152-0.095,0.229c-0.098,0.226-0.214,0.443-0.345,0.651c-0.169,0.274-0.362,0.529-0.577,0.767
+ c-0.984,1.088-4.325,3.028-5.556,3.816l-2.733,1.67c-2.003,1.234-3.896,2.107-6.283,2.167c-0.113,0.003-0.223,0.006-0.333,0.006
+ c-0.154,0-0.306-0.003-0.458-0.009c-4.042-0.154-7.567-2.324-9.6-5.531c-0.93-1.465-1.545-3.147-1.753-4.954
+ c0.437,2.47,2.589,4.342,5.184,4.342c0.909,0,1.763-0.229,2.508-0.633c0.006-0.003,0.012-0.006,0.018-0.009l0.267-0.161
+ l1.088-0.642l1.385-0.82v-0.039l0.179-0.107l12.391-7.341l0.954-0.565l0.095,0.032c0.003,0,0.009,0.003,0.012,0.003
+ c0.274,0.068,0.541,0.161,0.796,0.274c0.592,0.255,1.126,0.618,1.584,1.061c0.184,0.176,0.354,0.365,0.511,0.568
+ c0.134,0.169,0.255,0.348,0.365,0.535C36.921,32.793,37.198,33.736,37.198,34.74L37.198,34.74z"/>
+
+ <linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="14.2883" y1="16.0132" x2="14.2883" y2="-21.8356" gradientTransform="matrix(1 0 0 -1 0 18)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.3726" style="stop-color:#FDFDFD"/>
+ <stop offset="0.5069" style="stop-color:#F6F6F6"/>
+ <stop offset="0.6026" style="stop-color:#EBEBEB"/>
+ <stop offset="0.68" style="stop-color:#DADADA"/>
+ <stop offset="0.7463" style="stop-color:#C4C4C4"/>
+ <stop offset="0.805" style="stop-color:#A8A8A8"/>
+ <stop offset="0.8581" style="stop-color:#888888"/>
+ <stop offset="0.9069" style="stop-color:#626262"/>
+ <stop offset="0.9523" style="stop-color:#373737"/>
+ <stop offset="0.9926" style="stop-color:#090909"/>
+ <stop offset="1" style="stop-color:#000000"/>
+ </linearGradient>
+ <path opacity="0.1" fill="url(#SVGID_5_)" enable-background="new " d="M19.557,10.241l-0.004,27.328l-1.385,0.821l-1.089,0.641
+ l-0.268,0.162c-0.004,0-0.012,0.004-0.016,0.008c-0.747,0.402-1.6,0.634-2.51,0.634c-2.595,0-4.744-1.873-5.183-4.342
+ c-0.021-0.114-0.036-0.232-0.049-0.345c-0.016-0.215-0.028-0.427-0.032-0.642V3.749c0-0.971,0.788-1.763,1.763-1.763
+ c0.365,0,0.706,0.114,0.987,0.3l5.39,3.522c0.029,0.024,0.061,0.045,0.094,0.065C18.647,6.824,19.557,8.425,19.557,10.241z"/>
+</g>
+</svg>
diff --git a/browser/components/newtab/data/content/tippytop/images/ctrip-com@2x.png b/browser/components/newtab/data/content/tippytop/images/ctrip-com@2x.png
new file mode 100644
index 0000000000..76a81da5af
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/ctrip-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/duckduckgo-com@2x.svg b/browser/components/newtab/data/content/tippytop/images/duckduckgo-com@2x.svg
new file mode 100644
index 0000000000..a28cc833cd
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/duckduckgo-com@2x.svg
@@ -0,0 +1,12 @@
+<svg fill="none" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#DE5833" fill-rule="evenodd" d="M64 128c35.346 0 64-28.654 64-64 0-35.346-28.654-64-64-64C28.654 0 0 28.654 0 64c0 35.346 28.654 64 64 64Z" clip-rule="evenodd"/>
+ <path fill="#DDD" fill-rule="evenodd" d="M73 111.75c0-.5.123-.614-1.466-3.782-4.224-8.459-8.47-20.384-6.54-28.075.353-1.397-3.978-51.744-7.04-53.365-3.402-1.813-7.588-4.69-11.418-5.33-1.943-.31-4.49-.164-6.482.105-.353.047-.368.683-.03.798 1.308.443 2.895 1.212 3.83 2.375.178.22-.06.566-.342.577-.882.032-2.482.402-4.593 2.195-.244.207-.041.592.273.53 4.536-.897 9.17-.455 11.9 2.027.177.16.084.45-.147.512-23.694 6.44-19.003 27.05-12.696 52.344 5.619 22.53 7.733 29.792 8.4 32.004a.718.718 0 0 0 .423.467C55.228 118.38 73 118.524 73 113v-1.25Z" clip-rule="evenodd"/>
+ <path fill="#fff" fill-rule="evenodd" d="M122.75 64c0 32.447-26.303 58.75-58.75 58.75S5.25 96.447 5.25 64 31.553 5.25 64 5.25 122.75 31.553 122.75 64Zm-72.46 51.986c-1.624-5.016-6.161-19.551-10.643-37.92l-.447-1.828-.003-.016c-5.425-22.155-9.855-40.252 14.427-45.937.222-.052.33-.317.183-.492-2.786-3.305-8.005-4.388-14.604-2.111-.27.093-.506-.18-.338-.412 1.294-1.784 3.823-3.155 5.072-3.756.258-.124.242-.502-.031-.588a27.848 27.848 0 0 0-3.771-.9c-.37-.059-.404-.693-.032-.743 9.356-1.259 19.125 1.55 24.028 7.726a.325.325 0 0 0 .185.114c17.953 3.855 19.239 32.235 17.17 33.528-.407.255-1.714.108-3.438-.085-6.985-.781-20.818-2.329-9.401 18.948.113.21-.037.488-.272.525-6.416.997 1.755 21.034 7.812 34.323 23.815-5.52 41.563-26.868 41.563-52.362 0-29.685-24.065-53.75-53.75-53.75S10.25 34.315 10.25 64c0 24.947 16.995 45.924 40.04 51.986Z" clip-rule="evenodd"/>
+ <path fill="#3CA82B" d="M84.28 90.698c-1.367-.633-6.621 3.135-10.11 6.028-.728-1.031-2.103-1.78-5.203-1.242-2.713.472-4.211 1.126-4.88 2.254-4.283-1.623-11.488-4.13-13.229-1.71-1.902 2.646.476 15.161 3.003 16.786 1.32.849 7.63-3.208 10.926-6.005.532.749 1.388 1.178 3.148 1.137 2.662-.062 6.979-.681 7.649-1.921.04-.075.075-.164.105-.266 3.388 1.266 9.35 2.606 10.682 2.406 3.47-.521-.484-16.723-2.09-17.467Z"/>
+ <path fill="#4CBA3C" d="M74.49 97.097c.144.256.26.526.358.8.483 1.352 1.27 5.648.674 6.709-.595 1.062-4.459 1.574-6.843 1.615-2.384.041-2.92-.831-3.403-2.181-.387-1.081-.577-3.621-.572-5.075-.098-2.158.69-2.916 4.334-3.506 2.696-.436 4.121.071 4.944.94 3.828-2.857 10.215-6.889 10.838-6.152 3.106 3.674 3.499 12.42 2.826 15.939-.22 1.151-10.505-1.139-10.505-2.38 0-5.152-1.337-6.565-2.65-6.71Zm-22.53-1.609c.843-1.333 7.674.325 11.424 1.993 0 0-.77 3.491.456 7.604.359 1.203-8.627 6.558-9.8 5.637-1.355-1.065-3.85-12.432-2.08-15.234Z"/>
+ <path fill="#FC3" fill-rule="evenodd" d="M55.269 68.406c.553-2.403 3.127-6.932 12.321-6.822 4.648-.019 10.422-.002 14.25-.436a51.312 51.312 0 0 0 12.726-3.095c3.98-1.519 5.392-1.18 5.887-.272.544.999-.097 2.722-1.488 4.309-2.656 3.03-7.431 5.38-15.865 6.076-8.433.698-14.02-1.565-16.425 2.118-1.038 1.589-.236 5.333 7.92 6.512 11.02 1.59 20.072-1.917 21.19.201 1.119 2.118-5.323 6.428-16.362 6.518-11.039.09-17.934-3.865-20.379-5.83-3.102-2.495-4.49-6.133-3.775-9.279Z" clip-rule="evenodd"/>
+ <g fill="#14307E" opacity=".8">
+ <path d="M69.327 42.127c.616-1.008 1.981-1.786 4.216-1.786 2.234 0 3.285.889 4.013 1.88.148.202-.076.44-.306.34a59.869 59.869 0 0 1-.168-.073c-.817-.357-1.82-.795-3.54-.82-1.838-.026-2.997.435-3.727.831-.246.134-.634-.133-.488-.372Zm-25.157 1.29c2.17-.907 3.876-.79 5.081-.504.254.06.43-.213.227-.377-.935-.755-3.03-1.692-5.76-.674-2.437.909-3.585 2.796-3.592 4.038-.002.292.6.317.756.07.42-.67 1.12-1.646 3.289-2.553Z"/>
+ <path fill-rule="evenodd" d="M75.44 55.92a3.47 3.47 0 0 1-3.474-3.462 3.47 3.47 0 0 1 3.475-3.46 3.47 3.47 0 0 1 3.474 3.46 3.47 3.47 0 0 1-3.475 3.462Zm2.447-4.608a.899.899 0 0 0-1.799 0c0 .494.405.895.9.895.499 0 .9-.4.9-.895Zm-25.464 3.542a4.042 4.042 0 0 1-4.049 4.037 4.045 4.045 0 0 1-4.05-4.037 4.045 4.045 0 0 1 4.05-4.037 4.045 4.045 0 0 1 4.05 4.037Zm-1.193-1.338a1.05 1.05 0 0 0-2.097 0 1.048 1.048 0 0 0 2.097 0Z" clip-rule="evenodd"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/tippytop/images/ebay@2x.png b/browser/components/newtab/data/content/tippytop/images/ebay@2x.png
new file mode 100644
index 0000000000..744e2442ff
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/ebay@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/etsy@2x.jpg b/browser/components/newtab/data/content/tippytop/images/etsy@2x.jpg
new file mode 100644
index 0000000000..4bd477ca6a
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/etsy@2x.jpg
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/facebook-com@2x.png b/browser/components/newtab/data/content/tippytop/images/facebook-com@2x.png
new file mode 100644
index 0000000000..8827157878
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/facebook-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/geico@2x.jpg b/browser/components/newtab/data/content/tippytop/images/geico@2x.jpg
new file mode 100644
index 0000000000..938b7948e9
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/geico@2x.jpg
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/google-com@2x.png b/browser/components/newtab/data/content/tippytop/images/google-com@2x.png
new file mode 100644
index 0000000000..263bd973b1
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/google-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/hrblock@2x.png b/browser/components/newtab/data/content/tippytop/images/hrblock@2x.png
new file mode 100644
index 0000000000..ba66c46b72
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/hrblock@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/ifeng-com@2x.png b/browser/components/newtab/data/content/tippytop/images/ifeng-com@2x.png
new file mode 100644
index 0000000000..f7099f334e
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/ifeng-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/iqiyi-com@2x.png b/browser/components/newtab/data/content/tippytop/images/iqiyi-com@2x.png
new file mode 100644
index 0000000000..901d536ad9
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/iqiyi-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/leboncoin-fr@2x.png b/browser/components/newtab/data/content/tippytop/images/leboncoin-fr@2x.png
new file mode 100644
index 0000000000..af293fa8c3
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/leboncoin-fr@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/nike@2x.jpg b/browser/components/newtab/data/content/tippytop/images/nike@2x.jpg
new file mode 100644
index 0000000000..ac5d639d12
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/nike@2x.jpg
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/ok-ru@2x.png b/browser/components/newtab/data/content/tippytop/images/ok-ru@2x.png
new file mode 100644
index 0000000000..c771bf3ad9
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/ok-ru@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/olx-pl@2x.png b/browser/components/newtab/data/content/tippytop/images/olx-pl@2x.png
new file mode 100644
index 0000000000..964cd2df10
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/olx-pl@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/reddit-com@2x.png b/browser/components/newtab/data/content/tippytop/images/reddit-com@2x.png
new file mode 100644
index 0000000000..3b8833d6c6
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/reddit-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/samsung@2x.jpg b/browser/components/newtab/data/content/tippytop/images/samsung@2x.jpg
new file mode 100644
index 0000000000..dec2346e9f
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/samsung@2x.jpg
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/turbotax@2x.jpg b/browser/components/newtab/data/content/tippytop/images/turbotax@2x.jpg
new file mode 100644
index 0000000000..625703943f
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/turbotax@2x.jpg
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/twitter-com@2x.png b/browser/components/newtab/data/content/tippytop/images/twitter-com@2x.png
new file mode 100644
index 0000000000..e5835ff98a
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/twitter-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/vk-com@2x.png b/browser/components/newtab/data/content/tippytop/images/vk-com@2x.png
new file mode 100644
index 0000000000..b4c14412a4
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/vk-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/vodafone@2x.jpg b/browser/components/newtab/data/content/tippytop/images/vodafone@2x.jpg
new file mode 100644
index 0000000000..2597063b47
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/vodafone@2x.jpg
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/weibo-com@2x.png b/browser/components/newtab/data/content/tippytop/images/weibo-com@2x.png
new file mode 100644
index 0000000000..e047eaac87
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/weibo-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/wikipedia-org@2x.png b/browser/components/newtab/data/content/tippytop/images/wikipedia-org@2x.png
new file mode 100644
index 0000000000..53cf1af1c6
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/wikipedia-org@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/wix@2x.jpg b/browser/components/newtab/data/content/tippytop/images/wix@2x.jpg
new file mode 100644
index 0000000000..473caf38a3
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/wix@2x.jpg
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/wykop-pl@2x.png b/browser/components/newtab/data/content/tippytop/images/wykop-pl@2x.png
new file mode 100644
index 0000000000..fbde175696
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/wykop-pl@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/yandex-com@2x.png b/browser/components/newtab/data/content/tippytop/images/yandex-com@2x.png
new file mode 100644
index 0000000000..ebea409306
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/yandex-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/yandex-ru@2x.png b/browser/components/newtab/data/content/tippytop/images/yandex-ru@2x.png
new file mode 100644
index 0000000000..3d4ffd15c3
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/yandex-ru@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/youtube-com@2x.png b/browser/components/newtab/data/content/tippytop/images/youtube-com@2x.png
new file mode 100644
index 0000000000..6f1d7a1d7b
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/youtube-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/images/zhihu-com@2x.png b/browser/components/newtab/data/content/tippytop/images/zhihu-com@2x.png
new file mode 100644
index 0000000000..a1a9db845e
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/images/zhihu-com@2x.png
Binary files differ
diff --git a/browser/components/newtab/data/content/tippytop/top_sites.json b/browser/components/newtab/data/content/tippytop/top_sites.json
new file mode 100644
index 0000000000..82764a0527
--- /dev/null
+++ b/browser/components/newtab/data/content/tippytop/top_sites.json
@@ -0,0 +1,182 @@
+[
+ {
+ "domains": ["adidas.co.uk", "adidas.de", "adidas.fr"],
+ "image_url": "images/adidas@2x.png",
+ "favicon_url": "favicons/adidas.png"
+ },
+ {
+ "domains": ["aliexpress.com"],
+ "image_url": "images/aliexpress-com@2x.png",
+ "favicon_url": "favicons/aliexpress-com.ico"
+ },
+ {
+ "domains": ["allegro.pl"],
+ "image_url": "images/allegro-pl@2x.png",
+ "favicon_url": "favicons/allegro-pl.ico"
+ },
+ {
+ "domains": ["amazon.ae", "amazon.ca", "amazon.cn", "amazon.co.jp", "amazon.co.uk", "amazon.com", "amazon.com.au", "amazon.com.br", "amazon.com.mx", "amazon.de", "amazon.es", "amazon.fr", "amazon.in", "amazon.it", "amazon.nl", "amazon.sa", "amazon.se", "amazon.sg", "amazon.com.tr"],
+ "image_url": "images/amazon@2x.png",
+ "favicon_url": "favicons/amazon.ico"
+ },
+ {
+ "domains": ["avito.ru"],
+ "image_url": "images/avito-ru@2x.png",
+ "favicon_url": "favicons/avito-ru.ico"
+ },
+ {
+ "domains": ["baidu.com"],
+ "image_url": "images/baidu-com@2x.png",
+ "favicon_url": "favicons/baidu-com.png"
+ },
+ {
+ "domains": ["bbc.co.uk"],
+ "image_url": "images/bbc-uk@2x.png",
+ "favicon_url": "favicons/bbc-uk.ico"
+ },
+ {
+ "domains": ["bing.com"],
+ "image_url": "images/bing-com@2x.svg",
+ "favicon_url": "favicons/bing-com.ico"
+ },
+ {
+ "domains": ["ctrip.com"],
+ "image_url": "images/ctrip-com@2x.png",
+ "favicon_url": "favicons/ctrip-com.ico"
+ },
+ {
+ "domains": ["duckduckgo.com"],
+ "image_url": "images/duckduckgo-com@2x.svg",
+ "favicon_url": "favicons/duckduckgo-com.ico"
+ },
+ {
+ "domains": ["mx.ebay.com", "benl.ebay.be", "befr.ebay.be", "ebay.ca", "ebay.ch", "ebay.co.jp", "ebay.co.uk", "ebay.com", "ebay.com.au", "ebay.de", "ebay.es", "ebay.fr", "ebay.ie", "ebay.in", "ebay.it", "ebay.nl"],
+ "image_url": "images/ebay@2x.png",
+ "favicon_url": "favicons/ebay.ico"
+ },
+ {
+ "domains": ["etsy.com"],
+ "image_url": "images/etsy@2x.jpg",
+ "favicon_url": "favicons/etsy.ico"
+ },
+ {
+ "domains": ["facebook.com"],
+ "image_url": "images/facebook-com@2x.png",
+ "favicon_url": "favicons/facebook-com.ico"
+ },
+ {
+ "domains": ["geico.com"],
+ "image_url": "images/geico@2x.jpg",
+ "favicon_url": "favicons/geico.png"
+ },
+ {
+ "domains": ["google.com"],
+ "image_url": "images/google-com@2x.png",
+ "favicon_url": "favicons/google-com.ico"
+ },
+ {
+ "domains": ["hrblock.com"],
+ "image_url": "images/hrblock@2x.png",
+ "favicon_url": "favicons/hrblock.ico"
+ },
+ {
+ "domains": ["ifeng.com"],
+ "image_url": "images/ifeng-com@2x.png",
+ "favicon_url": "favicons/ifeng-com.ico"
+ },
+ {
+ "domains": ["iqiyi.com"],
+ "image_url": "images/iqiyi-com@2x.png",
+ "favicon_url": "favicons/iqiyi-com.ico"
+ },
+ {
+ "domains": ["leboncoin.fr"],
+ "image_url": "images/leboncoin-fr@2x.png",
+ "favicon_url": "favicons/leboncoin-fr.png"
+ },
+ {
+ "domains": ["nike.com"],
+ "image_url": "images/nike@2x.jpg",
+ "favicon_url": "favicons/nike.ico"
+ },
+ {
+ "domains": ["ok.ru"],
+ "image_url": "images/ok-ru@2x.png",
+ "favicon_url": "favicons/ok-ru.ico"
+ },
+ {
+ "domains": ["olx.pl"],
+ "image_url": "images/olx-pl@2x.png",
+ "favicon_url": "favicons/olx-pl.ico"
+ },
+ {
+ "domains": ["reddit.com"],
+ "image_url": "images/reddit-com@2x.png",
+ "favicon_url": "favicons/reddit-com.png"
+ },
+ {
+ "domains": ["samsung.com"],
+ "image_url": "images/samsung@2x.jpg",
+ "favicon_url": "favicons/samsung.ico"
+ },
+ {
+ "domains": ["turbotax.intuit.com"],
+ "image_url": "images/turbotax@2x.jpg",
+ "favicon_url": "favicons/turbotax.png"
+ },
+ {
+ "domains": ["twitter.com"],
+ "image_url": "images/twitter-com@2x.png",
+ "favicon_url": "favicons/twitter-com.ico"
+ },
+ {
+ "domains": ["vk.com"],
+ "image_url": "images/vk-com@2x.png",
+ "favicon_url": "favicons/vk-com.ico"
+ },
+ {
+ "domains": ["vodafone.co.uk"],
+ "image_url": "images/vodafone@2x.jpg",
+ "favicon_url": "favicons/vodafone.png"
+ },
+ {
+ "domains": ["weibo.com"],
+ "image_url": "images/weibo-com@2x.png",
+ "favicon_url": "favicons/weibo-com.ico"
+ },
+ {
+ "domains": ["wikipedia.org"],
+ "image_url": "images/wikipedia-org@2x.png",
+ "favicon_url": "favicons/wikipedia-org.ico"
+ },
+ {
+ "domains": ["wix.com"],
+ "image_url": "images/wix@2x.jpg",
+ "favicon_url": "favicons/wix.ico"
+ },
+ {
+ "domains": ["wykop.pl"],
+ "image_url": "images/wykop-pl@2x.png",
+ "favicon_url": "favicons/wykop-pl.png"
+ },
+ {
+ "domains": ["yandex.com", "yandex.com.tr"],
+ "image_url": "images/yandex-com@2x.png",
+ "favicon_url": "favicons/yandex-com.png"
+ },
+ {
+ "domains": ["yandex.by", "yandex.kz", "yandex.ru", "yandex.ua", "yandex.uz"],
+ "image_url": "images/yandex-ru@2x.png",
+ "favicon_url": "favicons/yandex-ru.png"
+ },
+ {
+ "domains": ["youtube.com"],
+ "image_url": "images/youtube-com@2x.png",
+ "favicon_url": "favicons/youtube-com.png"
+ },
+ {
+ "domains": ["zhihu.com"],
+ "image_url": "images/zhihu-com@2x.png",
+ "favicon_url": "favicons/zhihu-com.ico"
+ }
+]