/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 12); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ var MAIN_MESSAGE_TYPE = "ActivityStream:Main"; var CONTENT_MESSAGE_TYPE = "ActivityStream:Content"; var UI_CODE = 1; var 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; // Export for tests // Create an object that avoids accidental differing key/value pairs: // { // INIT: "INIT", // UNINIT: "UNINIT" // } const actionTypes = {}; for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "DISABLE_ONBOARDING", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_DELETED", "PLACES_LINK_BLOCKED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_EDIT", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) { actionTypes[type] = type; } // Helper function for creating routed actions between content and main // Not intended to be used by consumers function _RouteMessage(action, options) { const meta = action.meta ? Object.assign({}, action.meta) : {}; if (!options || !options.from || !options.to) { 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", "skipOrigin"].forEach(o => { if (typeof options[o] !== "undefined") { meta[o] = options[o]; } else if (meta[o]) { delete meta[o]; } }); return Object.assign({}, action, { meta }); } /** * SendToMain - Creates a message that will be sent to the Main process. * * @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 SendToMain(action, fromTarget) { return _RouteMessage(action, { from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE, fromTarget }); } /** * BroadcastToContent - Creates a message that will be 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 }); } /** * SendToContent - Creates a message that will be sent to a particular Content process. * * @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 SendToContent(action, target) { if (!target) { throw new Error("You must provide a target ID as the second parameter of SendToContent. If you want to send to all content processes, use BroadcastToContent"); } return _RouteMessage(action, { from: MAIN_MESSAGE_TYPE, to: CONTENT_MESSAGE_TYPE, toTarget: target }); } /** * 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 SendToMain action */ function UserEvent(data) { return SendToMain({ type: actionTypes.TELEMETRY_USER_EVENT, data }); } /** * UndesiredEvent - A telemetry ping indicating an undesired state. * * @param {object} data Fields to include in the ping (value, etc.) * @param {int} importContext (For testing) Override the import context for testing. * @return {object} An action. For UI code, a SendToMain action. */ function UndesiredEvent(data, importContext = globalImportContext) { const action = { type: actionTypes.TELEMETRY_UNDESIRED_EVENT, data }; return importContext === UI_CODE ? SendToMain(action) : action; } /** * PerfEvent - A telemetry ping indicating a performance-related event. * * @param {object} data Fields to include in the ping (value, etc.) * @param {int} importContext (For testing) Override the import context for testing. * @return {object} An action. For UI code, a SendToMain action. */ function PerfEvent(data, importContext = globalImportContext) { const action = { type: actionTypes.TELEMETRY_PERFORMANCE_EVENT, data }; return importContext === UI_CODE ? SendToMain(action) : action; } /** * ImpressionStats - A telemetry ping indicating an impression stats. * * @param {object} data Fields to include in the ping * @param {int} importContext (For testing) Override the import context for testing. * #return {object} An action. For UI code, a SendToMain action. */ function ImpressionStats(data, importContext = globalImportContext) { const action = { type: actionTypes.TELEMETRY_IMPRESSION_STATS, data }; return importContext === UI_CODE ? SendToMain(action) : action; } function SetPref(name, value, importContext = globalImportContext) { const action = { type: actionTypes.SET_PREF, data: { name, value } }; return importContext === UI_CODE ? SendToMain(action) : action; } var actionCreators = { BroadcastToContent, UserEvent, UndesiredEvent, PerfEvent, ImpressionStats, SendToContent, SendToMain, SetPref }; // These are helpers to test for certain kinds of actions var 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; }, isSendToContent(action) { if (!action.meta) { return false; } if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) { return true; } return false; }, getPortIdOfSender(action) { return action.meta && action.meta.fromTarget || null; }, _RouteMessage }; module.exports = { actionTypes, actionCreators, actionUtils, globalImportContext, UI_CODE, BACKGROUND_PROCESS, MAIN_MESSAGE_TYPE, CONTENT_MESSAGE_TYPE }; /***/ }), /* 1 */ /***/ (function(module, exports) { module.exports = React; /***/ }), /* 2 */ /***/ (function(module, exports) { module.exports = ReactIntl; /***/ }), /* 3 */ /***/ (function(module, exports) { module.exports = ReactRedux; /***/ }), /* 4 */ /***/ (function(module, exports) { var g; // This works in non-strict mode g = (function() { return this; })(); try { // This works if eval is allowed (see CSP) g = g || Function("return this")() || (1,eval)("this"); } catch(e) { // This works if the window reference is available if(typeof window === "object") g = window; } // g can still be undefined, but nothing to do about it... // We return undefined, instead of nothing here, so it's // easier to handle this case. if(!global) { ...} module.exports = g; /***/ }), /* 5 */ /***/ (function(module, exports) { module.exports = { TOP_SITES_SOURCE: "TOP_SITES", TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"], // minimum size necessary to show a rich icon instead of a screenshot MIN_RICH_FAVICON_SIZE: 96, // minimum size necessary to show any icon in the top left corner with a screenshot MIN_CORNER_FAVICON_SIZE: 16 }; /***/ }), /* 6 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { actionTypes: at } = __webpack_require__(0); const { Dedupe } = __webpack_require__(20); // Locales that should be displayed RTL const RTL_LIST = ["ar", "he", "fa", "ur"]; const TOP_SITES_DEFAULT_LENGTH = 6; const TOP_SITES_SHOWMORE_LENGTH = 12; const dedupe = new Dedupe(site => site && site.url); const INITIAL_STATE = { App: { // Have we received real data from the app yet? initialized: false, // The locale of the browser locale: "", // Localized strings with defaults strings: null, // The text direction for the locale textDirection: "", // The version of the system-addon version: null }, Snippets: { initialized: false }, TopSites: { // Have we received real data from history yet? initialized: false, // The history (and possibly default) links rows: [], // Used in content only to dispatch action from // context menu to TopSitesEdit. editForm: { visible: false, site: null } }, Prefs: { initialized: false, values: {} }, Dialog: { visible: false, data: {} }, Sections: [], PreferencesPane: { visible: false } }; function App(prevState = INITIAL_STATE.App, action) { switch (action.type) { case at.INIT: return Object.assign({}, prevState, action.data || {}, { initialized: true }); case at.LOCALE_UPDATED: { if (!action.data) { return prevState; } let { locale, strings } = action.data; return Object.assign({}, prevState, { locale, strings, textDirection: RTL_LIST.indexOf(locale.split("-")[0]) >= 0 ? "rtl" : "ltr" }); } 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 at.TOP_SITES_UPDATED: if (!action.data) { return prevState; } return Object.assign({}, prevState, { initialized: true, rows: action.data }); case at.TOP_SITES_EDIT: return Object.assign({}, prevState, { editForm: { visible: true, site: action.data } }); case at.TOP_SITES_CANCEL_EDIT: return Object.assign({}, prevState, { editForm: { visible: false } }); case at.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 at.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 at.PLACES_BOOKMARK_REMOVED: if (!action.data) { return prevState; } newRows = prevState.rows.map(site => { if (site && site.url === action.data.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 at.BLOCK_URL: case at.DELETE_HISTORY_URL: // Optimistically update the UI by responding to the context menu action // events and removing the site that was blocked/deleted with an empty slot. // Once refresh() finishes, we update the UI again with a new site newRows = prevState.rows.filter(val => val && val.url !== action.data.url); return Object.assign({}, prevState, { rows: newRows }); default: return prevState; } } function Dialog(prevState = INITIAL_STATE.Dialog, action) { switch (action.type) { case at.DIALOG_OPEN: return Object.assign({}, prevState, { visible: true, data: action.data }); case at.DIALOG_CANCEL: return Object.assign({}, prevState, { visible: false }); case at.DELETE_HISTORY_URL: return Object.assign({}, INITIAL_STATE.Dialog); default: return prevState; } } function Prefs(prevState = INITIAL_STATE.Prefs, action) { let newValues; switch (action.type) { case at.PREFS_INITIAL_VALUES: return Object.assign({}, prevState, { initialized: true, values: action.data }); case at.PREF_CHANGED: newValues = Object.assign({}, prevState.values); newValues[action.data.name] = action.data.value; return Object.assign({}, prevState, { values: newValues }); default: return prevState; } } function Sections(prevState = INITIAL_STATE.Sections, action) { let hasMatch; let newState; switch (action.type) { case at.SECTION_DEREGISTER: return prevState.filter(section => section.id !== action.data); case at.SECTION_REGISTER: // If section exists in prevState, update it newState = prevState.map(section => { if (section && section.id === action.data.id) { hasMatch = true; return Object.assign({}, section, action.data); } return section; }); // Invariant: Sections array sorted in increasing order of property `order`. // If section doesn't exist in prevState, create a new section object. If // the section has an order, insert it at the correct place in the array. // Otherwise, prepend it and set the order to be minimal. if (!hasMatch) { const initialized = !!(action.data.rows && action.data.rows.length > 0); let order; let index; if (prevState.length > 0) { order = action.data.order !== undefined ? action.data.order : prevState[0].order - 1; index = newState.findIndex(section => section.order >= order); if (index === -1) { index = newState.length; } } else { order = action.data.order !== undefined ? action.data.order : 0; index = 0; } const section = Object.assign({ title: "", rows: [], order, enabled: false }, action.data, { initialized }); newState.splice(index, 0, section); } return newState; case at.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 } : {}; 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 at.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 at.PLACES_BOOKMARK_ADDED: if (!action.data) { return prevState; } return prevState.map(section => Object.assign({}, section, { rows: section.rows.map(item => { // find the item within the rows that is attempted to be bookmarked if (item.url === action.data.url) { const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; return Object.assign({}, item, { bookmarkGuid, bookmarkTitle, bookmarkDateCreated: dateAdded, type: "bookmark" }); } return item; }) })); case at.PLACES_BOOKMARK_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 (item.url === action.data.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 at.PLACES_LINKS_DELETED: return prevState.map(section => Object.assign({}, section, { rows: section.rows.filter(site => !action.data.includes(site.url)) })); case at.PLACES_LINK_BLOCKED: return prevState.map(section => Object.assign({}, section, { rows: section.rows.filter(site => site.url !== action.data.url) })); default: return prevState; } } function Snippets(prevState = INITIAL_STATE.Snippets, action) { switch (action.type) { case at.SNIPPETS_DATA: return Object.assign({}, prevState, { initialized: true }, action.data); case at.SNIPPETS_RESET: return INITIAL_STATE.Snippets; default: return prevState; } } function PreferencesPane(prevState = INITIAL_STATE.PreferencesPane, action) { switch (action.type) { case at.SETTINGS_OPEN: return Object.assign({}, prevState, { visible: true }); case at.SETTINGS_CLOSE: return Object.assign({}, prevState, { visible: false }); default: return prevState; } } var reducers = { TopSites, App, Snippets, Prefs, Dialog, Sections, PreferencesPane }; module.exports = { reducers, INITIAL_STATE, insertPinned, TOP_SITES_DEFAULT_LENGTH, TOP_SITES_SHOWMORE_LENGTH }; /***/ }), /* 7 */ /***/ (function(module, exports, __webpack_require__) { var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; const React = __webpack_require__(1); const { actionCreators: ac, actionTypes: at } = __webpack_require__(0); const LinkMenu = __webpack_require__(8); const { TOP_SITES_SOURCE, TOP_SITES_CONTEXT_MENU_OPTIONS, MIN_RICH_FAVICON_SIZE, MIN_CORNER_FAVICON_SIZE } = __webpack_require__(5); const TopSiteLink = props => { const { link } = props; const topSiteOuterClassName = `top-site-outer${props.className ? ` ${props.className}` : ""}`; const { tippyTopIcon, faviconSize } = link; let imageClassName; let imageStyle; let showSmallFavicon = false; let smallFaviconStyle; let smallFaviconFallback; if (tippyTopIcon || faviconSize >= MIN_RICH_FAVICON_SIZE) { // styles and class names for top sites with rich icons imageClassName = "top-site-icon rich-icon"; imageStyle = { backgroundColor: link.backgroundColor, backgroundImage: `url(${tippyTopIcon || link.favicon})` }; } else { // styles and class names for top sites with screenshot + small icon in top left corner imageClassName = `screenshot${link.screenshot ? " active" : ""}`; imageStyle = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" }; // only show a favicon in top left if it's greater than 16x16 if (faviconSize >= MIN_CORNER_FAVICON_SIZE) { showSmallFavicon = true; smallFaviconStyle = { backgroundImage: `url(${link.favicon})` }; } else if (link.screenshot) { // Don't show a small favicon if there is no screenshot, because that // would result in two fallback icons showSmallFavicon = true; smallFaviconFallback = true; } } return React.createElement( "li", { className: topSiteOuterClassName, key: link.guid || link.url }, React.createElement( "a", { href: link.url, onClick: props.onClick }, React.createElement( "div", { className: "tile", "aria-hidden": true }, React.createElement( "span", { className: "letter-fallback" }, props.title[0] ), React.createElement("div", { className: imageClassName, style: imageStyle }), showSmallFavicon && React.createElement( "div", { className: "top-site-icon default-icon", style: smallFaviconStyle }, smallFaviconFallback && props.title[0] ) ), React.createElement( "div", { className: `title ${link.isPinned ? "pinned" : ""}` }, link.isPinned && React.createElement("div", { className: "icon icon-pin-small" }), React.createElement( "span", { dir: "auto" }, props.title ) ) ), props.children ); }; TopSiteLink.defaultProps = { title: "", link: {} }; class TopSite extends React.PureComponent { constructor(props) { super(props); this.state = { showContextMenu: false, activeTile: null }; this.onLinkClick = this.onLinkClick.bind(this); this.onMenuButtonClick = this.onMenuButtonClick.bind(this); this.onMenuUpdate = this.onMenuUpdate.bind(this); this.onDismissButtonClick = this.onDismissButtonClick.bind(this); this.onPinButtonClick = this.onPinButtonClick.bind(this); this.onEditButtonClick = this.onEditButtonClick.bind(this); } toggleContextMenu(event, index) { this.setState({ activeTile: index, showContextMenu: true }); } userEvent(event) { this.props.dispatch(ac.UserEvent({ event, source: TOP_SITES_SOURCE, action_position: this.props.index })); } onLinkClick(ev) { if (this.props.editMode) { // Ignore clicks if we are in the edit modal. ev.preventDefault(); return; } this.userEvent("CLICK"); } onMenuButtonClick(event) { event.preventDefault(); this.toggleContextMenu(event, this.props.index); } onMenuUpdate(showContextMenu) { this.setState({ showContextMenu }); } onDismissButtonClick() { const { link } = this.props; if (link.isPinned) { this.props.dispatch(ac.SendToMain({ type: at.TOP_SITES_UNPIN, data: { site: { url: link.url } } })); } this.props.dispatch(ac.SendToMain({ type: at.BLOCK_URL, data: link.url })); this.userEvent("BLOCK"); } onPinButtonClick() { const { link, index } = this.props; if (link.isPinned) { this.props.dispatch(ac.SendToMain({ type: at.TOP_SITES_UNPIN, data: { site: { url: link.url } } })); this.userEvent("UNPIN"); } else { this.props.dispatch(ac.SendToMain({ type: at.TOP_SITES_PIN, data: { site: { url: link.url }, index } })); this.userEvent("PIN"); } } onEditButtonClick() { this.props.onEdit(this.props.index); } render() { const { props } = this; const { link } = props; const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === props.index; const title = link.label || link.hostname; return React.createElement( TopSiteLink, _extends({}, props, { onClick: this.onLinkClick, className: isContextMenuOpen ? "active" : "", title: title }), !props.editMode && React.createElement( "div", null, React.createElement( "button", { className: "context-menu-button icon", onClick: this.onMenuButtonClick }, React.createElement( "span", { className: "sr-only" }, `Open context menu for ${title}` ) ), React.createElement(LinkMenu, { dispatch: props.dispatch, index: props.index, onUpdate: this.onMenuUpdate, options: TOP_SITES_CONTEXT_MENU_OPTIONS, site: link, source: TOP_SITES_SOURCE, visible: isContextMenuOpen }) ), props.editMode && React.createElement( "div", { className: "edit-menu" }, React.createElement("button", { className: `icon icon-${link.isPinned ? "unpin" : "pin"}`, title: this.props.intl.formatMessage({ id: `edit_topsites_${link.isPinned ? "unpin" : "pin"}_button` }), onClick: this.onPinButtonClick }), React.createElement("button", { className: "icon icon-edit", title: this.props.intl.formatMessage({ id: "edit_topsites_edit_button" }), onClick: this.onEditButtonClick }), React.createElement("button", { className: "icon icon-dismiss", title: this.props.intl.formatMessage({ id: "edit_topsites_dismiss_button" }), onClick: this.onDismissButtonClick }) ) ); } } TopSite.defaultProps = { editMode: false, link: {}, onEdit() {} }; const TopSitePlaceholder = () => React.createElement(TopSiteLink, { className: "placeholder" }); module.exports.TopSite = TopSite; module.exports.TopSiteLink = TopSiteLink; module.exports.TopSitePlaceholder = TopSitePlaceholder; /***/ }), /* 8 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { injectIntl } = __webpack_require__(2); const ContextMenu = __webpack_require__(18); const { actionCreators: ac } = __webpack_require__(0); const linkMenuOptions = __webpack_require__(19); const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"]; class LinkMenu extends React.PureComponent { getOptions() { const props = this.props; const { site, index, source } = props; // Handle special case of default site const propOptions = !site.isDefault ? props.options : DEFAULT_SITE_MENU_OPTIONS; const options = propOptions.map(o => linkMenuOptions[o](site, index, source)).map(option => { const { action, impression, id, type, userEvent } = option; if (!type && id) { option.label = props.intl.formatMessage(option); option.onClick = () => { props.dispatch(action); if (userEvent) { props.dispatch(ac.UserEvent({ event: userEvent, source, action_position: index })); } 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 React.createElement(ContextMenu, { visible: this.props.visible, onUpdate: this.props.onUpdate, options: this.getOptions() }); } } module.exports = injectIntl(LinkMenu); module.exports._unconnected = LinkMenu; /***/ }), /* 9 */ /***/ (function(module, exports, __webpack_require__) { var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; const React = __webpack_require__(1); const { actionCreators: ac, actionTypes: at } = __webpack_require__(0); const { injectIntl, FormattedMessage } = __webpack_require__(2); function getFormattedMessage(message) { return typeof message === "string" ? React.createElement( "span", null, message ) : React.createElement(FormattedMessage, message); } class Info extends React.PureComponent { constructor(props) { super(props); this.onInfoEnter = this.onInfoEnter.bind(this); this.onInfoLeave = this.onInfoLeave.bind(this); this.onManageClick = this.onManageClick.bind(this); this.state = { infoActive: false }; } /** * Take a truthy value to conditionally change the infoActive state. */ _setInfoState(nextActive) { const infoActive = !!nextActive; if (infoActive !== this.state.infoActive) { this.setState({ infoActive }); } } onInfoEnter() { // We're getting focus or hover, so info state should be true if not yet. this._setInfoState(true); } onInfoLeave(event) { // We currently have an active (true) info state, so keep it true only if we // have a related event target that is contained "within" the current target // (section-info-option) as itself or a descendant. Set to false otherwise. this._setInfoState(event && event.relatedTarget && (event.relatedTarget === event.currentTarget || event.relatedTarget.compareDocumentPosition(event.currentTarget) & Node.DOCUMENT_POSITION_CONTAINS)); } onManageClick() { this.props.dispatch({ type: at.SETTINGS_OPEN }); this.props.dispatch(ac.UserEvent({ event: "OPEN_NEWTAB_PREFS" })); } render() { const { infoOption, intl } = this.props; const infoOptionIconA11yAttrs = { "aria-haspopup": "true", "aria-controls": "info-option", "aria-expanded": this.state.infoActive ? "true" : "false", "role": "note", "tabIndex": 0 }; const sectionInfoTitle = intl.formatMessage({ id: "section_info_option" }); return React.createElement( "span", { className: "section-info-option", onBlur: this.onInfoLeave, onFocus: this.onInfoEnter, onMouseOut: this.onInfoLeave, onMouseOver: this.onInfoEnter }, React.createElement("img", _extends({ className: "info-option-icon", title: sectionInfoTitle }, infoOptionIconA11yAttrs)), React.createElement( "div", { className: "info-option" }, infoOption.header && React.createElement( "div", { className: "info-option-header", role: "heading" }, getFormattedMessage(infoOption.header) ), React.createElement( "p", { className: "info-option-body" }, infoOption.body && getFormattedMessage(infoOption.body), infoOption.link && React.createElement( "a", { href: infoOption.link.href, target: "_blank", rel: "noopener noreferrer", className: "info-option-link" }, getFormattedMessage(infoOption.link.title || infoOption.link) ) ), React.createElement( "div", { className: "info-option-manage" }, React.createElement( "button", { onClick: this.onManageClick }, React.createElement(FormattedMessage, { id: "settings_pane_header" }) ) ) ) ); } } const InfoIntl = injectIntl(Info); class CollapsibleSection extends React.PureComponent { constructor(props) { super(props); this.onInfoEnter = this.onInfoEnter.bind(this); this.onInfoLeave = this.onInfoLeave.bind(this); this.onHeaderClick = this.onHeaderClick.bind(this); this.onTransitionEnd = this.onTransitionEnd.bind(this); this.state = { enableAnimation: false, isAnimating: false, infoActive: false }; } componentDidUpdate(prevProps, prevState) { // Enable animations once we get prefs loaded in to avoid animations running during loading. if (prevProps.Prefs.values[this.props.prefName] === undefined && this.props.Prefs.values[this.props.prefName] !== undefined) { setTimeout(() => this.setState({ enableAnimation: true }), 0); } } _setInfoState(nextActive) { // Take a truthy value to conditionally change the infoActive state. const infoActive = !!nextActive; if (infoActive !== this.state.infoActive) { this.setState({ infoActive }); } } onInfoEnter() { // We're getting focus or hover, so info state should be true if not yet. this._setInfoState(true); } onInfoLeave(event) { // We currently have an active (true) info state, so keep it true only if we // have a related event target that is contained "within" the current target // (section-info-option) as itself or a descendant. Set to false otherwise. this._setInfoState(event && event.relatedTarget && (event.relatedTarget === event.currentTarget || event.relatedTarget.compareDocumentPosition(event.currentTarget) & Node.DOCUMENT_POSITION_CONTAINS)); } onHeaderClick() { this.setState({ isAnimating: true }); this.props.dispatch(ac.SetPref(this.props.prefName, !this.props.Prefs.values[this.props.prefName])); } onTransitionEnd() { this.setState({ isAnimating: false }); } renderIcon() { const icon = this.props.icon; if (icon && icon.startsWith("moz-extension://")) { return React.createElement("span", { className: "icon icon-small-spacer", style: { "background-image": `url('${icon}')` } }); } return React.createElement("span", { className: `icon icon-small-spacer icon-${icon || "webextension"}` }); } render() { const isCollapsed = this.props.Prefs.values[this.props.prefName]; const { enableAnimation, isAnimating } = this.state; const infoOption = this.props.infoOption; return React.createElement( "section", { className: `collapsible-section ${this.props.className}${isCollapsed ? " collapsed" : ""}` }, React.createElement( "div", { className: "section-top-bar" }, React.createElement( "h3", { className: "section-title" }, React.createElement( "span", { className: "click-target", onClick: this.onHeaderClick }, this.renderIcon(), this.props.title, React.createElement("span", { className: `icon ${isCollapsed ? "icon-arrowhead-forward" : "icon-arrowhead-down"}` }) ) ), infoOption && React.createElement(InfoIntl, { infoOption: infoOption, dispatch: this.props.dispatch }) ), React.createElement( "div", { className: `section-body${enableAnimation ? " animation-enabled" : ""}${isAnimating ? " animating" : ""}`, onTransitionEnd: this.onTransitionEnd }, this.props.children ) ); } } CollapsibleSection.defaultProps = { Prefs: { values: {} } }; module.exports = injectIntl(CollapsibleSection); module.exports._unconnected = CollapsibleSection; module.exports.Info = Info; module.exports.InfoIntl = InfoIntl; /***/ }), /* 10 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { actionCreators: ac, actionTypes: at } = __webpack_require__(0); const { perfService: perfSvc } = __webpack_require__(11); // 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 React.Component { constructor(props) { super(props); // Just for test dependency injection: this.perfSvc = this.props.perfSvc || perfSvc; 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/3688 // 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 `TELEMETRY_UNDESIRED_EVENT` 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(ac.SendToMain({ type: at.TELEMETRY_UNDESIRED_EVENT, data: { source: this.props.id.toUpperCase(), // highlights_data_late_by_ms, topsites_data_late_by_ms. event: `${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(ac.SendToMain({ type: at.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; } } module.exports = ComponentPerfTimer; /***/ }), /* 11 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* globals Services */ /* istanbul ignore if */ // Note: normally we would just feature detect Components.utils here, but // unfortunately that throws an ugly warning in content if we do. if (typeof Window === "undefined" && typeof Components !== "undefined" && Components.utils) { Components.utils.import("resource://gre/modules/Services.jsm"); } let usablePerfObj; /* istanbul ignore if */ /* istanbul ignore else */ if (typeof Services !== "undefined") { // Borrow the high-resolution timer from the hidden window.... usablePerfObj = Services.appShell.hiddenDOMWindow.performance; } else if (typeof performance !== "undefined") { // we must be running in content space usablePerfObj = performance; } else { // This is a dummy object so this file doesn't crash in the node prerendering // task. usablePerfObj = { now() {}, mark() {} }; } var _PerfService = function _PerfService(options) { // For testing, so that we can use a fake Window.performance object with // known state. if (options && options.performanceObj) { this._perf = options.performanceObj; } else { 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; } }; var perfService = new _PerfService(); module.exports = { _PerfService, perfService }; /***/ }), /* 12 */ /***/ (function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(global) {const React = __webpack_require__(1); const ReactDOM = __webpack_require__(13); const Base = __webpack_require__(14); const { Provider } = __webpack_require__(3); const initStore = __webpack_require__(31); const { reducers } = __webpack_require__(6); const DetectUserSessionStart = __webpack_require__(33); const { addSnippetsSubscriber } = __webpack_require__(34); const { actionTypes: at, actionCreators: ac } = __webpack_require__(0); new DetectUserSessionStart().sendEventOrAddListener(); const store = initStore(reducers, global.gActivityStreamPrerenderedState); // If we are starting in a prerendered state, we must wait until the first render // to request state rehydration (see Base.jsx). If we are NOT in a prerendered state, // we can request it immedately. if (!global.gActivityStreamPrerenderedState) { store.dispatch(ac.SendToMain({ type: at.NEW_TAB_STATE_REQUEST })); } ReactDOM.render(React.createElement( Provider, { store: store }, React.createElement(Base, { isPrerendered: !!global.gActivityStreamPrerenderedState }) ), document.getElementById("root")); addSnippetsSubscriber(store); /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) /***/ }), /* 13 */ /***/ (function(module, exports) { module.exports = ReactDOM; /***/ }), /* 14 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { connect } = __webpack_require__(3); const { addLocaleData, IntlProvider } = __webpack_require__(2); const TopSites = __webpack_require__(15); const Search = __webpack_require__(21); const ConfirmDialog = __webpack_require__(23); const ManualMigration = __webpack_require__(24); const PreferencesPane = __webpack_require__(25); const Sections = __webpack_require__(26); const { actionTypes: at, actionCreators: ac } = __webpack_require__(0); const { PrerenderData } = __webpack_require__(30); // Add the locale data for pluralization and relative-time formatting for now, // this just uses english locale data. We can make this more sophisticated if // more features are needed. function addLocaleDataForReactIntl({ locale, textDirection }) { addLocaleData([{ locale, parentLocale: "en" }]); document.documentElement.lang = locale; document.documentElement.dir = textDirection; } class Base extends React.PureComponent { componentWillMount() { this.sendNewTabRehydrated(this.props.App); } componentDidMount() { // Request state AFTER the first render to ensure we don't cause the // prerendered DOM to be unmounted. Otherwise, NEW_TAB_STATE_REQUEST is // dispatched right after the store is ready. if (this.props.isPrerendered) { this.props.dispatch(ac.SendToMain({ type: at.NEW_TAB_STATE_REQUEST })); } // Also wait for the preloaded page to show, so the tab's title and favicon updates addEventListener("visibilitychange", () => { this.updateTitle(this.props.App); document.getElementById("favicon").href += "#"; }, { once: true }); } componentWillUpdate({ App }) { this.sendNewTabRehydrated(App); // Early loads might not have locale yet, so wait until we do if (App.locale && App.locale !== this.props.App.locale) { addLocaleDataForReactIntl(App); this.updateTitle(App); } } updateTitle({ strings }) { if (strings) { document.title = strings.newtab_page_title; } } // 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(App) { if (App && App.initialized && !this.renderNotified) { this.props.dispatch(ac.SendToMain({ type: at.NEW_TAB_REHYDRATED, data: {} })); this.renderNotified = true; } } render() { const props = this.props; const { locale, strings, initialized } = props.App; const prefs = props.Prefs.values; const shouldBeFixedToTop = PrerenderData.arePrefsValid(name => prefs[name]); const outerClassName = `outer-wrapper${shouldBeFixedToTop ? " fixed-to-top" : ""}`; if (!props.isPrerendered && !initialized) { return null; } // Note: the key on IntlProvider must be static in order to not blow away // all elements on a locale change (such as after preloading). // See https://github.com/yahoo/react-intl/issues/695 for more info. return React.createElement( IntlProvider, { key: "STATIC", locale: locale, messages: strings }, React.createElement( "div", { className: outerClassName }, React.createElement( "main", null, prefs.showSearch && React.createElement(Search, null), React.createElement( "div", { className: `body-wrapper${initialized ? " on" : ""}` }, !prefs.migrationExpired && React.createElement(ManualMigration, null), prefs.showTopSites && React.createElement(TopSites, null), React.createElement(Sections, null) ), React.createElement(ConfirmDialog, null) ), initialized && React.createElement(PreferencesPane, null) ) ); } } module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base); module.exports._unconnected = Base; /***/ }), /* 15 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { connect } = __webpack_require__(3); const { FormattedMessage } = __webpack_require__(2); const TopSitesEdit = __webpack_require__(16); const { TopSite, TopSitePlaceholder } = __webpack_require__(7); const CollapsibleSection = __webpack_require__(9); const ComponentPerfTimer = __webpack_require__(10); const TopSites = props => { const realTopSites = props.TopSites.rows.slice(0, props.TopSitesCount); const placeholderCount = props.TopSitesCount - realTopSites.length; const infoOption = { header: { id: "settings_pane_topsites_header" }, body: { id: "settings_pane_topsites_body" } }; return React.createElement( ComponentPerfTimer, { id: "topsites", initialized: props.TopSites.initialized, dispatch: props.dispatch }, React.createElement( CollapsibleSection, { className: "top-sites", icon: "topsites", title: React.createElement(FormattedMessage, { id: "header_top_sites" }), infoOption: infoOption, prefName: "collapseTopSites", Prefs: props.Prefs, dispatch: props.dispatch }, React.createElement( "ul", { className: "top-sites-list" }, realTopSites.map((link, index) => link && React.createElement(TopSite, { key: link.guid || link.url, dispatch: props.dispatch, link: link, index: index, intl: props.intl })), placeholderCount > 0 && [...Array(placeholderCount)].map((_, i) => React.createElement(TopSitePlaceholder, { key: i })) ), React.createElement(TopSitesEdit, props) ) ); }; module.exports = connect(state => ({ TopSites: state.TopSites, Prefs: state.Prefs, TopSitesCount: state.Prefs.values.topSitesCount }))(TopSites); module.exports._unconnected = TopSites; /***/ }), /* 16 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { FormattedMessage, injectIntl } = __webpack_require__(2); const { actionCreators: ac, actionTypes: at } = __webpack_require__(0); const TopSiteForm = __webpack_require__(17); const { TopSite, TopSitePlaceholder } = __webpack_require__(7); const { TOP_SITES_DEFAULT_LENGTH, TOP_SITES_SHOWMORE_LENGTH } = __webpack_require__(6); const { TOP_SITES_SOURCE } = __webpack_require__(5); class TopSitesEdit extends React.PureComponent { constructor(props) { super(props); this.state = { showEditModal: false, showAddForm: false, showEditForm: false, editIndex: -1 // Index of top site being edited }; this.onEditButtonClick = this.onEditButtonClick.bind(this); this.onShowMoreLessClick = this.onShowMoreLessClick.bind(this); this.onModalOverlayClick = this.onModalOverlayClick.bind(this); this.onAddButtonClick = this.onAddButtonClick.bind(this); this.onFormClose = this.onFormClose.bind(this); this.onEdit = this.onEdit.bind(this); } onEditButtonClick() { this.setState({ showEditModal: !this.state.showEditModal }); const event = this.state.showEditModal ? "TOP_SITES_EDIT_OPEN" : "TOP_SITES_EDIT_CLOSE"; this.props.dispatch(ac.UserEvent({ source: TOP_SITES_SOURCE, event })); } onModalOverlayClick() { this.setState({ showEditModal: false, showAddForm: false, showEditForm: false }); this.props.dispatch(ac.UserEvent({ source: TOP_SITES_SOURCE, event: "TOP_SITES_EDIT_CLOSE" })); this.props.dispatch({ type: at.TOP_SITES_CANCEL_EDIT }); } onShowMoreLessClick() { const prefIsSetToDefault = this.props.TopSitesCount === TOP_SITES_DEFAULT_LENGTH; this.props.dispatch(ac.SendToMain({ type: at.SET_PREF, data: { name: "topSitesCount", value: prefIsSetToDefault ? TOP_SITES_SHOWMORE_LENGTH : TOP_SITES_DEFAULT_LENGTH } })); this.props.dispatch(ac.UserEvent({ source: TOP_SITES_SOURCE, event: prefIsSetToDefault ? "TOP_SITES_EDIT_SHOW_MORE" : "TOP_SITES_EDIT_SHOW_LESS" })); } onAddButtonClick() { this.setState({ showAddForm: true }); this.props.dispatch(ac.UserEvent({ source: TOP_SITES_SOURCE, event: "TOP_SITES_ADD_FORM_OPEN" })); } onFormClose() { this.setState({ showAddForm: false, showEditForm: false }); this.props.dispatch({ type: at.TOP_SITES_CANCEL_EDIT }); } onEdit(index) { this.setState({ showEditForm: true, editIndex: index }); this.props.dispatch(ac.UserEvent({ source: TOP_SITES_SOURCE, event: "TOP_SITES_EDIT_FORM_OPEN" })); } render() { const realTopSites = this.props.TopSites.rows.slice(0, this.props.TopSitesCount); const placeholderCount = this.props.TopSitesCount - realTopSites.length; const showEditForm = this.props.TopSites.editForm && this.props.TopSites.editForm.visible || this.state.showEditModal && this.state.showEditForm; let editIndex = this.state.editIndex; if (showEditForm && this.props.TopSites.editForm.visible) { const targetURL = this.props.TopSites.editForm.site.url; editIndex = this.props.TopSites.rows.findIndex(s => s.url === targetURL); } return React.createElement( "div", { className: "edit-topsites-wrapper" }, React.createElement( "div", { className: "edit-topsites-button" }, React.createElement( "button", { className: "edit", title: this.props.intl.formatMessage({ id: "edit_topsites_button_label" }), onClick: this.onEditButtonClick }, React.createElement(FormattedMessage, { id: "edit_topsites_button_text" }) ) ), this.state.showEditModal && !this.state.showAddForm && !this.state.showEditForm && React.createElement( "div", { className: "edit-topsites" }, React.createElement("div", { className: "modal-overlay", onClick: this.onModalOverlayClick }), React.createElement( "div", { className: "modal" }, React.createElement( "section", { className: "edit-topsites-inner-wrapper" }, React.createElement( "div", { className: "section-top-bar" }, React.createElement( "h3", { className: "section-title" }, React.createElement("span", { className: `icon icon-small-spacer icon-topsites` }), React.createElement(FormattedMessage, { id: "header_top_sites" }) ) ), React.createElement( "ul", { className: "top-sites-list" }, realTopSites.map((link, index) => link && React.createElement(TopSite, { key: link.guid || link.url, dispatch: this.props.dispatch, link: link, index: index, intl: this.props.intl, onEdit: this.onEdit, editMode: true })), placeholderCount > 0 && [...Array(placeholderCount)].map((_, i) => React.createElement(TopSitePlaceholder, { key: i })) ) ), React.createElement( "section", { className: "actions" }, React.createElement( "button", { className: "add", onClick: this.onAddButtonClick }, React.createElement(FormattedMessage, { id: "edit_topsites_add_button" }) ), React.createElement( "button", { className: `icon icon-topsites show-${this.props.TopSitesCount === TOP_SITES_DEFAULT_LENGTH ? "more" : "less"}`, onClick: this.onShowMoreLessClick }, React.createElement(FormattedMessage, { id: `edit_topsites_show${this.props.TopSitesCount === TOP_SITES_DEFAULT_LENGTH ? "more" : "less"}_button` }) ), React.createElement( "button", { className: "done", onClick: this.onEditButtonClick }, React.createElement(FormattedMessage, { id: "edit_topsites_done_button" }) ) ) ) ), this.state.showEditModal && this.state.showAddForm && React.createElement( "div", { className: "edit-topsites" }, React.createElement("div", { className: "modal-overlay", onClick: this.onModalOverlayClick }), React.createElement( "div", { className: "modal" }, React.createElement(TopSiteForm, { onClose: this.onFormClose, dispatch: this.props.dispatch, intl: this.props.intl }) ) ), showEditForm && React.createElement( "div", { className: "edit-topsites" }, React.createElement("div", { className: "modal-overlay", onClick: this.onModalOverlayClick }), React.createElement( "div", { className: "modal" }, React.createElement(TopSiteForm, { label: this.props.TopSites.rows[editIndex].label || this.props.TopSites.rows[editIndex].hostname, url: this.props.TopSites.rows[editIndex].url, index: editIndex, editMode: true, onClose: this.onFormClose, dispatch: this.props.dispatch, intl: this.props.intl }) ) ) ); } } module.exports = injectIntl(TopSitesEdit); module.exports._unconnected = TopSitesEdit; /***/ }), /* 17 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { actionCreators: ac, actionTypes: at } = __webpack_require__(0); const { FormattedMessage } = __webpack_require__(2); const { TOP_SITES_SOURCE } = __webpack_require__(5); class TopSiteForm extends React.PureComponent { constructor(props) { super(props); this.state = { label: props.label || "", url: props.url || "", validationError: false }; this.onLabelChange = this.onLabelChange.bind(this); this.onUrlChange = this.onUrlChange.bind(this); this.onCancelButtonClick = this.onCancelButtonClick.bind(this); this.onAddButtonClick = this.onAddButtonClick.bind(this); this.onSaveButtonClick = this.onSaveButtonClick.bind(this); this.onUrlInputMount = this.onUrlInputMount.bind(this); } onLabelChange(event) { this.resetValidation(); this.setState({ "label": event.target.value }); } onUrlChange(event) { this.resetValidation(); this.setState({ "url": event.target.value }); } onCancelButtonClick(ev) { ev.preventDefault(); this.props.onClose(); } onAddButtonClick(ev) { ev.preventDefault(); if (this.validateForm()) { let site = { url: this.cleanUrl() }; if (this.state.label !== "") { site.label = this.state.label; } this.props.dispatch(ac.SendToMain({ type: at.TOP_SITES_ADD, data: { site } })); this.props.dispatch(ac.UserEvent({ source: TOP_SITES_SOURCE, event: "TOP_SITES_ADD" })); this.props.onClose(); } } onSaveButtonClick(ev) { ev.preventDefault(); if (this.validateForm()) { let site = { url: this.cleanUrl() }; if (this.state.label !== "") { site.label = this.state.label; } this.props.dispatch(ac.SendToMain({ type: at.TOP_SITES_PIN, data: { site, index: this.props.index } })); this.props.dispatch(ac.UserEvent({ source: TOP_SITES_SOURCE, event: "TOP_SITES_EDIT", action_position: this.props.index })); this.props.onClose(); } } cleanUrl() { let url = this.state.url; // If we are missing a protocol, prepend http:// if (!url.startsWith("http:") && !url.startsWith("https:")) { url = `http://${url}`; } return url; } resetValidation() { if (this.state.validationError) { this.setState({ validationError: false }); } } validateUrl() { try { return !!new URL(this.cleanUrl()); } catch (e) { return false; } } validateForm() { this.resetValidation(); // Only the URL is required and must be valid. if (!this.state.url || !this.validateUrl()) { this.setState({ validationError: true }); this.inputUrl.focus(); return false; } return true; } onUrlInputMount(input) { this.inputUrl = input; } render() { return React.createElement( "form", { className: "topsite-form" }, React.createElement( "section", { className: "edit-topsites-inner-wrapper" }, React.createElement( "div", { className: "form-wrapper" }, React.createElement( "h3", { className: "section-title" }, React.createElement(FormattedMessage, { id: this.props.editMode ? "topsites_form_edit_header" : "topsites_form_add_header" }) ), React.createElement( "div", { className: "field title" }, React.createElement("input", { type: "text", value: this.state.label, onChange: this.onLabelChange, placeholder: this.props.intl.formatMessage({ id: "topsites_form_title_placeholder" }) }) ), React.createElement( "div", { className: `field url${this.state.validationError ? " invalid" : ""}` }, React.createElement("input", { type: "text", ref: this.onUrlInputMount, value: this.state.url, onChange: this.onUrlChange, placeholder: this.props.intl.formatMessage({ id: "topsites_form_url_placeholder" }) }), this.state.validationError && React.createElement( "aside", { className: "error-tooltip" }, React.createElement(FormattedMessage, { id: "topsites_form_url_validation" }) ) ) ) ), React.createElement( "section", { className: "actions" }, React.createElement( "button", { className: "cancel", type: "button", onClick: this.onCancelButtonClick }, React.createElement(FormattedMessage, { id: "topsites_form_cancel_button" }) ), this.props.editMode && React.createElement( "button", { className: "done save", type: "submit", onClick: this.onSaveButtonClick }, React.createElement(FormattedMessage, { id: "topsites_form_save_button" }) ), !this.props.editMode && React.createElement( "button", { className: "done add", type: "submit", onClick: this.onAddButtonClick }, React.createElement(FormattedMessage, { id: "topsites_form_add_button" }) ) ) ); } } TopSiteForm.defaultProps = { label: "", url: "", index: 0, editMode: false // by default we are in "Add New Top Site" mode }; module.exports = TopSiteForm; /***/ }), /* 18 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); class ContextMenu extends React.PureComponent { constructor(props) { super(props); this.hideContext = this.hideContext.bind(this); } hideContext() { this.props.onUpdate(false); } componentWillMount() { this.hideContext(); } componentDidUpdate(prevProps) { if (this.props.visible && !prevProps.visible) { setTimeout(() => { window.addEventListener("click", this.hideContext); }, 0); } if (!this.props.visible && prevProps.visible) { window.removeEventListener("click", this.hideContext); } } componentWillUnmount() { window.removeEventListener("click", this.hideContext); } render() { return React.createElement( "span", { hidden: !this.props.visible, className: "context-menu" }, React.createElement( "ul", { role: "menu", className: "context-menu-list" }, this.props.options.map((option, i) => option.type === "separator" ? React.createElement("li", { key: i, className: "separator" }) : React.createElement(ContextMenuItem, { key: i, option: option, hideContext: this.hideContext })) ) ); } } class ContextMenuItem extends React.PureComponent { constructor(props) { super(props); this.onClick = this.onClick.bind(this); this.onKeyDown = this.onKeyDown.bind(this); } onClick() { this.props.hideContext(); this.props.option.onClick(); } onKeyDown(event) { const { option } = this.props; switch (event.key) { case "Tab": // tab goes down in context menu, shift + tab goes up in context menu // if we're on the last item, one more tab will close the context menu // similarly, if we're on the first item, one more shift + tab will close it if (event.shiftKey && option.first || !event.shiftKey && option.last) { this.props.hideContext(); } break; case "Enter": this.props.hideContext(); option.onClick(); break; } } render() { const { option } = this.props; return React.createElement( "li", { role: "menuitem", className: "context-menu-item" }, React.createElement( "a", { onClick: this.onClick, onKeyDown: this.onKeyDown, tabIndex: "0" }, option.icon && React.createElement("span", { className: `icon icon-spacer icon-${option.icon}` }), option.label ) ); } } module.exports = ContextMenu; module.exports.ContextMenu = ContextMenu; module.exports.ContextMenuItem = ContextMenuItem; /***/ }), /* 19 */ /***/ (function(module, exports, __webpack_require__) { const { actionTypes: at, actionCreators: ac } = __webpack_require__(0); /** * List of functions that return items that can be included as menu options in a * LinkMenu. All functions take the site as the first parameter, and optionally * the index of the site. */ module.exports = { Separator: () => ({ type: "separator" }), RemoveBookmark: site => ({ id: "menu_action_remove_bookmark", icon: "bookmark-added", action: ac.SendToMain({ type: at.DELETE_BOOKMARK_BY_ID, data: site.bookmarkGuid }), userEvent: "BOOKMARK_DELETE" }), AddBookmark: site => ({ id: "menu_action_bookmark", icon: "bookmark-hollow", action: ac.SendToMain({ type: at.BOOKMARK_URL, data: { url: site.url, title: site.title } }), userEvent: "BOOKMARK_ADD" }), OpenInNewWindow: site => ({ id: "menu_action_open_new_window", icon: "new-window", action: ac.SendToMain({ type: at.OPEN_NEW_WINDOW, data: { url: site.url, referrer: site.referrer } }), userEvent: "OPEN_NEW_WINDOW" }), OpenInPrivateWindow: site => ({ id: "menu_action_open_private_window", icon: "new-window-private", action: ac.SendToMain({ type: at.OPEN_PRIVATE_WINDOW, data: { url: site.url, referrer: site.referrer } }), userEvent: "OPEN_PRIVATE_WINDOW" }), BlockUrl: (site, index, eventSource) => ({ id: "menu_action_dismiss", icon: "dismiss", action: ac.SendToMain({ type: at.BLOCK_URL, data: site.url }), impression: ac.ImpressionStats({ source: eventSource, block: 0, incognito: true, tiles: [{ id: site.guid, pos: index }] }), userEvent: "BLOCK" }), DeleteUrl: site => ({ id: "menu_action_delete", icon: "delete", action: { type: at.DIALOG_OPEN, data: { onConfirm: [ac.SendToMain({ type: at.DELETE_HISTORY_URL, data: { url: site.url, forceBlock: site.bookmarkGuid } }), ac.UserEvent({ event: "DELETE" })], body_string_id: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"], confirm_button_string_id: "menu_action_delete" } }, userEvent: "DIALOG_OPEN" }), PinTopSite: (site, index) => ({ id: "menu_action_pin", icon: "pin", action: ac.SendToMain({ type: at.TOP_SITES_PIN, data: { site: { url: site.url }, index } }), userEvent: "PIN" }), UnpinTopSite: site => ({ id: "menu_action_unpin", icon: "unpin", action: ac.SendToMain({ type: at.TOP_SITES_UNPIN, data: { site: { url: site.url } } }), userEvent: "UNPIN" }), SaveToPocket: (site, index, eventSource) => ({ id: "menu_action_save_to_pocket", icon: "pocket", action: ac.SendToMain({ type: at.SAVE_TO_POCKET, data: { site: { url: site.url, title: site.title } } }), impression: ac.ImpressionStats({ source: eventSource, pocket: 0, incognito: true, tiles: [{ id: site.guid, pos: index }] }), userEvent: "SAVE_TO_POCKET" }), EditTopSite: site => ({ id: "edit_topsites_button_text", icon: "edit", action: { type: at.TOP_SITES_EDIT, data: { url: site.url, label: site.label } } }) }; module.exports.CheckBookmark = site => site.bookmarkGuid ? module.exports.RemoveBookmark(site) : module.exports.AddBookmark(site); module.exports.CheckPinTopSite = (site, index) => site.isPinned ? module.exports.UnpinTopSite(site) : module.exports.PinTopSite(site, index); /***/ }), /* 20 */ /***/ (function(module, exports) { var Dedupe = 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())); } }; module.exports = { Dedupe }; /***/ }), /* 21 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* globals ContentSearchUIController */ const React = __webpack_require__(1); const { connect } = __webpack_require__(3); const { FormattedMessage, injectIntl } = __webpack_require__(2); const { actionCreators: ac } = __webpack_require__(0); const { IS_NEWTAB } = __webpack_require__(22); class Search extends React.PureComponent { constructor(props) { super(props); this.onClick = this.onClick.bind(this); this.onInputMount = this.onInputMount.bind(this); } handleEvent(event) { // Also track search events with our own telemetry if (event.detail.type === "Search") { this.props.dispatch(ac.UserEvent({ event: "SEARCH" })); } } onClick(event) { window.gContentSearchController.search(event); } componentWillUnmount() { delete window.gContentSearchController; } onInputMount(input) { if (input) { // The "healthReportKey" and needs to be "newtab" or "abouthome" so that // BrowserUsageTelemetry.jsm 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); // Focus the search box if we are on about:home if (!IS_NEWTAB) { input.focus(); } } else { window.gContentSearchController = null; removeEventListener("ContentSearchClient", this); } } /* * 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() { return React.createElement( "div", { className: "search-wrapper" }, React.createElement( "label", { htmlFor: "newtab-search-text", className: "search-label" }, React.createElement( "span", { className: "sr-only" }, React.createElement(FormattedMessage, { id: "search_web_placeholder" }) ) ), React.createElement("input", { id: "newtab-search-text", maxLength: "256", placeholder: this.props.intl.formatMessage({ id: "search_web_placeholder" }), ref: this.onInputMount, title: this.props.intl.formatMessage({ id: "search_web_placeholder" }), type: "search" }), React.createElement( "button", { id: "searchSubmit", className: "search-button", onClick: this.onClick, title: this.props.intl.formatMessage({ id: "search_button" }) }, React.createElement( "span", { className: "sr-only" }, React.createElement(FormattedMessage, { id: "search_button" }) ) ) ); } } // initialized is passed to props so that Search will rerender when it receives strings module.exports = connect(state => ({ locale: state.App.locale }))(injectIntl(Search)); module.exports._unconnected = Search; /***/ }), /* 22 */ /***/ (function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(global) {module.exports = { // constant to know if the page is about:newtab or about:home IS_NEWTAB: global.document && global.document.documentURI === "about:newtab" }; /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) /***/ }), /* 23 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { connect } = __webpack_require__(3); const { FormattedMessage } = __webpack_require__(2); const { actionTypes, actionCreators: ac } = __webpack_require__(0); /** * ConfirmDialog component. * One primary action button, one cancel button. * * Content displayed is controlled by `data` prop the component receives. * Example: * data: { * // Any sort of data needed to be passed around by actions. * payload: site.url, * // Primary button SendToMain 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" * }, */ const ConfirmDialog = React.createClass({ displayName: "ConfirmDialog", getDefaultProps() { return { visible: false, data: {} }; }, _handleCancelBtn() { this.props.dispatch({ type: actionTypes.DIALOG_CANCEL }); this.props.dispatch(ac.UserEvent({ event: actionTypes.DIALOG_CANCEL })); }, _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 React.createElement( "span", null, message_body.map(msg => React.createElement( "p", { key: msg }, React.createElement(FormattedMessage, { id: msg }) )) ); }, render() { if (!this.props.visible) { return null; } return React.createElement( "div", { className: "confirmation-dialog" }, React.createElement("div", { className: "modal-overlay", onClick: this._handleCancelBtn }), React.createElement( "div", { className: "modal" }, React.createElement( "section", { className: "modal-message" }, this._renderModalMessage() ), React.createElement( "section", { className: "actions" }, React.createElement( "button", { onClick: this._handleCancelBtn }, React.createElement(FormattedMessage, { id: "topsites_form_cancel_button" }) ), React.createElement( "button", { className: "done", onClick: this._handleConfirmBtn }, React.createElement(FormattedMessage, { id: this.props.data.confirm_button_string_id }) ) ) ) ); } }); module.exports = connect(state => state.Dialog)(ConfirmDialog); module.exports._unconnected = ConfirmDialog; module.exports.Dialog = ConfirmDialog; /***/ }), /* 24 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { connect } = __webpack_require__(3); const { FormattedMessage } = __webpack_require__(2); const { actionTypes: at, actionCreators: ac } = __webpack_require__(0); /** * Manual migration component used to start the profile import wizard. * Message is presented temporarily and will go away if: * 1. User clicks "No Thanks" * 2. User completed the data import * 3. After 3 active days * 4. User clicks "Cancel" on the import wizard (currently not implemented). */ class ManualMigration extends React.PureComponent { constructor(props) { super(props); this.onLaunchTour = this.onLaunchTour.bind(this); this.onCancelTour = this.onCancelTour.bind(this); } onLaunchTour() { this.props.dispatch(ac.SendToMain({ type: at.MIGRATION_START })); this.props.dispatch(ac.UserEvent({ event: at.MIGRATION_START })); } onCancelTour() { this.props.dispatch(ac.SendToMain({ type: at.MIGRATION_CANCEL })); this.props.dispatch(ac.UserEvent({ event: at.MIGRATION_CANCEL })); } render() { return React.createElement( "div", { className: "manual-migration-container" }, React.createElement( "p", null, React.createElement("span", { className: "icon icon-import" }), React.createElement(FormattedMessage, { id: "manual_migration_explanation2" }) ), React.createElement( "div", { className: "manual-migration-actions actions" }, React.createElement( "button", { className: "dismiss", onClick: this.onCancelTour }, React.createElement(FormattedMessage, { id: "manual_migration_cancel_button" }) ), React.createElement( "button", { onClick: this.onLaunchTour }, React.createElement(FormattedMessage, { id: "manual_migration_import_button" }) ) ) ); } } module.exports = connect()(ManualMigration); module.exports._unconnected = ManualMigration; /***/ }), /* 25 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { connect } = __webpack_require__(3); const { injectIntl, FormattedMessage } = __webpack_require__(2); const { actionCreators: ac, actionTypes: at } = __webpack_require__(0); const { TOP_SITES_DEFAULT_LENGTH, TOP_SITES_SHOWMORE_LENGTH } = __webpack_require__(6); const getFormattedMessage = message => typeof message === "string" ? React.createElement( "span", null, message ) : React.createElement(FormattedMessage, message); const PreferencesInput = props => React.createElement( "section", null, React.createElement("input", { type: "checkbox", id: props.prefName, name: props.prefName, checked: props.value, disabled: props.disabled, onChange: props.onChange, className: props.className }), React.createElement( "label", { htmlFor: props.prefName, className: props.labelClassName }, getFormattedMessage(props.titleString) ), props.descString && React.createElement( "p", { className: "prefs-input-description" }, getFormattedMessage(props.descString) ), React.Children.map(props.children, child => React.createElement( "div", { className: `options${child.props.disabled ? " disabled" : ""}` }, child )) ); class PreferencesPane extends React.PureComponent { constructor(props) { super(props); this.handleClickOutside = this.handleClickOutside.bind(this); this.handlePrefChange = this.handlePrefChange.bind(this); this.handleSectionChange = this.handleSectionChange.bind(this); this.togglePane = this.togglePane.bind(this); this.onWrapperMount = this.onWrapperMount.bind(this); } componentDidUpdate(prevProps, prevState) { if (prevProps.PreferencesPane.visible !== this.props.PreferencesPane.visible) { // While the sidebar is open, listen for all document clicks. if (this.isSidebarOpen()) { document.addEventListener("click", this.handleClickOutside); } else { document.removeEventListener("click", this.handleClickOutside); } } } isSidebarOpen() { return this.props.PreferencesPane.visible; } handleClickOutside(event) { // if we are showing the sidebar and there is a click outside, close it. if (this.isSidebarOpen() && !this.wrapper.contains(event.target)) { this.togglePane(); } } handlePrefChange(event) { const target = event.target; const { name, checked } = target; let value = checked; if (name === "topSitesCount") { value = checked ? TOP_SITES_SHOWMORE_LENGTH : TOP_SITES_DEFAULT_LENGTH; } this.props.dispatch(ac.SetPref(name, value)); } handleSectionChange(event) { const target = event.target; const id = target.name; const type = target.checked ? at.SECTION_ENABLE : at.SECTION_DISABLE; this.props.dispatch(ac.SendToMain({ type, data: id })); } togglePane() { if (this.isSidebarOpen()) { this.props.dispatch({ type: at.SETTINGS_CLOSE }); this.props.dispatch(ac.UserEvent({ event: "CLOSE_NEWTAB_PREFS" })); } else { this.props.dispatch({ type: at.SETTINGS_OPEN }); this.props.dispatch(ac.UserEvent({ event: "OPEN_NEWTAB_PREFS" })); } } onWrapperMount(wrapper) { this.wrapper = wrapper; } render() { const props = this.props; const prefs = props.Prefs.values; const sections = props.Sections; const isVisible = this.isSidebarOpen(); return React.createElement( "div", { className: "prefs-pane-wrapper", ref: this.onWrapperMount }, React.createElement( "div", { className: "prefs-pane-button" }, React.createElement("button", { className: `prefs-button icon ${isVisible ? "icon-dismiss" : "icon-settings"}`, title: props.intl.formatMessage({ id: isVisible ? "settings_pane_done_button" : "settings_pane_button_label" }), onClick: this.togglePane }) ), React.createElement( "div", { className: "prefs-pane" }, React.createElement( "div", { className: `sidebar ${isVisible ? "" : "hidden"}` }, React.createElement( "div", { className: "prefs-modal-inner-wrapper" }, React.createElement( "h1", null, React.createElement(FormattedMessage, { id: "settings_pane_header" }) ), React.createElement( "p", null, React.createElement(FormattedMessage, { id: "settings_pane_body2" }) ), React.createElement(PreferencesInput, { className: "showSearch", prefName: "showSearch", value: prefs.showSearch, onChange: this.handlePrefChange, titleString: { id: "settings_pane_search_header" }, descString: { id: "settings_pane_search_body" } }), React.createElement("hr", null), React.createElement( PreferencesInput, { className: "showTopSites", prefName: "showTopSites", value: prefs.showTopSites, onChange: this.handlePrefChange, titleString: { id: "settings_pane_topsites_header" }, descString: { id: "settings_pane_topsites_body" } }, React.createElement(PreferencesInput, { className: "showMoreTopSites", prefName: "topSitesCount", disabled: !prefs.showTopSites, value: prefs.topSitesCount !== TOP_SITES_DEFAULT_LENGTH, onChange: this.handlePrefChange, titleString: { id: "settings_pane_topsites_options_showmore" }, labelClassName: "icon icon-topsites" }) ), sections.filter(section => !section.shouldHidePref).map(({ id, title, enabled, pref }) => React.createElement( PreferencesInput, { key: id, className: "showSection", prefName: pref && pref.feed || id, value: enabled, onChange: pref && pref.feed ? this.handlePrefChange : this.handleSectionChange, titleString: pref && pref.titleString || title, descString: pref && pref.descString }, pref.nestedPrefs && pref.nestedPrefs.map(nestedPref => React.createElement(PreferencesInput, { key: nestedPref.name, prefName: nestedPref.name, disabled: !enabled, value: prefs[nestedPref.name], onChange: this.handlePrefChange, titleString: nestedPref.titleString, labelClassName: `icon ${nestedPref.icon}` })) )), React.createElement("hr", null), React.createElement(PreferencesInput, { className: "showSnippets", prefName: "feeds.snippets", value: prefs["feeds.snippets"], onChange: this.handlePrefChange, titleString: { id: "settings_pane_snippets_header" }, descString: { id: "settings_pane_snippets_body" } }) ), React.createElement( "section", { className: "actions" }, React.createElement( "button", { className: "done", onClick: this.togglePane }, React.createElement(FormattedMessage, { id: "settings_pane_done_button" }) ) ) ) ) ); } } module.exports = connect(state => ({ Prefs: state.Prefs, PreferencesPane: state.PreferencesPane, Sections: state.Sections }))(injectIntl(PreferencesPane)); module.exports.PreferencesPane = PreferencesPane; module.exports.PreferencesInput = PreferencesInput; /***/ }), /* 26 */ /***/ (function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(global) {var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; const React = __webpack_require__(1); const { connect } = __webpack_require__(3); const { injectIntl, FormattedMessage } = __webpack_require__(2); const Card = __webpack_require__(27); const { PlaceholderCard } = Card; const Topics = __webpack_require__(29); const { actionCreators: ac } = __webpack_require__(0); const CollapsibleSection = __webpack_require__(9); const ComponentPerfTimer = __webpack_require__(10); const VISIBLE = "visible"; const VISIBILITY_CHANGE_EVENT = "visibilitychange"; const CARDS_PER_ROW = 3; function getFormattedMessage(message) { return typeof message === "string" ? React.createElement( "span", null, message ) : React.createElement(FormattedMessage, message); } class Section extends React.PureComponent { _dispatchImpressionStats() { const { props } = this; const maxCards = 3 * props.maxRows; const cards = props.rows.slice(0, maxCards); if (this.needsImpressionStats(cards)) { props.dispatch(ac.ImpressionStats({ source: props.eventSource, tiles: cards.map(link => ({ id: link.guid })), incognito: props.options && props.options.personalized })); 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 === 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(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); } // When the page becoems visible, send the impression stats ping if the section isn't collapsed. this._onVisibilityChange = () => { if (props.document.visibilityState === VISIBLE) { const { id, Prefs } = this.props; const isCollapsed = Prefs.values[`section.${id}.collapsed`]; if (!isCollapsed) { this._dispatchImpressionStats(); } props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); } }; props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); } } componentDidMount() { const { id, rows, Prefs } = this.props; const isCollapsed = Prefs.values[`section.${id}.collapsed`]; if (rows.length && !isCollapsed) { this.sendImpressionStatsOrAddListener(); } } componentDidUpdate(prevProps) { const { props } = this; const { id, Prefs } = props; const isCollapsedPref = `section.${id}.collapsed`; const isCollapsed = Prefs.values[isCollapsedPref]; const wasCollapsed = prevProps.Prefs.values[isCollapsedPref]; 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(); } } 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; } numberOfPlaceholders(items) { if (items === 0) { return CARDS_PER_ROW; } const remainder = items % CARDS_PER_ROW; if (remainder === 0) { return 0; } return CARDS_PER_ROW - remainder; } render() { const { id, eventSource, title, icon, rows, infoOption, emptyState, dispatch, maxRows, contextMenuOptions, initialized } = this.props; const maxCards = CARDS_PER_ROW * maxRows; // Show topics only for top stories and if it's not initialized yet (so // content doesn't shift when it is loaded) or has loaded with topics const shouldShowTopics = id === "topstories" && (!this.props.topics || this.props.topics.length > 0); const realRows = rows.slice(0, maxCards); const placeholders = this.numberOfPlaceholders(realRows.length); // 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; //
<-- React component //
<-- HTML5 element return React.createElement( ComponentPerfTimer, this.props, React.createElement( CollapsibleSection, { className: "section", icon: icon, title: getFormattedMessage(title), infoOption: infoOption, prefName: `section.${id}.collapsed`, Prefs: this.props.Prefs, dispatch: this.props.dispatch }, !shouldShowEmptyState && React.createElement( "ul", { className: "section-list", style: { padding: 0 } }, realRows.map((link, index) => link && React.createElement(Card, { key: index, index: index, dispatch: dispatch, link: link, contextMenuOptions: contextMenuOptions, eventSource: eventSource, shouldSendImpressionStats: this.props.shouldSendImpressionStats })), placeholders > 0 && [...new Array(placeholders)].map((_, i) => React.createElement(PlaceholderCard, { key: i })) ), shouldShowEmptyState && React.createElement( "div", { className: "section-empty-state" }, React.createElement( "div", { className: "empty-state" }, emptyState.icon && emptyState.icon.startsWith("moz-extension://") ? React.createElement("img", { className: "empty-state-icon icon", style: { "background-image": `url('${emptyState.icon}')` } }) : React.createElement("img", { className: `empty-state-icon icon icon-${emptyState.icon}` }), React.createElement( "p", { className: "empty-state-message" }, getFormattedMessage(emptyState.message) ) ) ), shouldShowTopics && React.createElement(Topics, { topics: this.props.topics, read_more_endpoint: this.props.read_more_endpoint }) ) ); } } Section.defaultProps = { document: global.document, rows: [], emptyState: {}, title: "" }; const SectionIntl = injectIntl(Section); class Sections extends React.PureComponent { render() { const sections = this.props.Sections; return React.createElement( "div", { className: "sections-list" }, sections.filter(section => section.enabled).map(section => React.createElement(SectionIntl, _extends({ key: section.id }, section, { Prefs: this.props.Prefs, dispatch: this.props.dispatch }))) ); } } module.exports = connect(state => ({ Sections: state.Sections, Prefs: state.Prefs }))(Sections); module.exports._unconnected = Sections; module.exports.SectionIntl = SectionIntl; module.exports._unconnectedSection = Section; /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) /***/ }), /* 27 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const LinkMenu = __webpack_require__(8); const { FormattedMessage } = __webpack_require__(2); const cardContextTypes = __webpack_require__(28); const { actionCreators: ac, actionTypes: at } = __webpack_require__(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 React.PureComponent { constructor(props) { super(props); this.state = { activeCard: null, imageLoaded: false, showContextMenu: false }; this.onMenuButtonClick = this.onMenuButtonClick.bind(this); this.onMenuUpdate = this.onMenuUpdate.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 { image } = this.props.link; if (!this.state.imageLoaded && image) { // Initialize a promise to share a load across multiple card updates if (!gImageLoading.has(image)) { const loaderPromise = new Promise((resolve, reject) => { const loader = new Image(); loader.addEventListener("load", resolve); loader.addEventListener("error", reject); loader.src = image; }); // Save and remove the promise only while it's pending gImageLoading.set(image, loaderPromise); loaderPromise.catch(ex => ex).then(() => gImageLoading.delete(image)).catch(); } // Wait for the image whether just started loading or reused promise await gImageLoading.get(image); // Only update state if we're still waiting to load the original image if (this.props.link.image === image && !this.state.imageLoaded) { this.setState({ imageLoaded: true }); } } } onMenuButtonClick(event) { event.preventDefault(); this.setState({ activeCard: this.props.index, showContextMenu: true }); } onLinkClick(event) { event.preventDefault(); const { altKey, button, ctrlKey, metaKey, shiftKey } = event; this.props.dispatch(ac.SendToMain({ type: at.OPEN_LINK, data: Object.assign(this.props.link, { event: { altKey, button, ctrlKey, metaKey, shiftKey } }) })); this.props.dispatch(ac.UserEvent({ event: "CLICK", source: this.props.eventSource, action_position: this.props.index })); if (this.props.shouldSendImpressionStats) { this.props.dispatch(ac.ImpressionStats({ source: this.props.eventSource, click: 0, incognito: true, tiles: [{ id: this.props.link.guid, pos: this.props.index }] })); } } onMenuUpdate(showContextMenu) { this.setState({ showContextMenu }); } componentDidMount() { this.maybeLoadImage(); } componentDidUpdate() { this.maybeLoadImage(); } componentWillReceiveProps(nextProps) { // Clear the image state if changing images if (nextProps.link.image !== this.props.link.image) { this.setState({ imageLoaded: false }); } } render() { const { index, link, dispatch, contextMenuOptions, eventSource, shouldSendImpressionStats } = this.props; const { props } = this; const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index; // Display "now" as "trending" until we have new strings #3402 const { icon, intlID } = cardContextTypes[link.type === "now" ? "trending" : link.type] || {}; const hasImage = link.image || link.hasImage; const imageStyle = { backgroundImage: link.image ? `url(${link.image})` : "none" }; return React.createElement( "li", { className: `card-outer${isContextMenuOpen ? " active" : ""}${props.placeholder ? " placeholder" : ""}` }, React.createElement( "a", { href: link.url, onClick: !props.placeholder && this.onLinkClick }, React.createElement( "div", { className: "card" }, hasImage && React.createElement( "div", { className: "card-preview-image-outer" }, React.createElement("div", { className: `card-preview-image${this.state.imageLoaded ? " loaded" : ""}`, style: imageStyle }) ), React.createElement( "div", { className: `card-details${hasImage ? "" : " no-image"}` }, link.hostname && React.createElement( "div", { className: "card-host-name" }, link.hostname ), React.createElement( "div", { className: ["card-text", icon ? "" : "no-context", link.description ? "" : "no-description", link.hostname ? "" : "no-host-name", hasImage ? "" : "no-image"].join(" ") }, React.createElement( "h4", { className: "card-title", dir: "auto" }, link.title ), React.createElement( "p", { className: "card-description", dir: "auto" }, link.description ) ), React.createElement( "div", { className: "card-context" }, icon && !link.context && React.createElement("span", { className: `card-context-icon icon icon-${icon}` }), link.icon && link.context && React.createElement("span", { className: "card-context-icon icon", style: { backgroundImage: `url('${link.icon}')` } }), intlID && !link.context && React.createElement( "div", { className: "card-context-label" }, React.createElement(FormattedMessage, { id: intlID, defaultMessage: "Visited" }) ), link.context && React.createElement( "div", { className: "card-context-label" }, link.context ) ) ) ) ), !props.placeholder && React.createElement( "button", { className: "context-menu-button icon", onClick: this.onMenuButtonClick }, React.createElement( "span", { className: "sr-only" }, `Open context menu for ${link.title}` ) ), !props.placeholder && React.createElement(LinkMenu, { dispatch: dispatch, index: index, source: eventSource, onUpdate: this.onMenuUpdate, options: link.contextMenuOptions || contextMenuOptions, site: link, visible: isContextMenuOpen, shouldSendImpressionStats: shouldSendImpressionStats }) ); } } Card.defaultProps = { link: {} }; const PlaceholderCard = () => React.createElement(Card, { placeholder: true }); module.exports = Card; module.exports.PlaceholderCard = PlaceholderCard; /***/ }), /* 28 */ /***/ (function(module, exports) { module.exports = { history: { intlID: "type_label_visited", icon: "historyItem" }, bookmark: { intlID: "type_label_bookmarked", icon: "bookmark-added" }, trending: { intlID: "type_label_recommended", icon: "trending" }, now: { intlID: "type_label_now", icon: "now" } }; /***/ }), /* 29 */ /***/ (function(module, exports, __webpack_require__) { const React = __webpack_require__(1); const { FormattedMessage } = __webpack_require__(2); class Topic extends React.PureComponent { render() { const { url, name } = this.props; return React.createElement( "li", null, React.createElement( "a", { key: name, className: "topic-link", href: url }, name ) ); } } class Topics extends React.PureComponent { render() { const { topics, read_more_endpoint } = this.props; return React.createElement( "div", { className: "topic" }, React.createElement( "span", null, React.createElement(FormattedMessage, { id: "pocket_read_more" }) ), React.createElement( "ul", null, topics && topics.map(t => React.createElement(Topic, { key: t.name, url: t.url, name: t.name })) ), read_more_endpoint && React.createElement( "a", { className: "topic-read-more", href: read_more_endpoint }, React.createElement(FormattedMessage, { id: "pocket_read_even_more" }) ) ); } } module.exports = Topics; module.exports._unconnected = Topics; module.exports.Topic = Topic; /***/ }), /* 30 */ /***/ (function(module, exports) { class _PrerenderData { constructor(options) { this.initialPrefs = options.initialPrefs; this.initialSections = options.initialSections; this._setValidation(options.validation); } get validation() { return this._validation; } set validation(value) { this._setValidation(value); } get invalidatingPrefs() { return this._invalidatingPrefs; } // This is needed so we can use it in the constructor _setValidation(value = []) { this._validation = value; this._invalidatingPrefs = value.reduce((result, next) => { if (typeof next === "string") { result.push(next); return result; } else if (next && next.oneOf) { return result.concat(next.oneOf); } throw new Error("Your validation configuration is not properly configured"); }, []); } arePrefsValid(getPref) { for (const prefs of this.validation) { // {oneOf: ["foo", "bar"]} if (prefs && prefs.oneOf && !prefs.oneOf.some(name => getPref(name) === this.initialPrefs[name])) { return false; // "foo" } else if (getPref(prefs) !== this.initialPrefs[prefs]) { return false; } } return true; } } var PrerenderData = new _PrerenderData({ initialPrefs: { "migrationExpired": true, "showTopSites": true, "showSearch": true, "topSitesCount": 6, "feeds.section.topstories": true, "feeds.section.highlights": true }, // Prefs listed as invalidating will prevent the prerendered version // of AS from being used if their value is something other than what is listed // here. This is required because some preferences cause the page layout to be // too different for the prerendered version to be used. Unfortunately, this // will result in users who have modified some of their preferences not being // able to get the benefits of prerendering. validation: ["showTopSites", "showSearch", // This means if either of these are set to their default values, // prerendering can be used. { oneOf: ["feeds.section.topstories", "feeds.section.highlights"] }], initialSections: [{ enabled: true, icon: "pocket", id: "topstories", order: 1, title: { id: "header_recommended_by", values: { provider: "Pocket" } } }, { enabled: true, id: "highlights", icon: "highlights", order: 2, title: { id: "header_highlights" } }] }); module.exports = { PrerenderData, _PrerenderData }; /***/ }), /* 31 */ /***/ (function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(global) {/* eslint-env mozilla/frame-script */ const { createStore, combineReducers, applyMiddleware } = __webpack_require__(32); const { actionTypes: at, actionCreators: ac, actionUtils: au } = __webpack_require__(0); const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE"; const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain"; const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent"; /** * A higher-order function which returns a reducer that, on MERGE_STORE action, * will return the action.data object merged into the previous state. * * 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 Object.assign({}, 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 => { if (au.isSendToMain(action)) { sendAsyncMessage(OUTGOING_MESSAGE_NAME, action); } next(action); }; const rehydrationMiddleware = store => next => action => { if (store._didRehydrate) { return next(action); } const isMergeStoreAction = action.type === MERGE_STORE_ACTION; const isRehydrationRequest = action.type === at.NEW_TAB_STATE_REQUEST; if (isRehydrationRequest) { store._didRequestInitialState = true; return next(action); } if (isMergeStoreAction) { store._didRehydrate = true; return next(action); } // If init happened after our request was made, we need to re-request if (store._didRequestInitialState && action.type === at.INIT) { return next(ac.SendToMain({ type: at.NEW_TAB_STATE_REQUEST })); } if (au.isBroadcastToContent(action) || au.isSendToContent(action)) { // Note that actions received before didRehydrate will not be dispatched // because this could negatively affect preloading and the the state // will be replaced by rehydration anyway. return null; } return next(action); }; /** * initStore - Create a store and listen for incoming actions * * @param {object} reducers An object containing Redux reducers * @param {object} intialState (optional) The initial state of the store, if desired * @return {object} A redux store */ module.exports = function initStore(reducers, initialState) { const store = createStore(mergeStateReducer(combineReducers(reducers)), initialState, global.addMessageListener && applyMiddleware(rehydrationMiddleware, messageMiddleware)); store._didRehydrate = false; store._didRequestInitialState = false; if (global.addMessageListener) { global.addMessageListener(INCOMING_MESSAGE_NAME, msg => { try { store.dispatch(msg.data); } catch (ex) { console.error("Content msg:", msg, "Dispatch error: ", ex); // eslint-disable-line no-console dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`); } }); } return store; }; module.exports.rehydrationMiddleware = rehydrationMiddleware; module.exports.MERGE_STORE_ACTION = MERGE_STORE_ACTION; module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME; module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME; /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) /***/ }), /* 32 */ /***/ (function(module, exports) { module.exports = Redux; /***/ }), /* 33 */ /***/ (function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(global) {const { actionTypes: at } = __webpack_require__(0); const { perfService: perfSvc } = __webpack_require__(11); const VISIBLE = "visible"; const VISIBILITY_CHANGE_EVENT = "visibilitychange"; module.exports = class DetectUserSessionStart { constructor(options = {}) { // Overrides for testing this.sendAsyncMessage = options.sendAsyncMessage || global.sendAsyncMessage; this.document = options.document || global.document; this._perfService = options.perfService || perfSvc; this._onVisibilityChange = this._onVisibilityChange.bind(this); } /** * sendEventOrAddListener - Notify immediately if the page is already visible, * or else set up a listener for when visibility changes. * This is needed for accurate session tracking for telemetry, * because tabs are pre-loaded. */ sendEventOrAddListener() { if (this.document.visibilityState === 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(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.sendAsyncMessage("ActivityStream:ContentToMain", { type: at.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 === VISIBLE) { this._sendEvent(); this.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); } } }; /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) /***/ }), /* 34 */ /***/ (function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(global) {const DATABASE_NAME = "snippets_db"; const DATABASE_VERSION = 1; const SNIPPETS_OBJECTSTORE_NAME = "snippets"; const SNIPPETS_UPDATE_INTERVAL_MS = 14400000; // 4 hours. const SNIPPETS_ENABLED_EVENT = "Snippets:Enabled"; const SNIPPETS_DISABLED_EVENT = "Snippets:Disabled"; const { actionTypes: at, actionCreators: ac } = __webpack_require__(0); /** * SnippetsMap - A utility for cacheing values related to the snippet. It has * the same interface as a Map, but is optionally backed by * indexedDB for persistent storage. * Call .connect() to open a database connection and restore any * previously cached data, if necessary. * */ class SnippetsMap extends Map { constructor(dispatch) { super(); this._db = null; this._dispatch = dispatch; } set(key, value) { super.set(key, value); return this._dbTransaction(db => db.put(value, key)); } delete(key) { super.delete(key); return this._dbTransaction(db => db.delete(key)); } clear() { super.clear(); return this._dbTransaction(db => db.clear()); } get blockList() { return this.get("blockList") || []; } /** * blockSnippetById - Blocks a snippet given an id * * @param {str|int} id The id of the snippet * @return {Promise} Resolves when the id has been written to indexedDB, * or immediately if the snippetMap is not connected */ async blockSnippetById(id) { if (!id) { return; } let blockList = this.blockList; if (!blockList.includes(id)) { blockList.push(id); } await this.set("blockList", blockList); } disableOnboarding() { this._dispatch(ac.SendToMain({ type: at.DISABLE_ONBOARDING })); } showFirefoxAccounts() { this._dispatch(ac.SendToMain({ type: at.SHOW_FIREFOX_ACCOUNTS })); } /** * connect - Attaches an indexedDB back-end to the Map so that any set values * are also cached in a store. It also restores any existing values * that are already stored in the indexedDB store. * * @return {type} description */ async connect() { // Open the connection const db = await this._openDB(); // Restore any existing values await this._restoreFromDb(db); // Attach a reference to the db this._db = db; } /** * _dbTransaction - Returns a db transaction wrapped with the given modifier * function as a Promise. If the db has not been connected, * it resolves immediately. * * @param {func} modifier A function to call with the transaction * @return {obj} A Promise that resolves when the transaction has * completed or errored */ _dbTransaction(modifier) { if (!this._db) { return Promise.resolve(); } return new Promise((resolve, reject) => { const transaction = modifier(this._db.transaction(SNIPPETS_OBJECTSTORE_NAME, "readwrite").objectStore(SNIPPETS_OBJECTSTORE_NAME)); transaction.onsuccess = event => resolve(); /* istanbul ignore next */ transaction.onerror = event => reject(transaction.error); }); } _openDB() { return new Promise((resolve, reject) => { const openRequest = indexedDB.open(DATABASE_NAME, DATABASE_VERSION); /* istanbul ignore next */ openRequest.onerror = event => { // Try to delete the old database so that we can start this process over // next time. indexedDB.deleteDatabase(DATABASE_NAME); reject(event); }; openRequest.onupgradeneeded = event => { const db = event.target.result; if (!db.objectStoreNames.contains(SNIPPETS_OBJECTSTORE_NAME)) { db.createObjectStore(SNIPPETS_OBJECTSTORE_NAME); } }; openRequest.onsuccess = event => { let db = event.target.result; /* istanbul ignore next */ db.onerror = err => console.error(err); // eslint-disable-line no-console /* istanbul ignore next */ db.onversionchange = versionChangeEvent => versionChangeEvent.target.close(); resolve(db); }; }); } _restoreFromDb(db) { return new Promise((resolve, reject) => { let cursorRequest; try { cursorRequest = db.transaction(SNIPPETS_OBJECTSTORE_NAME).objectStore(SNIPPETS_OBJECTSTORE_NAME).openCursor(); } catch (err) { // istanbul ignore next reject(err); // istanbul ignore next return; } /* istanbul ignore next */ cursorRequest.onerror = event => reject(event); cursorRequest.onsuccess = event => { let cursor = event.target.result; // Populate the cache from the persistent storage. if (cursor) { this.set(cursor.key, cursor.value); cursor.continue(); } else { // We are done. resolve(); } }; }); } } /** * SnippetsProvider - Initializes a SnippetsMap and loads snippets from a * remote location, or else default snippets if the remote * snippets cannot be retrieved. */ class SnippetsProvider { constructor(dispatch) { // Initialize the Snippets Map and attaches it to a global so that // the snippet payload can interact with it. global.gSnippetsMap = new SnippetsMap(dispatch); } get snippetsMap() { return global.gSnippetsMap; } async _refreshSnippets() { // Check if the cached version of of the snippets in snippetsMap. If it's too // old, blow away the entire snippetsMap. const cachedVersion = this.snippetsMap.get("snippets-cached-version"); if (cachedVersion !== this.appData.version) { this.snippetsMap.clear(); } // Has enough time passed for us to require an update? const lastUpdate = this.snippetsMap.get("snippets-last-update"); const needsUpdate = !(lastUpdate >= 0) || Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS; if (needsUpdate && this.appData.snippetsURL) { this.snippetsMap.set("snippets-last-update", Date.now()); try { const response = await fetch(this.appData.snippetsURL); if (response.status === 200) { const payload = await response.text(); this.snippetsMap.set("snippets", payload); this.snippetsMap.set("snippets-cached-version", this.appData.version); } } catch (e) { console.error(e); // eslint-disable-line no-console } } } _noSnippetFallback() { // TODO } _forceOnboardingVisibility(shouldBeVisible) { const onboardingEl = document.getElementById("onboarding-notification-bar"); if (onboardingEl) { onboardingEl.style.display = shouldBeVisible ? "" : "none"; } } _showRemoteSnippets() { const snippetsEl = document.getElementById(this.elementId); const payload = this.snippetsMap.get("snippets"); if (!snippetsEl) { throw new Error(`No element was found with id '${this.elementId}'.`); } // This could happen if fetching failed if (!payload) { throw new Error("No remote snippets were found in gSnippetsMap."); } if (typeof payload !== "string") { throw new Error("Snippet payload was incorrectly formatted"); } // Note that injecting snippets can throw if they're invalid XML. snippetsEl.innerHTML = payload; // Scripts injected by innerHTML are inactive, so we have to relocate them // through DOM manipulation to activate their contents. for (const scriptEl of snippetsEl.getElementsByTagName("script")) { const relocatedScript = document.createElement("script"); relocatedScript.text = scriptEl.text; scriptEl.parentNode.replaceChild(relocatedScript, scriptEl); } } /** * init - Fetch the snippet payload and show snippets * * @param {obj} options * @param {str} options.appData.snippetsURL The URL from which we fetch snippets * @param {int} options.appData.version The current snippets version * @param {str} options.elementId The id of the element in which to inject snippets * @param {bool} options.connect Should gSnippetsMap connect to indexedDB? */ async init(options) { Object.assign(this, { appData: {}, elementId: "snippets", connect: true }, options); // TODO: Requires enabling indexedDB on newtab // Restore the snippets map from indexedDB if (this.connect) { try { await this.snippetsMap.connect(); } catch (e) { console.error(e); // eslint-disable-line no-console } } // Cache app data values so they can be accessible from gSnippetsMap for (const key of Object.keys(this.appData)) { this.snippetsMap.set(`appData.${key}`, this.appData[key]); } // Refresh snippets, if enough time has passed. await this._refreshSnippets(); // Try showing remote snippets, falling back to defaults if necessary. try { this._showRemoteSnippets(); } catch (e) { this._noSnippetFallback(e); } window.dispatchEvent(new Event(SNIPPETS_ENABLED_EVENT)); this._forceOnboardingVisibility(true); this.initialized = true; } uninit() { window.dispatchEvent(new Event(SNIPPETS_DISABLED_EVENT)); this._forceOnboardingVisibility(false); this.initialized = false; } } /** * addSnippetsSubscriber - Creates a SnippetsProvider that Initializes * when the store has received the appropriate * Snippet data. * * @param {obj} store The redux store * @return {obj} Returns the snippets instance and unsubscribe function */ function addSnippetsSubscriber(store) { const snippets = new SnippetsProvider(store.dispatch); let initializing = false; store.subscribe(async () => { const state = store.getState(); // state.Prefs.values["feeds.snippets"]: Should snippets be shown? // state.Snippets.initialized Is the snippets data initialized? // snippets.initialized: Is SnippetsProvider currently initialised? if (state.Prefs.values["feeds.snippets"] && state.Snippets.initialized && !snippets.initialized && // Don't call init multiple times !initializing) { initializing = true; await snippets.init({ appData: state.Snippets }); initializing = false; } else if (state.Prefs.values["feeds.snippets"] === false && snippets.initialized) { snippets.uninit(); } }); // These values are returned for testing purposes return snippets; } module.exports = { addSnippetsSubscriber, SnippetsMap, SnippetsProvider, SNIPPETS_UPDATE_INTERVAL_MS }; /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) /***/ }) /******/ ]);