--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -55,17 +55,17 @@
/******/
/******/ // 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 = 10);
+/******/ return __webpack_require__(__webpack_require__.s = 11);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/* This Source Code Form is subject to the terms of the Mozilla Public
@@ -89,17 +89,17 @@ const globalImportContext = typeof Windo
// Create an object that avoids accidental differing key/value pairs:
// {
// INIT: "INIT",
// UNINIT: "UNINIT"
// }
const actionTypes = {};
-for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "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", "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"]) {
+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) {
@@ -387,17 +387,18 @@ const INITIAL_STATE = {
Prefs: {
initialized: false,
values: {}
},
Dialog: {
visible: false,
data: {}
},
- Sections: []
+ 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:
{
@@ -653,17 +654,28 @@ function Snippets(prevState = INITIAL_ST
return Object.assign({}, prevState, { initialized: true }, action.data);
case at.SNIPPETS_RESET:
return INITIAL_STATE.Snippets;
default:
return prevState;
}
}
-var reducers = { TopSites, App, Snippets, Prefs, Dialog, Sections };
+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
};
@@ -1021,19 +1033,19 @@ module.exports.TopSiteLink = TopSiteLink
module.exports.TopSitePlaceholder = TopSitePlaceholder;
/***/ }),
/* 9 */
/***/ (function(module, exports, __webpack_require__) {
const React = __webpack_require__(1);
const { injectIntl } = __webpack_require__(2);
-const ContextMenu = __webpack_require__(17);
+const ContextMenu = __webpack_require__(18);
const { actionCreators: ac } = __webpack_require__(0);
-const linkMenuOptions = __webpack_require__(18);
+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
@@ -1077,19 +1089,308 @@ class LinkMenu extends React.PureCompone
module.exports = injectIntl(LinkMenu);
module.exports._unconnected = LinkMenu;
/***/ }),
/* 10 */
/***/ (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__(20);
+const { PlaceholderCard } = Card;
+const Topics = __webpack_require__(22);
+const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
+
+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 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 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.
+ this._onVisibilityChange = () => {
+ if (props.document.visibilityState === VISIBLE) {
+ this._dispatchImpressionStats();
+ props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ };
+ props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+
+ componentDidMount() {
+ if (this.props.rows.length) {
+ this.sendImpressionStatsOrAddListener();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ const { props } = this;
+ if (
+ // Don't send impression stats for the empty state
+ props.rows.length &&
+ // We only want to send impression stats if the content of the cards has changed
+ props.rows !== prevProps.rows) {
+ this.sendImpressionStatsOrAddListener();
+ }
+ }
+
+ 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;
+
+ // <Section> <-- React component
+ // <section> <-- HTML5 element
+ return React.createElement(
+ "section",
+ { className: "section" },
+ React.createElement(
+ "div",
+ { className: "section-top-bar" },
+ React.createElement(
+ "h3",
+ { className: "section-title" },
+ icon && icon.startsWith("moz-extension://") ? React.createElement("span", { className: "icon icon-small-spacer", style: { "background-image": `url('${icon}')` } }) : React.createElement("span", { className: `icon icon-small-spacer icon-${icon || "webextension"}` }),
+ getFormattedMessage(title)
+ ),
+ infoOption && React.createElement(InfoIntl, { infoOption: infoOption, dispatch: 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, { dispatch: this.props.dispatch })))
+ );
+ }
+}
+
+module.exports = connect(state => ({ Sections: state.Sections }))(Sections);
+module.exports._unconnected = Sections;
+module.exports.SectionIntl = SectionIntl;
+module.exports._unconnectedSection = Section;
+module.exports.Info = Info;
+module.exports.InfoIntl = InfoIntl;
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
+
+/***/ }),
+/* 11 */
+/***/ (function(module, exports, __webpack_require__) {
+
/* WEBPACK VAR INJECTION */(function(global) {const React = __webpack_require__(1);
-const ReactDOM = __webpack_require__(11);
-const Base = __webpack_require__(12);
+const ReactDOM = __webpack_require__(12);
+const Base = __webpack_require__(13);
const { Provider } = __webpack_require__(3);
const initStore = __webpack_require__(29);
const { reducers } = __webpack_require__(6);
const DetectUserSessionStart = __webpack_require__(31);
const { addSnippetsSubscriber } = __webpack_require__(32);
const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
new DetectUserSessionStart().sendEventOrAddListener();
@@ -1108,34 +1409,34 @@ ReactDOM.render(React.createElement(
{ store: store },
React.createElement(Base, { isPrerendered: !!global.gActivityStreamPrerenderedState })
), document.getElementById("root"));
addSnippetsSubscriber(store);
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
-/* 11 */
+/* 12 */
/***/ (function(module, exports) {
module.exports = ReactDOM;
/***/ }),
-/* 12 */
+/* 13 */
/***/ (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__(13);
-const Search = __webpack_require__(19);
-const ConfirmDialog = __webpack_require__(21);
-const ManualMigration = __webpack_require__(22);
-const PreferencesPane = __webpack_require__(23);
-const Sections = __webpack_require__(24);
+const TopSites = __webpack_require__(14);
+const Search = __webpack_require__(23);
+const ConfirmDialog = __webpack_require__(25);
+const ManualMigration = __webpack_require__(26);
+const PreferencesPane = __webpack_require__(27);
+const Sections = __webpack_require__(10);
const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
const { PrerenderData } = __webpack_require__(28);
// 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" }]);
@@ -1229,45 +1530,51 @@ class Base extends React.PureComponent {
);
}
}
module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
module.exports._unconnected = Base;
/***/ }),
-/* 13 */
+/* 14 */
/***/ (function(module, exports, __webpack_require__) {
const React = __webpack_require__(1);
const { connect } = __webpack_require__(3);
const { FormattedMessage } = __webpack_require__(2);
-const TopSitesPerfTimer = __webpack_require__(14);
-const TopSitesEdit = __webpack_require__(15);
+const TopSitesPerfTimer = __webpack_require__(15);
+const TopSitesEdit = __webpack_require__(16);
const { TopSite, TopSitePlaceholder } = __webpack_require__(8);
+const { InfoIntl } = __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(
TopSitesPerfTimer,
null,
React.createElement(
"section",
- { className: "top-sites" },
+ { className: "section top-sites" },
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(InfoIntl, { infoOption: infoOption, 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,
@@ -1279,17 +1586,17 @@ const TopSites = props => {
)
);
};
module.exports = connect(state => ({ TopSites: state.TopSites, TopSitesCount: state.Prefs.values.topSitesCount }))(TopSites);
module.exports._unconnected = TopSites;
/***/ }),
-/* 14 */
+/* 15 */
/***/ (function(module, exports, __webpack_require__) {
const React = __webpack_require__(1);
const { connect } = __webpack_require__(3);
const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
const { perfService: perfSvc } = __webpack_require__(7);
/**
@@ -1398,24 +1705,24 @@ class TopSitesPerfTimer extends React.Pu
return this.props.children;
}
}
module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSitesPerfTimer);
module.exports._unconnected = TopSitesPerfTimer;
/***/ }),
-/* 15 */
+/* 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__(16);
+const TopSiteForm = __webpack_require__(17);
const { TopSite, TopSitePlaceholder } = __webpack_require__(8);
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);
@@ -1586,17 +1893,17 @@ class TopSitesEdit extends React.PureCom
);
}
}
module.exports = injectIntl(TopSitesEdit);
module.exports._unconnected = TopSitesEdit;
/***/ }),
-/* 16 */
+/* 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);
@@ -1766,17 +2073,17 @@ TopSiteForm.defaultProps = {
url: "",
index: 0,
editMode: false // by default we are in "Add New Top Site" mode
};
module.exports = TopSiteForm;
/***/ }),
-/* 17 */
+/* 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);
@@ -1855,17 +2162,17 @@ class ContextMenuItem extends React.Pure
}
}
module.exports = ContextMenu;
module.exports.ContextMenu = ContextMenu;
module.exports.ContextMenuItem = ContextMenuItem;
/***/ }),
-/* 18 */
+/* 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.
@@ -1978,733 +2285,23 @@ module.exports = {
}
})
};
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);
/***/ }),
-/* 19 */
-/***/ (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__(20);
-
-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;
-
-/***/ }),
/* 20 */
/***/ (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)))
-
-/***/ }),
-/* 21 */
-/***/ (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;
-
-/***/ }),
-/* 22 */
-/***/ (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;
-
-/***/ }),
-/* 23 */
-/***/ (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)
- )
-);
-
-class PreferencesPane extends React.PureComponent {
- constructor(props) {
- super(props);
- this.state = { visible: false };
- 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);
- }
- componentDidMount() {
- document.addEventListener("click", this.handleClickOutside);
- }
- componentWillUnmount() {
- document.removeEventListener("click", this.handleClickOutside);
- }
- handleClickOutside(event) {
- // if we are showing the sidebar and there is a click outside, close it.
- if (this.state.visible && !this.wrapper.contains(event.target)) {
- this.togglePane();
- }
- }
- handlePrefChange(event) {
- const target = event.target;
- const { name, 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() {
- this.setState({ visible: !this.state.visible });
- const event = this.state.visible ? "CLOSE_NEWTAB_PREFS" : "OPEN_NEWTAB_PREFS";
- this.props.dispatch(ac.UserEvent({ event }));
- }
- onWrapperMount(wrapper) {
- this.wrapper = wrapper;
- }
- render() {
- const props = this.props;
- const prefs = props.Prefs.values;
- const sections = props.Sections;
- const isVisible = this.state.visible;
- 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(
- "div",
- { className: `options${prefs.showTopSites ? "" : " disabled"}` },
- 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 })),
- 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, Sections: state.Sections }))(injectIntl(PreferencesPane));
-module.exports.PreferencesPane = PreferencesPane;
-module.exports.PreferencesInput = PreferencesInput;
-
-/***/ }),
-/* 24 */
-/***/ (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__(25);
-const { PlaceholderCard } = Card;
-const Topics = __webpack_require__(27);
-const { actionCreators: ac } = __webpack_require__(0);
-
-const VISIBLE = "visible";
-const VISIBILITY_CHANGE_EVENT = "visibilitychange";
-const CARDS_PER_ROW = 3;
-
-class Section extends React.PureComponent {
- constructor(props) {
- super(props);
- this.onInfoEnter = this.onInfoEnter.bind(this);
- this.onInfoLeave = this.onInfoLeave.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));
- }
-
- getFormattedMessage(message) {
- return typeof message === "string" ? React.createElement(
- "span",
- null,
- message
- ) : React.createElement(FormattedMessage, message);
- }
-
- _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.
- this._onVisibilityChange = () => {
- if (props.document.visibilityState === VISIBLE) {
- this._dispatchImpressionStats();
- props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
- }
- };
- props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
- }
- }
-
- componentDidMount() {
- if (this.props.rows.length) {
- this.sendImpressionStatsOrAddListener();
- }
- }
-
- componentDidUpdate(prevProps) {
- const { props } = this;
- if (
- // Don't send impression stats for the empty state
- props.rows.length &&
- // We only want to send impression stats if the content of the cards has changed
- props.rows !== prevProps.rows) {
- this.sendImpressionStatsOrAddListener();
- }
- }
-
- 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, intl, 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 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" });
-
- 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;
-
- // <Section> <-- React component
- // <section> <-- HTML5 element
- return React.createElement(
- "section",
- null,
- React.createElement(
- "div",
- { className: "section-top-bar" },
- React.createElement(
- "h3",
- { className: "section-title" },
- icon && icon.startsWith("moz-extension://") ? React.createElement("span", { className: "icon icon-small-spacer", style: { "background-image": `url('${icon}')` } }) : React.createElement("span", { className: `icon icon-small-spacer icon-${icon || "webextension"}` }),
- this.getFormattedMessage(title)
- ),
- infoOption && 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" },
- this.getFormattedMessage(infoOption.header)
- ),
- infoOption.body && React.createElement(
- "p",
- { className: "info-option-body" },
- this.getFormattedMessage(infoOption.body)
- ),
- infoOption.link && React.createElement(
- "a",
- { href: infoOption.link.href, target: "_blank", rel: "noopener noreferrer", className: "info-option-link" },
- this.getFormattedMessage(infoOption.link.title || infoOption.link)
- )
- )
- )
- ),
- !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" },
- this.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, { dispatch: this.props.dispatch })))
- );
- }
-}
-
-module.exports = connect(state => ({ Sections: state.Sections }))(Sections);
-module.exports._unconnected = Sections;
-module.exports.SectionIntl = SectionIntl;
-module.exports._unconnectedSection = Section;
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
-
-/***/ }),
-/* 25 */
-/***/ (function(module, exports, __webpack_require__) {
-
const React = __webpack_require__(1);
const LinkMenu = __webpack_require__(9);
const { FormattedMessage } = __webpack_require__(2);
-const cardContextTypes = __webpack_require__(26);
+const cardContextTypes = __webpack_require__(21);
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
@@ -2890,17 +2487,17 @@ class Card extends React.PureComponent {
Card.defaultProps = { link: {} };
const PlaceholderCard = () => React.createElement(Card, { placeholder: true });
module.exports = Card;
module.exports.PlaceholderCard = PlaceholderCard;
/***/ }),
-/* 26 */
+/* 21 */
/***/ (function(module, exports) {
module.exports = {
history: {
intlID: "type_label_visited",
icon: "historyItem"
},
bookmark: {
@@ -2913,17 +2510,17 @@ module.exports = {
},
now: {
intlID: "type_label_now",
icon: "now"
}
};
/***/ }),
-/* 27 */
+/* 22 */
/***/ (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;
@@ -2964,16 +2561,468 @@ class Topics extends React.PureComponent
}
}
module.exports = Topics;
module.exports._unconnected = Topics;
module.exports.Topic = Topic;
/***/ }),
+/* 23 */
+/***/ (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__(24);
+
+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;
+
+/***/ }),
+/* 24 */
+/***/ (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)))
+
+/***/ }),
+/* 25 */
+/***/ (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;
+
+/***/ }),
+/* 26 */
+/***/ (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;
+
+/***/ }),
+/* 27 */
+/***/ (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)
+ )
+);
+
+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(
+ "div",
+ { className: `options${prefs.showTopSites ? "" : " disabled"}` },
+ 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 })),
+ 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;
+
+/***/ }),
/* 28 */
/***/ (function(module, exports) {
class _PrerenderData {
constructor(options) {
this.initialPrefs = options.initialPrefs;
this.initialSections = options.initialSections;
this._setValidation(options.validation);