summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/data/content/activity-stream.bundle.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/data/content/activity-stream.bundle.js')
-rw-r--r--browser/components/newtab/data/content/activity-stream.bundle.js9558
1 files changed, 9558 insertions, 0 deletions
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