diff --git a/browser/extensions/activity-stream/common/Actions.jsm b/browser/extensions/activity-stream/common/Actions.jsm index d05ec4c95d6d..178ec06192bb 100644 --- a/browser/extensions/activity-stream/common/Actions.jsm +++ b/browser/extensions/activity-stream/common/Actions.jsm @@ -34,6 +34,8 @@ for (const type of [ "FEED_INIT", "INIT", "LOCALE_UPDATED", + "MIGRATION_CANCEL", + "MIGRATION_START", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", diff --git a/browser/extensions/activity-stream/common/Reducers.jsm b/browser/extensions/activity-stream/common/Reducers.jsm index 5ec24cf47541..8aaf6eb0e822 100644 --- a/browser/extensions/activity-stream/common/Reducers.jsm +++ b/browser/extensions/activity-stream/common/Reducers.jsm @@ -202,6 +202,37 @@ function Sections(prevState = INITIAL_STATE.Sections, action) { } 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, lastModified} = action.data; + Object.assign(item, {bookmarkGuid, bookmarkTitle, bookmarkDateCreated: lastModified}); + } + 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; + return newSite; + } + return item; + }) + })); case at.PLACES_LINK_DELETED: case at.PLACES_LINK_BLOCKED: return prevState.map(section => diff --git a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js index 47306715aace..6c4269d75a20 100644 --- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js +++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js @@ -1,41 +1,41 @@ /******/ (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; - +/******/ /******/ // identity function for calling harmony imports with the correct context /******/ __webpack_require__.i = function(value) { return value; }; - +/******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { @@ -46,7 +46,7 @@ /******/ }); /******/ } /******/ }; - +/******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? @@ -55,15 +55,15 @@ /******/ __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 = 25); +/******/ return __webpack_require__(__webpack_require__.s = 26); /******/ }) /************************************************************************/ /******/ ([ @@ -103,7 +103,7 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : // UNINIT: "UNINIT" // } const actionTypes = {}; -for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "FEED_INIT", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_REGISTER", "SECTION_ROWS_UPDATE", "SET_PREF", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "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", "FEED_INIT", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_START", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_REGISTER", "SECTION_ROWS_UPDATE", "SET_PREF", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) { actionTypes[type] = type; } @@ -312,6 +312,7 @@ module.exports = ReactRedux; * via Services.eTLD.getPublicSuffix * {str} link.hostname (optional) - The hostname of the url * e.g. for http://www.hello.com/foo/bar, the hostname would be "www.hello.com" + * {str} link.title (optional) - The title of the link * @return {str} A short url */ module.exports = function shortURL(link) { @@ -326,7 +327,7 @@ module.exports = function shortURL(link) { const eTLDLength = (eTLD || "").length || hostname.match(/\.com$/) && 3; const eTLDExtra = eTLDLength > 0 ? -(eTLDLength + 1) : Infinity; // If URL and hostname are not present fallback to page title. - return hostname.slice(0, eTLDExtra).toLowerCase() || hostname || link.title; + return hostname.slice(0, eTLDExtra).toLowerCase() || hostname || link.title || link.url; }; /***/ }), @@ -348,7 +349,7 @@ var _require2 = __webpack_require__(1); const ac = _require2.actionCreators; -const linkMenuOptions = __webpack_require__(21); +const linkMenuOptions = __webpack_require__(22); const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow"]; class LinkMenu extends React.Component { @@ -420,11 +421,12 @@ var _require2 = __webpack_require__(2); const addLocaleData = _require2.addLocaleData, IntlProvider = _require2.IntlProvider; -const TopSites = __webpack_require__(19); -const Search = __webpack_require__(17); +const TopSites = __webpack_require__(20); +const Search = __webpack_require__(18); const ConfirmDialog = __webpack_require__(14); -const PreferencesPane = __webpack_require__(16); -const Sections = __webpack_require__(18); +const ManualMigration = __webpack_require__(16); +const PreferencesPane = __webpack_require__(17); +const Sections = __webpack_require__(19); // Locales that should be displayed RTL const RTL_LIST = ["ar", "he", "fa", "ur"]; @@ -468,7 +470,6 @@ class Base extends React.Component { initialized = _props$App.initialized; const prefs = props.Prefs.values; - if (!initialized) { return null; } @@ -483,6 +484,7 @@ class Base extends React.Component { "main", null, prefs.showSearch && React.createElement(Search, null), + !prefs.migrationExpired && React.createElement(ManualMigration, null), prefs.showTopSites && React.createElement(TopSites, null), React.createElement(Sections, null), React.createElement(ConfirmDialog, null) @@ -506,7 +508,7 @@ var _require = __webpack_require__(1); const at = _require.actionTypes; -var _require2 = __webpack_require__(22); +var _require2 = __webpack_require__(23); const perfSvc = _require2.perfService; @@ -579,7 +581,7 @@ module.exports = class DetectUserSessionStart { /* eslint-env mozilla/frame-script */ -var _require = __webpack_require__(24); +var _require = __webpack_require__(25); const createStore = _require.createStore, combineReducers = _require.combineReducers, @@ -644,7 +646,7 @@ module.exports = function initStore(reducers) { 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 }`); + dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`); } }); @@ -853,7 +855,7 @@ class SnippetsProvider { const payload = this.snippetsMap.get("snippets"); if (!snippetsEl) { - throw new Error(`No element was found with id '${ this.elementId }'.`); + throw new Error(`No element was found with id '${this.elementId}'.`); } // This could happen if fetching failed @@ -920,7 +922,7 @@ class SnippetsProvider { module.exports.SnippetsMap = SnippetsMap; module.exports.SnippetsProvider = SnippetsProvider; module.exports.SNIPPETS_UPDATE_INTERVAL_MS = SNIPPETS_UPDATE_INTERVAL_MS; -/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(23))) +/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(24))) /***/ }), /* 10 */ @@ -1159,6 +1161,41 @@ function Sections() { } 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) { + var _action$data3 = action.data; + const bookmarkGuid = _action$data3.bookmarkGuid, + bookmarkTitle = _action$data3.bookmarkTitle, + lastModified = _action$data3.lastModified; + + Object.assign(item, { bookmarkGuid, bookmarkTitle, bookmarkDateCreated: lastModified }); + } + 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; + return newSite; + } + return item; + }) + })); case at.PLACES_LINK_DELETED: case at.PLACES_LINK_BLOCKED: return prevState.map(section => Object.assign({}, section, { rows: section.rows.filter(site => site.url !== action.data.url) })); @@ -1224,9 +1261,18 @@ class Card extends React.Component { constructor(props) { super(props); this.state = { showContextMenu: false, activeCard: null }; + this.onMenuButtonClick = this.onMenuButtonClick.bind(this); + this.onMenuUpdate = this.onMenuUpdate.bind(this); } - toggleContextMenu(event, index) { - this.setState({ showContextMenu: true, activeCard: index }); + onMenuButtonClick(event) { + event.preventDefault(); + this.setState({ + activeCard: this.props.index, + showContextMenu: true + }); + } + onMenuUpdate(showContextMenu) { + this.setState({ showContextMenu }); } render() { var _props = this.props; @@ -1244,14 +1290,14 @@ class Card extends React.Component { return React.createElement( "li", - { className: `card-outer${ isContextMenuOpen ? " active" : "" }` }, + { className: `card-outer${isContextMenuOpen ? " active" : ""}` }, React.createElement( "a", { href: link.url }, React.createElement( "div", { className: "card" }, - link.image && React.createElement("div", { className: "card-preview-image", style: { backgroundImage: `url(${ link.image })` } }), + link.image && React.createElement("div", { className: "card-preview-image", style: { backgroundImage: `url(${link.image})` } }), React.createElement( "div", { className: "card-details" }, @@ -1264,17 +1310,17 @@ class Card extends React.Component { ), React.createElement( "div", - { className: `card-text${ link.image ? "" : " full-height" }` }, + { className: `card-text${link.image ? "" : " full-height"}` }, React.createElement( "h4", - { className: "card-title" }, + { className: "card-title", dir: "auto" }, " ", link.title, " " ), React.createElement( "p", - { className: "card-description" }, + { className: "card-description", dir: "auto" }, " ", link.description, " " @@ -1283,7 +1329,7 @@ class Card extends React.Component { React.createElement( "div", { className: "card-context" }, - React.createElement("span", { className: `card-context-icon icon icon-${ icon }` }), + React.createElement("span", { className: `card-context-icon icon icon-${icon}` }), React.createElement( "div", { className: "card-context-label" }, @@ -1296,23 +1342,20 @@ class Card extends React.Component { React.createElement( "button", { className: "context-menu-button", - onClick: e => { - e.preventDefault(); - this.toggleContextMenu(e, index); - } }, + onClick: this.onMenuButtonClick }, React.createElement( "span", { className: "sr-only" }, - `Open context menu for ${ link.title }` + `Open context menu for ${link.title}` ) ), React.createElement(LinkMenu, { dispatch: dispatch, - visible: isContextMenuOpen, - onUpdate: val => this.setState({ showContextMenu: val }), index: index, + onUpdate: this.onMenuUpdate, + options: link.context_menu_options || contextMenuOptions, site: link, - options: link.context_menu_options || contextMenuOptions }) + visible: isContextMenuOpen }) ); } } @@ -1337,6 +1380,10 @@ module.exports = { trending: { intlID: "type_label_recommended", icon: "trending" + }, + now: { + intlID: "type_label_now", + icon: "now" } }; @@ -1489,25 +1536,9 @@ class ContextMenu extends React.Component { window.removeEventListener("click", this.hideContext); } } - componentDidUnmount() { + componentWillUnmount() { window.removeEventListener("click", this.hideContext); } - onKeyDown(event, option) { - 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.hideContext(); - } - break; - case "Enter": - this.hideContext(); - option.onClick(); - break; - } - } render() { return React.createElement( "span", @@ -1515,32 +1546,59 @@ class ContextMenu extends React.Component { React.createElement( "ul", { role: "menu", className: "context-menu-list" }, - this.props.options.map((option, i) => { - if (option.type === "separator") { - return React.createElement("li", { key: i, className: "separator" }); - } - return React.createElement( - "li", - { role: "menuitem", className: "context-menu-item", key: i }, - React.createElement( - "a", - { tabIndex: "0", - onKeyDown: e => this.onKeyDown(e, option), - onClick: () => { - this.hideContext(); - option.onClick(); - } }, - option.icon && React.createElement("span", { className: `icon icon-spacer icon-${ option.icon }` }), - option.label - ) - ); - }) + 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.Component { + 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.option; + + 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.option; + + 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; /***/ }), /* 16 */ @@ -1549,6 +1607,84 @@ module.exports = ContextMenu; "use strict"; +const React = __webpack_require__(0); + +var _require = __webpack_require__(3); + +const connect = _require.connect; + +var _require2 = __webpack_require__(2); + +const FormattedMessage = _require2.FormattedMessage; + +var _require3 = __webpack_require__(1); + +const at = _require3.actionTypes, + ac = _require3.actionCreators; + +/** + * 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.Component { + 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-info" }), + React.createElement(FormattedMessage, { id: "manual_migration_explanation" }) + ), + React.createElement( + "div", + { className: "manual-migration-actions actions" }, + React.createElement( + "button", + { onClick: this.onCancelTour }, + React.createElement(FormattedMessage, { id: "manual_migration_cancel_button" }) + ), + React.createElement( + "button", + { className: "done", onClick: this.onLaunchTour }, + React.createElement(FormattedMessage, { id: "manual_migration_import_button" }) + ) + ) + ); + } +} + +module.exports = connect()(ManualMigration); +module.exports._unconnected = ManualMigration; + +/***/ }), +/* 17 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + const React = __webpack_require__(0); var _require = __webpack_require__(3); @@ -1621,7 +1757,7 @@ class PreferencesPane extends React.Component { "div", { className: "prefs-pane-button" }, React.createElement("button", { - className: `prefs-button icon ${ isVisible ? "icon-dismiss" : "icon-settings" }`, + 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 }) ), @@ -1630,7 +1766,7 @@ class PreferencesPane extends React.Component { { className: "prefs-pane" }, React.createElement( "div", - { className: `sidebar ${ isVisible ? "" : "hidden" }` }, + { className: `sidebar ${isVisible ? "" : "hidden"}` }, React.createElement( "div", { className: "prefs-modal-inner-wrapper" }, @@ -1671,7 +1807,7 @@ module.exports.PreferencesPane = PreferencesPane; module.exports.PreferencesInput = PreferencesInput; /***/ }), -/* 17 */ +/* 18 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -1770,7 +1906,7 @@ module.exports = connect()(injectIntl(Search)); module.exports._unconnected = Search; /***/ }), -/* 18 */ +/* 19 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -1789,7 +1925,7 @@ var _require2 = __webpack_require__(2); const FormattedMessage = _require2.FormattedMessage; const Card = __webpack_require__(12); -const Topics = __webpack_require__(20); +const Topics = __webpack_require__(21); class Section extends React.Component { render() { @@ -1817,7 +1953,7 @@ class Section extends React.Component { React.createElement( "h3", { className: "section-title" }, - React.createElement("span", { className: `icon icon-small-spacer icon-${ icon }` }), + React.createElement("span", { className: `icon icon-small-spacer icon-${icon}` }), React.createElement(FormattedMessage, title) ), infoOption && React.createElement( @@ -1861,7 +1997,7 @@ class Section extends React.Component { React.createElement( "div", { className: "empty-state" }, - React.createElement("img", { className: `empty-state-icon icon icon-${ emptyState.icon }` }), + React.createElement("img", { className: `empty-state-icon icon icon-${emptyState.icon}` }), React.createElement( "p", { className: "empty-state-message" }, @@ -1890,7 +2026,7 @@ module.exports._unconnected = Sections; module.exports.Section = Section; /***/ }), -/* 19 */ +/* 20 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -1920,17 +2056,30 @@ class TopSite extends React.Component { 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); } toggleContextMenu(event, index) { - this.setState({ showContextMenu: true, activeTile: index }); + this.setState({ + activeTile: index, + showContextMenu: true + }); } - trackClick() { + onLinkClick() { this.props.dispatch(ac.UserEvent({ event: "CLICK", source: TOP_SITES_SOURCE, action_position: this.props.index })); } + onMenuButtonClick(event) { + event.preventDefault(); + this.toggleContextMenu(event, this.props.index); + } + onMenuUpdate(showContextMenu) { + this.setState({ showContextMenu }); + } render() { var _props = this.props; const link = _props.link, @@ -1939,15 +2088,15 @@ class TopSite extends React.Component { const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index; const title = link.pinTitle || shortURL(link); - const screenshotClassName = `screenshot${ link.screenshot ? " active" : "" }`; - const topSiteOuterClassName = `top-site-outer${ isContextMenuOpen ? " active" : "" }`; - const style = { backgroundImage: link.screenshot ? `url(${ link.screenshot })` : "none" }; + const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`; + const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`; + const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" }; return React.createElement( "li", { className: topSiteOuterClassName, key: link.guid || link.url }, React.createElement( "a", - { onClick: () => this.trackClick(), href: link.url }, + { href: link.url, onClick: this.onLinkClick }, React.createElement( "div", { className: "tile", "aria-hidden": true }, @@ -1960,36 +2109,32 @@ class TopSite extends React.Component { ), React.createElement( "div", - { className: `title ${ link.isPinned ? "pinned" : "" }` }, + { className: `title ${link.isPinned ? "pinned" : ""}` }, link.isPinned && React.createElement("div", { className: "icon icon-pin-small" }), React.createElement( "span", - null, + { dir: "auto" }, title ) ) ), React.createElement( "button", - { className: "context-menu-button", - onClick: e => { - e.preventDefault(); - this.toggleContextMenu(e, index); - } }, + { className: "context-menu-button", onClick: this.onMenuButtonClick }, React.createElement( "span", { className: "sr-only" }, - `Open context menu for ${ title }` + `Open context menu for ${title}` ) ), React.createElement(LinkMenu, { dispatch: dispatch, - visible: isContextMenuOpen, - onUpdate: val => this.setState({ showContextMenu: val }), - site: link, index: index, + onUpdate: this.onMenuUpdate, + options: TOP_SITES_CONTEXT_MENU_OPTIONS, + site: link, source: TOP_SITES_SOURCE, - options: TOP_SITES_CONTEXT_MENU_OPTIONS }) + visible: isContextMenuOpen }) ); } } @@ -2000,6 +2145,7 @@ const TopSites = props => React.createElement( 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( @@ -2018,7 +2164,7 @@ module.exports._unconnected = TopSites; module.exports.TopSite = TopSite; /***/ }), -/* 20 */ +/* 21 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -2083,7 +2229,7 @@ module.exports._unconnected = Topics; module.exports.Topic = Topic; /***/ }), -/* 21 */ +/* 22 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -2194,7 +2340,7 @@ module.exports.CheckBookmark = site => site.bookmarkGuid ? module.exports.Remove module.exports.CheckPinTopSite = (site, index) => site.isPinned ? module.exports.UnpinTopSite(site) : module.exports.PinTopSite(site, index); /***/ }), -/* 22 */ +/* 23 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -2284,7 +2430,7 @@ _PerfService.prototype = { let entries = this.getEntriesByName(name, "mark"); if (!entries.length) { - throw new Error(`No marks with the name ${ name }`); + throw new Error(`No marks with the name ${name}`); } let mostRecentEntry = entries[entries.length - 1]; @@ -2299,7 +2445,7 @@ module.exports = { }; /***/ }), -/* 23 */ +/* 24 */ /***/ (function(module, exports) { var g; @@ -2326,13 +2472,13 @@ module.exports = g; /***/ }), -/* 24 */ +/* 25 */ /***/ (function(module, exports) { module.exports = Redux; /***/ }), -/* 25 */ +/* 26 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; diff --git a/browser/extensions/activity-stream/data/content/activity-stream.css b/browser/extensions/activity-stream/data/content/activity-stream.css index 320ae633d6a9..84337f0e8d3e 100644 --- a/browser/extensions/activity-stream/data/content/activity-stream.css +++ b/browser/extensions/activity-stream/data/content/activity-stream.css @@ -41,6 +41,8 @@ input { background-image: url("assets/glyph-delete-16.svg"); } .icon.icon-dismiss { background-image: url("assets/glyph-dismiss-16.svg"); } + .icon.icon-info { + background-image: url("assets/glyph-info-16.svg"); } .icon.icon-new-window { background-image: url("assets/glyph-newWindow-16.svg"); } .icon.icon-new-window-private { @@ -59,6 +61,8 @@ input { background-image: url("assets/glyph-trending-16.svg"); } .icon.icon-now { background-image: url("assets/glyph-now-16.svg"); } + .icon.icon-topsites { + background-image: url("assets/glyph-topsites-16.svg"); } .icon.icon-pin-small { background-image: url("assets/glyph-pin-12.svg"); background-size: 12px; @@ -344,7 +348,8 @@ main { height: 266px; display: flex; border: solid 1px rgba(0, 0, 0, 0.1); - border-radius: 3px; } + border-radius: 3px; + margin-bottom: 16px; } .sections-list .section-empty-state .empty-state { margin: auto; max-width: 350px; } @@ -397,7 +402,7 @@ main { cursor: default; display: flex; position: relative; - margin: 0 0 48px; + margin: 0 0 40px; width: 100%; height: 36px; } .search-wrapper input { @@ -757,3 +762,34 @@ main { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +.manual-migration-container { + background: rgba(215, 215, 219, 0.5); + font-size: 13px; + height: 50px; + border-radius: 2px; + margin-bottom: 40px; + display: flex; + justify-content: space-between; } + .manual-migration-container p { + margin: 0 4px 0 12px; + align-self: center; + display: flex; + justify-content: space-between; } + .manual-migration-container .icon { + margin: 0 12px 0 0; + align-self: center; } + +.manual-migration-actions { + display: flex; + justify-content: space-between; + border: none; + padding: 0; } + .manual-migration-actions button { + align-self: center; + padding: 0 12px; + height: 24px; + margin-inline-end: 0; + margin-right: 12px; + font-size: 13px; + width: 106px; } diff --git a/browser/extensions/activity-stream/data/content/assets/glyph-info-16.svg b/browser/extensions/activity-stream/data/content/assets/glyph-info-16.svg new file mode 100644 index 000000000000..e901fe96b6da --- /dev/null +++ b/browser/extensions/activity-stream/data/content/assets/glyph-info-16.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/extensions/activity-stream/data/content/assets/glyph-topsites-16.svg b/browser/extensions/activity-stream/data/content/assets/glyph-topsites-16.svg new file mode 100644 index 000000000000..5f917c255dc8 --- /dev/null +++ b/browser/extensions/activity-stream/data/content/assets/glyph-topsites-16.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/browser/extensions/activity-stream/data/locales.json b/browser/extensions/activity-stream/data/locales.json index 48bbce8f03da..b382313ba50b 100644 --- a/browser/extensions/activity-stream/data/locales.json +++ b/browser/extensions/activity-stream/data/locales.json @@ -3,7 +3,7 @@ "newtab_page_title": "Dirica matidi manyen", "default_label_loading": "Tye ka cano…", "header_top_sites": "Kakube maloyo", - "header_highlights": "Wiye madito", + "header_bookmarks_placeholder": "Pud i pee ki alamabuk.", "type_label_visited": "Kilimo", "type_label_bookmarked": "Kiketo alamabuk", "type_label_synced": "Kiribo ki i nyonyo mukene", @@ -17,6 +17,8 @@ "menu_action_open_private_window": "Yab i dirica manyen me mung", "menu_action_dismiss": "Kwer", "menu_action_delete": "Kwany ki ii gin mukato", + "menu_action_pin": "Mwon", + "menu_action_save_to_pocket": "Gwoki i jaba", "search_for_something_with": "Yeny pi {search_term} ki:", "search_button": "Yeny", "search_header": "Yeny me {search_engine_name}", @@ -37,8 +39,8 @@ "settings_pane_topsites_header": "Kakube ma gi loyo", "settings_pane_topsites_body": "Nong kakube ma ilimo loyo.", "settings_pane_topsites_options_showmore": "Nyut rek ariyo", - "settings_pane_highlights_header": "Wiye madito", - "settings_pane_highlights_body": "Nen angec i yeny mamegi mukato ki alamabukke ni ma i cweyo manyen.", + "settings_pane_bookmarks_header": "Alamabuk ma cocoki", + "settings_pane_visit_again_header": "Lim Kidoco", "settings_pane_done_button": "Otum", "edit_topsites_button_text": "Yubi", "edit_topsites_button_label": "Yub bute pi kakubi ni ma giloyo", @@ -47,7 +49,12 @@ "edit_topsites_done_button": "Otum", "edit_topsites_pin_button": "Mwon kakube man", "edit_topsites_edit_button": "Yub kakube man", - "edit_topsites_dismiss_button": "Kwer kakube man" + "edit_topsites_dismiss_button": "Kwer kakube man", + "edit_topsites_add_button": "Medi", + "topsites_form_edit_header": "Yub Kakube maloyo", + "topsites_form_add_button": "Medi", + "topsites_form_save_button": "Gwoki", + "topsites_form_cancel_button": "Kwer" }, "af": {}, "an": {}, @@ -55,8 +62,10 @@ "newtab_page_title": "لسان جديد", "default_label_loading": "يُحمّل…", "header_top_sites": "المواقع الأكثر زيارة", - "header_highlights": "أهم الأحداث", "header_stories": "أهم الأخبار", + "header_visit_again": "زرها مجددا", + "header_bookmarks": "أحدث العلامات", + "header_bookmarks_placeholder": "لا علامات لديك بعد.", "header_stories_from": "من", "type_label_visited": "مُزارة", "type_label_bookmarked": "معلّمة", @@ -72,6 +81,10 @@ "menu_action_open_private_window": "افتح في نافذة خاصة جديدة", "menu_action_dismiss": "ألغِ", "menu_action_delete": "احذف من التأريخ", + "menu_action_pin": "ثبّت", + "menu_action_unpin": "أزل", + "confirm_history_delete_p1": "هل أنت متأكد أنك تريد حذف كل وجود لهذه الصفحة من تأريخك؟", + "confirm_history_delete_notice_p2": "لا يمكن التراجع عن هذا الإجراء.", "menu_action_save_to_pocket": "احفظ في Pocket", "search_for_something_with": "ابحث عن {search_term} مستخدما:", "search_button": "ابحث", @@ -93,8 +106,10 @@ "settings_pane_topsites_header": "المواقع الأكثر زيارة", "settings_pane_topsites_body": "وصول للمواقع التي تزورها أكثر.", "settings_pane_topsites_options_showmore": "اعرض صفّين", - "settings_pane_highlights_header": "أهم الأحداث", - "settings_pane_highlights_body": "اطّلع على تأريخ التصفح الأحدث، و العلامات المنشأة حديثًا.", + "settings_pane_bookmarks_header": "أحدث العلامات", + "settings_pane_bookmarks_body": "علاماتك المعلّمة حديثًا في مكان واحد.", + "settings_pane_visit_again_header": "زرها مجددا", + "settings_pane_visit_again_body": "سيعرض لك فَيَرفُكس بعضًا من تأريخ تصفحك الذي قد تود تذكّره لاحقًا.", "settings_pane_pocketstories_header": "أهم المواضيع", "settings_pane_pocketstories_body": "يساعدك Pocket –عضو في أسرة موزيلا– على الوصول إلى محتوى عالِ الجودة ربما لم يُكن ليتاح لك بدونه.", "settings_pane_done_button": "تمّ", @@ -496,6 +511,7 @@ "settings_pane_topsites_options_showmore": "Mostra dues files", "settings_pane_bookmarks_header": "Adreces d'interès recents", "settings_pane_bookmarks_body": "Les adreces d'interès que aneu creant, en un lloc còmode.", + "settings_pane_visit_again_header": "Torneu a visitar", "settings_pane_visit_again_body": "El Firefox us mostrarà parts del vostre historial de navegació que potser us agradaria recordar o tornar a visitar.", "settings_pane_pocketstories_header": "Articles populars", "settings_pane_pocketstories_body": "El Pocket, membre de la família Mozilla, us permet accedir a contingut d'alta qualitat que d'altra manera potser no trobaríeu.", @@ -600,7 +616,7 @@ "topsites_form_url_validation": "Je vyžadována platná URL", "pocket_read_more": "Populární témata:", "pocket_read_even_more": "Zobrazit více příběhů", - "pocket_feedback_header": "To nejlepší na webu podle hodnocení více než 25 miliony lidí.", + "pocket_feedback_header": "To nejlepší na webu podle hodnocení více než 25 milionů lidí.", "pocket_feedback_body": "Pocket, služba od Mozilly, vám pomůže najít vysoce kvalitní obsah, který byste jinak neobjevili.", "pocket_send_feedback": "Odeslat zpětnou vazbu" }, @@ -608,10 +624,15 @@ "newtab_page_title": "Tab Newydd", "default_label_loading": "Llwytho…", "header_top_sites": "Hoff Wefannau", - "header_highlights": "Goreuon", + "header_stories": "Hoff Straeon", + "header_visit_again": "Ymweld Eto", + "header_bookmarks": "Nodau Tudalen Diweddar", + "header_bookmarks_placeholder": "Nid oes gennych unrhyw nodau tudalen eto.", + "header_stories_from": "oddi wrth", "type_label_visited": "Ymwelwyd", "type_label_bookmarked": "Nod Tudalen", "type_label_synced": "Cydweddwyd o ddyfais arall", + "type_label_recommended": "Trendio", "type_label_open": "Ar Agor", "type_label_topic": "Pwnc", "menu_action_bookmark": "Nod Tudalen", @@ -622,6 +643,11 @@ "menu_action_open_private_window": "Agor mewn Ffenestr Preifat Newydd", "menu_action_dismiss": "Cau", "menu_action_delete": "Dileu o'r Hanes", + "menu_action_pin": "Pinio", + "menu_action_unpin": "Dad-binio", + "confirm_history_delete_p1": "Ydych chi'n siŵr eich bod chi am ddileu pob enghraifft o'r dudalen hon o'ch hanes?", + "confirm_history_delete_notice_p2": "Nid oes modd dadwneud hyn.", + "menu_action_save_to_pocket": "Cadw i Pocket", "search_for_something_with": "Chwilio am {search_term} gyda:", "search_button": "Chwilio", "search_header": "{search_engine_name} Chwilio", @@ -642,8 +668,12 @@ "settings_pane_topsites_header": "Hoff Wefannau", "settings_pane_topsites_body": "Cael mynediad at y gwefannau rydych yn ymweld â nhw amlaf.", "settings_pane_topsites_options_showmore": "Dangos dwy res", - "settings_pane_highlights_header": "Goreuon", - "settings_pane_highlights_body": "Edrych nôl ar eich hanes pori a nodau tudalen diweddar.", + "settings_pane_bookmarks_header": "Nodau Tudalen Diweddar", + "settings_pane_bookmarks_body": "Eich nodau tudalen diweddaraf mewn un lleoliad hwylus.", + "settings_pane_visit_again_header": "Ymweld Eto", + "settings_pane_visit_again_body": "Gall Firefox ddangos i chi rannau o'ch hanes pori yr hoffech eu cofio neu fynd nôl atyn nhw.", + "settings_pane_pocketstories_header": "Hoff Straeon", + "settings_pane_pocketstories_body": "Gall Pocket, sy'n rhan o deulu Mozilla, eich helpu i ganfod cynnwys o ansawdd uchel na fyddech wedi eu canfod fel arall.", "settings_pane_done_button": "Gorffen", "edit_topsites_button_text": "Golygu", "edit_topsites_button_label": "Cyfaddasu eich adran Hoff Wefannau", @@ -651,8 +681,23 @@ "edit_topsites_showless_button": "Dangos llai", "edit_topsites_done_button": "Gorffen", "edit_topsites_pin_button": "Pinio'r wefan", + "edit_topsites_unpin_button": "Dad-binio'r wefan", "edit_topsites_edit_button": "Golygu'r wefan", - "edit_topsites_dismiss_button": "Dileu'r wefan" + "edit_topsites_dismiss_button": "Dileu'r wefan", + "edit_topsites_add_button": "Ychwanegu", + "topsites_form_add_header": "Hoff Wefan Newydd", + "topsites_form_edit_header": "Golygu'r Hoff Wefan", + "topsites_form_title_placeholder": "Rhoi teitl", + "topsites_form_url_placeholder": "Teipio neu ludo URL", + "topsites_form_add_button": "Ychwanegu", + "topsites_form_save_button": "Cadw", + "topsites_form_cancel_button": "Diddymu", + "topsites_form_url_validation": "Mae angen URL Ddilys", + "pocket_read_more": "Pynciau Poblogaidd:", + "pocket_read_even_more": "Gweld Rhagor o Straeon", + "pocket_feedback_header": "Y gorau o'r we, wedi ei gasglu gan dros 25 miliwn o bobl.", + "pocket_feedback_body": "Gall Pocket, sy'n rhan o deulu Mozilla, eich helpu i ganfod cynnwys o ansawdd uchel na fyddech wedi eu canfod fel arall.", + "pocket_send_feedback": "Anfon Adborth" }, "da": { "newtab_page_title": "Nyt faneblad", @@ -703,7 +748,9 @@ "settings_pane_topsites_body": "Adgang til de websider, du besøger oftest.", "settings_pane_topsites_options_showmore": "Vis to rækker", "settings_pane_bookmarks_header": "Seneste bogmærker", + "settings_pane_bookmarks_body": "Dine seneste bogmærker samlet ét sted.", "settings_pane_visit_again_header": "Besøg igen", + "settings_pane_visit_again_body": "Firefox viser dig dele af din browserhistorik, som du måske vil huske på eller vende tilbage til.", "settings_pane_pocketstories_header": "Tophistorier", "settings_pane_pocketstories_body": "Pocket, en del af Mozilla-familien, hjælper dig med at opdage indhold af høj kvalitet, som du måske ellers ikke ville have fundet.", "settings_pane_done_button": "Færdig", @@ -972,10 +1019,15 @@ "newtab_page_title": "New Tab", "default_label_loading": "Loading…", "header_top_sites": "Top Sites", - "header_highlights": "Highlights", + "header_stories": "Top Stories", + "header_visit_again": "Visit Again", + "header_bookmarks": "Recent Bookmarks", + "header_bookmarks_placeholder": "You don’t have any bookmarks yet.", + "header_stories_from": "from", "type_label_visited": "Visited", "type_label_bookmarked": "Bookmarked", "type_label_synced": "Synchronised from another device", + "type_label_recommended": "Trending", "type_label_open": "Open", "type_label_topic": "Topic", "menu_action_bookmark": "Bookmark", @@ -986,6 +1038,11 @@ "menu_action_open_private_window": "Open in a New Private Window", "menu_action_dismiss": "Dismiss", "menu_action_delete": "Delete from History", + "menu_action_pin": "Pin", + "menu_action_unpin": "Unpin", + "confirm_history_delete_p1": "Are you sure you want to delete every instance of this page from your history?", + "confirm_history_delete_notice_p2": "This action cannot be undone.", + "menu_action_save_to_pocket": "Save to Pocket", "search_for_something_with": "Search for {search_term} with:", "search_button": "Search", "search_header": "{search_engine_name} Search", @@ -1006,8 +1063,12 @@ "settings_pane_topsites_header": "Top Sites", "settings_pane_topsites_body": "Access the web sites you visit most.", "settings_pane_topsites_options_showmore": "Show two rows", - "settings_pane_highlights_header": "Highlights", - "settings_pane_highlights_body": "Look back at your recent browsing history and newly created bookmarks.", + "settings_pane_bookmarks_header": "Recent Bookmarks", + "settings_pane_bookmarks_body": "Your newly created bookmarks in one handy location.", + "settings_pane_visit_again_header": "Visit Again", + "settings_pane_visit_again_body": "Firefox will show you parts of your browsing history that you might want to remember or get back to.", + "settings_pane_pocketstories_header": "Top Stories", + "settings_pane_pocketstories_body": "Pocket, a part of the Mozilla family, will help connect you to high-quality content that you may not have found otherwise.", "settings_pane_done_button": "Done", "edit_topsites_button_text": "Edit", "edit_topsites_button_label": "Customise your Top Sites section", @@ -1015,8 +1076,23 @@ "edit_topsites_showless_button": "Show less", "edit_topsites_done_button": "Done", "edit_topsites_pin_button": "Pin this site", + "edit_topsites_unpin_button": "Unpin this site", "edit_topsites_edit_button": "Edit this site", - "edit_topsites_dismiss_button": "Dismiss this site" + "edit_topsites_dismiss_button": "Dismiss this site", + "edit_topsites_add_button": "Add", + "topsites_form_add_header": "Top Sites", + "topsites_form_edit_header": "Edit Top Site", + "topsites_form_title_placeholder": "Enter a title", + "topsites_form_url_placeholder": "Type or paste a URL", + "topsites_form_add_button": "Add", + "topsites_form_save_button": "Save", + "topsites_form_cancel_button": "Cancel", + "topsites_form_url_validation": "Valid URL required", + "pocket_read_more": "Popular Topics:", + "pocket_read_even_more": "View More Stories", + "pocket_feedback_header": "The best of the web, curated by over 25 million people.", + "pocket_feedback_body": "Pocket, a part of the Mozilla family, will help connect you to high-quality content that you may not have found otherwise.", + "pocket_send_feedback": "Send Feedback" }, "en-US": { "newtab_page_title": "New Tab", @@ -1034,6 +1110,7 @@ "type_label_recommended": "Trending", "type_label_open": "Open", "type_label_topic": "Topic", + "type_label_now": "Now", "menu_action_bookmark": "Bookmark", "menu_action_remove_bookmark": "Remove Bookmark", "menu_action_copy_address": "Copy Address", @@ -1098,17 +1175,25 @@ "pocket_feedback_header": "The best of the web, curated by over 25 million people.", "pocket_feedback_body": "Pocket, a part of the Mozilla family, will help connect you to high-quality content that you may not have found otherwise.", "pocket_send_feedback": "Send Feedback", - "empty_state_topstories": "You’ve caught up. Check back later for more top stories from Pocket. Can’t wait? Select a popular topic to find more great stories from around the web." + "topstories_empty_state": "You’ve caught up. Check back later for more top stories from {provider}. Can’t wait? Select a popular topic to find more great stories from around the web.", + "manual_migration_explanation": "Try Firefox with your favorite sites and bookmarks from another browser.", + "manual_migration_cancel_button": "No Thanks", + "manual_migration_import_button": "Import Now" }, "en-ZA": {}, "eo": { "newtab_page_title": "Nova legosigno", "default_label_loading": "Ŝargado…", "header_top_sites": "Plej vizititaj", - "header_highlights": "Elstaraĵoj", + "header_stories": "Ĉefaj artikoloj", + "header_visit_again": "Viziti denove", + "header_bookmarks": "Ĵusaj legosignoj", + "header_bookmarks_placeholder": "Vi ankoraŭ ne havas legosignojn.", + "header_stories_from": "el", "type_label_visited": "Vizititaj", "type_label_bookmarked": "Kun legosigno", "type_label_synced": "Spegulitaj el alia aparato", + "type_label_recommended": "Tendencoj", "type_label_open": "Malfermita", "type_label_topic": "Temo", "menu_action_bookmark": "Aldoni legosignon", @@ -1119,6 +1204,11 @@ "menu_action_open_private_window": "Malfermi en nova privata fenestro", "menu_action_dismiss": "Ignori", "menu_action_delete": "Forigi el historio", + "menu_action_pin": "Alpingli", + "menu_action_unpin": "Depingli", + "confirm_history_delete_p1": "Ĉu vi certe volas forigi ĉiun aperon de tiu ĉi paĝo el via historio?", + "confirm_history_delete_notice_p2": "Tiu ĉi ago ne estas malfarebla.", + "menu_action_save_to_pocket": "Konservi en Pocket", "search_for_something_with": "Serĉi {search_term} per:", "search_button": "Serĉi", "search_header": "Serĉo de {search_engine_name}", @@ -1131,7 +1221,44 @@ "time_label_minute": "{number}m", "time_label_hour": "{number}h", "time_label_day": "{number}t", - "settings_pane_button_label": "Personecigi la paĝon por novaj langetoj" + "settings_pane_button_label": "Personecigi la paĝon por novaj langetoj", + "settings_pane_header": "Preferoj pri nova langeto", + "settings_pane_body": "Elekti tion, kio estos videbla je malfermo de nova langeto.", + "settings_pane_search_header": "Serĉi", + "settings_pane_search_body": "Serĉi la Teksaĵon el via nova langeto.", + "settings_pane_topsites_header": "Plej vizitaj", + "settings_pane_topsites_body": "Aliri la plej ofte vizitajn retejojn.", + "settings_pane_topsites_options_showmore": "Montri en du vicoj", + "settings_pane_bookmarks_header": "Ĵusaj legosignoj", + "settings_pane_bookmarks_body": "Viaj ĵus kreitaj legosignoj, ĉemane.", + "settings_pane_visit_again_header": "Viziti denove", + "settings_pane_visit_again_body": "Firefoĉ montros al vi partojn de via retuma historio, kiujn vi eble volas memori aŭ viziti denove.", + "settings_pane_pocketstories_header": "Ĉefaj artikoloj", + "settings_pane_pocketstories_body": "Pocket, parto de la familio de Mozilla, helpos vin trovi altkvalitan enhavon, kiun vi eble ne trovos aliloke.", + "settings_pane_done_button": "Farita", + "edit_topsites_button_text": "Redakti", + "edit_topsites_button_label": "Personecigi la sekcion 'plej vizititaj'", + "edit_topsites_showmore_button": "Montri pli", + "edit_topsites_showless_button": "Montri malpli", + "edit_topsites_done_button": "Farita", + "edit_topsites_pin_button": "Alpingli ĉi tiun retejon", + "edit_topsites_unpin_button": "Depingli tiun ĉi retejon", + "edit_topsites_edit_button": "Redakti ĉi tiun retejon", + "edit_topsites_dismiss_button": "Ignori ĉi tiun retejon", + "edit_topsites_add_button": "Aldoni", + "topsites_form_add_header": "Nova ofta retejo", + "topsites_form_edit_header": "Redakti ofta retejo", + "topsites_form_title_placeholder": "Tajpu titolon", + "topsites_form_url_placeholder": "Tajpu aŭ alguu retadreson", + "topsites_form_add_button": "Aldoni", + "topsites_form_save_button": "Konservi", + "topsites_form_cancel_button": "Nuligi", + "topsites_form_url_validation": "Valida retadreso estas postulata", + "pocket_read_more": "Ĉefaj temoj:", + "pocket_read_even_more": "Montri pli da artikoloj", + "pocket_feedback_header": "La plejbono el la Teksaĵo, reviziita de pli ol 25 milionoj da personoj.", + "pocket_feedback_body": "Pocket, parto de la familio de Mozilla, helpos vin trovi altkvalitan enhavon, kiun vi eble ne trovos aliloke.", + "pocket_send_feedback": "Sendi komentojn" }, "es-AR": { "newtab_page_title": "Nueva pestaña", @@ -2027,10 +2154,15 @@ "newtab_page_title": "לשונית חדשה", "default_label_loading": "בטעינה…", "header_top_sites": "אתרים מובילים", - "header_highlights": "המלצות", + "header_stories": "סיפורים מובילים", + "header_visit_again": "ביקור חוזר", + "header_bookmarks": "סימניות אחרונות", + "header_bookmarks_placeholder": "אין לך סימניות עדיין.", + "header_stories_from": "מאת", "type_label_visited": "ביקורים קודמים", "type_label_bookmarked": "נוצרה סימניה", "type_label_synced": "סונכרן מהתקן אחר", + "type_label_recommended": "פופולרי", "type_label_open": "פתיחה", "type_label_topic": "נושא", "menu_action_bookmark": "הוספת סימניה", @@ -2039,8 +2171,13 @@ "menu_action_email_link": "שליחת קישור בדוא״ל…", "menu_action_open_new_window": "פתיחה בחלון חדש", "menu_action_open_private_window": "פתיחה בלשונית פרטית חדשה", - "menu_action_dismiss": "ביטול", + "menu_action_dismiss": "הסרה", "menu_action_delete": "מחיקה מההיסטוריה", + "menu_action_pin": "הצמדה", + "menu_action_unpin": "ביטול הצמדה", + "confirm_history_delete_p1": "למחוק כל עותק של העמוד הזה מההיסטוריה שלך?", + "confirm_history_delete_notice_p2": "לא ניתן לבטל פעולה זו.", + "menu_action_save_to_pocket": "שמירה ל־Pocket", "search_for_something_with": "חיפוש אחר {search_term} עם:", "search_button": "חיפוש", "search_header": "חיפוש ב־{search_engine_name}", @@ -2061,8 +2198,12 @@ "settings_pane_topsites_header": "אתרים מובילים", "settings_pane_topsites_body": "גישה לאתרים בהם ביקרת הכי הרבה.", "settings_pane_topsites_options_showmore": "הצגת שתי שורות", - "settings_pane_highlights_header": "המלצות", - "settings_pane_highlights_body": "ניתן להסתכל על היסטוריית הגלישה העדכנית שלך ועל הסימניות האחרונות שנוצרו.", + "settings_pane_bookmarks_header": "סימניות אחרונות", + "settings_pane_bookmarks_body": "הסימניות החדשות שיצרת במיקום נוח ואחיד.", + "settings_pane_visit_again_header": "ביקור חוזר", + "settings_pane_visit_again_body": "Firefox תציג לך חלקים מהיסטוריית הגלישה שלך שאולי יעניין אותך להיזכר בהם או לחזור אליהם.", + "settings_pane_pocketstories_header": "סיפורים מובילים", + "settings_pane_pocketstories_body": "Pocket, חלק ממשפחת Mozilla, יסייע לך להתחבר לתוכן באיכות גבוהה שיתכן שלא היה מגיע אליך בדרך אחרת.", "settings_pane_done_button": "סיום", "edit_topsites_button_text": "עריכה", "edit_topsites_button_label": "התאמת אגף האתרים המובילים שלך", @@ -2070,8 +2211,23 @@ "edit_topsites_showless_button": "להציג פחות", "edit_topsites_done_button": "בוצע", "edit_topsites_pin_button": "נעיצת אתר זה", + "edit_topsites_unpin_button": "ביטול הצמדת אתר זה", "edit_topsites_edit_button": "עריכת אתר זה", - "edit_topsites_dismiss_button": "התעלמות מאתר זה" + "edit_topsites_dismiss_button": "התעלמות מאתר זה", + "edit_topsites_add_button": "הוספה", + "topsites_form_add_header": "אתר מוביל חדש", + "topsites_form_edit_header": "עריכת אתר מוביל", + "topsites_form_title_placeholder": "נא להזין כותרת", + "topsites_form_url_placeholder": "נא להקליד או להזין כתובת", + "topsites_form_add_button": "הוספה", + "topsites_form_save_button": "שמירה", + "topsites_form_cancel_button": "ביטול", + "topsites_form_url_validation": "נדרשת כתובת תקינה", + "pocket_read_more": "נושאים פופולריים:", + "pocket_read_even_more": "צפייה בחדשות נוספות", + "pocket_feedback_header": "המיטב מרחבי האינטרנט, נאסף על ידי 25 מיליון אנשים.", + "pocket_feedback_body": "Pocket, חלק ממשפחת Mozilla, יסייע לך להתחבר לתוכן באיכות גבוהה שיתכן שלא היה מגיע אליך בדרך אחרת.", + "pocket_send_feedback": "שליחת משוב" }, "hi-IN": { "newtab_page_title": "नया टैब", @@ -3038,8 +3194,10 @@ "newtab_page_title": "Nauja kortelė", "default_label_loading": "Įkeliama…", "header_top_sites": "Lankomiausios svetainės", - "header_highlights": "Akcentai", "header_stories": "Populiariausi straipsniai", + "header_visit_again": "Aplankykite vėl", + "header_bookmarks": "Paskiausi adresyno įrašai", + "header_bookmarks_placeholder": "Jūs dar neturite adresyno įrašų.", "header_stories_from": "iš", "type_label_visited": "Aplankyti", "type_label_bookmarked": "Adresyne", @@ -3055,6 +3213,10 @@ "menu_action_open_private_window": "Atverti naujame privačiajame lange", "menu_action_dismiss": "Paslėpti", "menu_action_delete": "Pašalinti iš istorijos", + "menu_action_pin": "Įsegti", + "menu_action_unpin": "Išsegti", + "confirm_history_delete_p1": "Ar tikrai norite pašalinti visus šio tinklalapio įrašus iš savo naršymo žurnalo?", + "confirm_history_delete_notice_p2": "Atlikus šį veiksmą, jo atšaukti neįmanoma.", "menu_action_save_to_pocket": "Įrašyti į „Pocket“", "search_for_something_with": "Ieškoti „{search_term}“ per:", "search_button": "Ieškoti", @@ -3076,8 +3238,10 @@ "settings_pane_topsites_header": "Lankomiausios svetainės", "settings_pane_topsites_body": "Pasiekite jūsų dažniausiai lankomas svetaines.", "settings_pane_topsites_options_showmore": "Rodyti dvi eilutes", - "settings_pane_highlights_header": "Akcentai", - "settings_pane_highlights_body": "Pažvelkite į savo naujausią naršymo istoriją bei paskiausiai pridėtus adresyno įrašus.", + "settings_pane_bookmarks_header": "Paskiausi adresyno įrašai", + "settings_pane_bookmarks_body": "Jūsų naujai sukurti adresyno įrašai vienoje vietoje.", + "settings_pane_visit_again_header": "Aplankykite vėl", + "settings_pane_visit_again_body": "„Firefox“ pateiks ištraukas iš jūsų naršymo žurnalo, kurias galbūt norėtumėte prisiminti.", "settings_pane_pocketstories_header": "Populiariausi straipsniai", "settings_pane_pocketstories_body": "„Pocket“, „Mozillos“ šeimos dalis, padės jums atrasti kokybišką turinį, kurio kitaip gal nebūtumėte radę.", "settings_pane_done_button": "Atlikta", @@ -3846,7 +4010,7 @@ "menu_action_open_new_window": "Abrir em nova janela", "menu_action_open_private_window": "Abrir em nova janela privada", "menu_action_dismiss": "Dispensar", - "menu_action_delete": "Eliminar do histórico", + "menu_action_delete": "Apagar do histórico", "menu_action_pin": "Afixar", "menu_action_unpin": "Desafixar", "confirm_history_delete_p1": "Tem a certeza de que deseja apagar todas as instâncias desta página do seu histórico?", @@ -3865,9 +4029,9 @@ "time_label_hour": "{number}h", "time_label_day": "{number}d", "settings_pane_button_label": "Personalizar a sua página de novo separador", - "settings_pane_header": "Novas preferências de separador", - "settings_pane_body": "Escolha o que ver quando abre um novo separador.", - "settings_pane_search_header": "Pesquisar", + "settings_pane_header": "Preferências de novo separador", + "settings_pane_body": "Escolha o que vê quando abre um novo separador.", + "settings_pane_search_header": "Pesquisa", "settings_pane_search_body": "Pesquise na Web a partir do seu novo separador.", "settings_pane_topsites_header": "Sites mais visitados", "settings_pane_topsites_body": "Aceda aos websites que mais visita.", @@ -4999,7 +5163,41 @@ "pocket_send_feedback": "جواب الجواب ارسال کریں" }, "uz": {}, - "vi": {}, + "vi": { + "newtab_page_title": "Tab mới", + "default_label_loading": "Đang tải…", + "header_top_sites": "Trang web hàng đầu", + "header_stories": "Câu chuyện hàng đầu", + "header_visit_again": "Truy cập lại", + "header_bookmarks": "Các bookmark gần đây", + "header_bookmarks_placeholder": "Bạn chưa có bookmark nào.", + "header_stories_from": "từ", + "type_label_visited": "Đã truy cập", + "type_label_bookmarked": "Đã được đánh dấu", + "type_label_synced": "Đồng bộ từ thiết bị khác", + "type_label_recommended": "Xu hướng", + "type_label_open": "Mở", + "type_label_topic": "Chủ đề", + "menu_action_bookmark": "Đánh dấu", + "menu_action_remove_bookmark": "Xóa đánh dấu", + "menu_action_copy_address": "Chép địa chỉ", + "menu_action_email_link": "Liên kết Email...", + "menu_action_open_new_window": "Mở trong Cửa Sổ Mới", + "menu_action_open_private_window": "Mở trong cửa sổ riêng tư mới", + "menu_action_dismiss": "Bỏ qua", + "menu_action_delete": "Xóa từ lịch xử", + "menu_action_pin": "Ghim", + "menu_action_unpin": "Bỏ ghim", + "confirm_history_delete_notice_p2": "Hành động này không thể hoàn tác.", + "menu_action_save_to_pocket": "Lưu vào Pocket", + "search_for_something_with": "Tìm {search_term} với:", + "search_button": "Tìm kiếm", + "search_header": "Công cụ tìm kiếm {search_engine_name}", + "time_label_less_than_minute": "<1phút", + "time_label_minute": "{number}phút", + "time_label_hour": "{number}giờ", + "settings_pane_search_header": "Tìm kiếm" + }, "wo": {}, "xh": {}, "zh-CN": { diff --git a/browser/extensions/activity-stream/lib/ActivityStream.jsm b/browser/extensions/activity-stream/lib/ActivityStream.jsm index ba81e17f29a9..2fd01c1ba4a4 100644 --- a/browser/extensions/activity-stream/lib/ActivityStream.jsm +++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm @@ -10,6 +10,7 @@ const {utils: Cu} = Components; const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {}); const {DefaultPrefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {}); const {LocalizationFeed} = Cu.import("resource://activity-stream/lib/LocalizationFeed.jsm", {}); +const {ManualMigration} = Cu.import("resource://activity-stream/lib/ManualMigration.jsm", {}); const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {}); const {PlacesFeed} = Cu.import("resource://activity-stream/lib/PlacesFeed.jsm", {}); const {PrefsFeed} = Cu.import("resource://activity-stream/lib/PrefsFeed.jsm", {}); @@ -81,6 +82,18 @@ const PREFS_CONFIG = new Map([ "provider_name": "Pocket", "provider_icon": "pocket" }` + }], + ["migrationExpired", { + title: "Boolean flag that decides whether to show the migration message or not.", + value: false + }], + ["migrationRemainingDays", { + title: "Number of days to show the manual migration message", + value: 4 + }], + ["migrationLastShownDate", { + title: "Timestamp when migration message was last shown. In seconds.", + value: 0 }] ]); @@ -133,6 +146,12 @@ for (const {name, factory, title, value} of SECTION_FEEDS_CONFIG.concat([ factory: () => new TopSitesFeed(), title: "Queries places and gets metadata for Top Sites section", value: true + }, + { + name: "migration", + factory: () => new ManualMigration(), + title: "Manual migration wizard", + value: true } ])) { const pref = `feeds.${name}`; diff --git a/browser/extensions/activity-stream/lib/ManualMigration.jsm b/browser/extensions/activity-stream/lib/ManualMigration.jsm new file mode 100644 index 000000000000..a2b254d254fe --- /dev/null +++ b/browser/extensions/activity-stream/lib/ManualMigration.jsm @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {utils: Cu} = Components; +const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {}); +const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {}); +const MIGRATION_ENDED_EVENT = "Migration:Ended"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils", "resource:///modules/MigrationUtils.jsm"); + +this.ManualMigration = class ManualMigration { + constructor() { + Services.obs.addObserver(this, MIGRATION_ENDED_EVENT); + this._prefs = new Prefs(); + } + + uninit() { + Services.obs.removeObserver(this, MIGRATION_ENDED_EVENT); + } + + isMigrationMessageExpired() { + let migrationLastShownDate = new Date(this._prefs.get("migrationLastShownDate") * 1000); + let today = new Date(); + // Round down to midnight. + today = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + + if (migrationLastShownDate < today) { + let migrationRemainingDays = this._prefs.get("migrationRemainingDays") - 1; + + this._prefs.set("migrationRemainingDays", migrationRemainingDays); + // .valueOf returns a value that is too large to store so we need to divide by 1000. + this._prefs.set("migrationLastShownDate", today.valueOf() / 1000); + + if (migrationRemainingDays <= 0) { + return true; + } + } + + return false; + } + + /** + * While alreadyExpired is false the migration message is displayed and we also + * keep checking if we should expire it. Broadcast expiration to store. + * + * @param {bool} alreadyExpired Pref flag that is false for the first 3 active days, + * time in which we display the migration message to the user. + */ + expireIfNecessary(alreadyExpired) { + if (!alreadyExpired && this.isMigrationMessageExpired()) { + this.expireMigration(); + } + } + + expireMigration() { + this.store.dispatch(ac.SetPref("migrationExpired", true)); + } + + /** + * Event listener for migration wizard completion event. + */ + observe() { + this.expireMigration(); + } + + onAction(action) { + switch (action.type) { + case at.PREFS_INITIAL_VALUES: + this.expireIfNecessary(action.data.migrationExpired); + break; + case at.MIGRATION_START: + MigrationUtils.showMigrationWizard(action._target.browser.ownerGlobal, [MigrationUtils.MIGRATION_ENTRYPOINT_NEWTAB]); + break; + case at.MIGRATION_CANCEL: + this.expireMigration(); + break; + case at.UNINIT: + this.uninit(); + break; + } + } +}; + +this.EXPORTED_SYMBOLS = ["ManualMigration"]; diff --git a/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm b/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm index 57952968cc8a..8231909ac900 100644 --- a/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm +++ b/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm @@ -14,6 +14,7 @@ const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.js const STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours +const STORIES_NOW_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours const SECTION_ID = "TopStories"; this.TopStoriesFeed = class TopStoriesFeed { @@ -38,7 +39,7 @@ this.TopStoriesFeed = class TopStoriesFeed { title: {id: "header_recommended_by", values: {provider: options.provider_name}}, rows: [], maxCards: 3, - contextMenuOptions: ["SaveToPocket", "Separator", "CheckBookmark", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"], + contextMenuOptions: ["CheckBookmark", "SaveToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"], infoOption: { header: {id: "pocket_feedback_header"}, body: {id: "pocket_feedback_body"}, @@ -48,7 +49,7 @@ this.TopStoriesFeed = class TopStoriesFeed { } }, emptyState: { - message: {id: "empty_state_topstories"}, + message: {id: "topstories_empty_state", values: {provider: options.provider_name}}, icon: "check" } }; @@ -77,15 +78,14 @@ this.TopStoriesFeed = class TopStoriesFeed { .then(body => { let items = JSON.parse(body).list; items = items - .filter(s => !NewTabUtils.blockedLinks.isBlocked(s.dedupe_url)) + .filter(s => !NewTabUtils.blockedLinks.isBlocked({"url": s.dedupe_url})) .map(s => ({ "guid": s.id, - "type": "trending", + "type": (Date.now() - (s.published_timestamp * 1000)) <= STORIES_NOW_THRESHOLD ? "now" : "trending", "title": s.title, "description": s.excerpt, "image": this._normalizeUrl(s.image_src), - "url": s.dedupe_url, - "lastVisitDate": s.published_timestamp + "url": s.dedupe_url })); return items; }) diff --git a/browser/extensions/activity-stream/test/.eslintrc.js b/browser/extensions/activity-stream/test/.eslintrc.js index 807cc99ad6ee..d21144823412 100644 --- a/browser/extensions/activity-stream/test/.eslintrc.js +++ b/browser/extensions/activity-stream/test/.eslintrc.js @@ -8,5 +8,8 @@ module.exports = { "assert": true, "sinon": true, "chai": true + }, + "rules": { + "react/jsx-no-bind": 0 } }; diff --git a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js index 60d796bf7199..edee9a337346 100644 --- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js +++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js @@ -251,6 +251,66 @@ describe("Reducers", () => { assert.deepEqual(section.rows, [{url: "www.other.url"}]); }); }); + it("should not update state for empty action.data on PLACES_BOOKMARK_ADDED", () => { + const nextState = Sections(undefined, {type: at.PLACES_BOOKMARK_ADDED}); + assert.equal(nextState, INITIAL_STATE.Sections); + }); + it("should bookmark an item when PLACES_BOOKMARK_ADDED is received", () => { + const action = { + type: at.PLACES_BOOKMARK_ADDED, + data: { + url: "www.foo.bar", + bookmarkGuid: "bookmark123", + bookmarkTitle: "Title for bar.com", + lastModified: 1234567 + } + }; + const nextState = Sections(oldState, action); + // check a section to ensure the correct url was bookmarked + const newRow = nextState[0].rows[0]; + const oldRow = nextState[0].rows[1]; + + // new row has bookmark data + assert.equal(newRow.url, action.data.url); + assert.equal(newRow.bookmarkGuid, action.data.bookmarkGuid); + assert.equal(newRow.bookmarkTitle, action.data.bookmarkTitle); + assert.equal(newRow.bookmarkDateCreated, action.data.lastModified); + + // old row is unchanged + assert.equal(oldRow, oldState[0].rows[1]); + }); + it("should not update state for empty action.data on PLACES_BOOKMARK_REMOVED", () => { + const nextState = Sections(undefined, {type: at.PLACES_BOOKMARK_REMOVED}); + assert.equal(nextState, INITIAL_STATE.Sections); + }); + it("should remove the bookmark when PLACES_BOOKMARK_REMOVED is received", () => { + const action = { + type: at.PLACES_BOOKMARK_REMOVED, + data: { + url: "www.foo.bar", + bookmarkGuid: "bookmark123" + } + }; + // add some bookmark data for the first url in rows + oldState.forEach(item => { + item.rows[0].bookmarkGuid = "bookmark123"; + item.rows[0].bookmarkTitle = "Title for bar.com"; + item.rows[0].bookmarkDateCreated = 1234567; + }); + const nextState = Sections(oldState, action); + // check a section to ensure the correct bookmark was removed + const newRow = nextState[0].rows[0]; + const oldRow = nextState[0].rows[1]; + + // new row has bookmark data + assert.equal(newRow.url, action.data.url); + assert.isUndefined(newRow.bookmarkGuid); + assert.isUndefined(newRow.bookmarkTitle); + assert.isUndefined(newRow.bookmarkDateCreated); + + // old row is unchanged + assert.equal(oldRow, oldState[0].rows[1]); + }); }); describe("#insertPinned", () => { let links; diff --git a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js index 84f320807ccf..ca16f328fb73 100644 --- a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js +++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js @@ -13,14 +13,15 @@ describe("ActivityStream", () => { sandbox = sinon.sandbox.create(); ({ActivityStream, SECTIONS} = injector({ "lib/LocalizationFeed.jsm": {LocalizationFeed: Fake}, + "lib/ManualMigration.jsm": {ManualMigration: Fake}, "lib/NewTabInit.jsm": {NewTabInit: Fake}, "lib/PlacesFeed.jsm": {PlacesFeed: Fake}, - "lib/TelemetryFeed.jsm": {TelemetryFeed: Fake}, - "lib/TopSitesFeed.jsm": {TopSitesFeed: Fake}, "lib/PrefsFeed.jsm": {PrefsFeed: Fake}, "lib/SnippetsFeed.jsm": {SnippetsFeed: Fake}, - "lib/TopStoriesFeed.jsm": {TopStoriesFeed: Fake}, - "lib/SystemTickFeed.jsm": {SystemTickFeed: Fake} + "lib/SystemTickFeed.jsm": {SystemTickFeed: Fake}, + "lib/TelemetryFeed.jsm": {TelemetryFeed: Fake}, + "lib/TopSitesFeed.jsm": {TopSitesFeed: Fake}, + "lib/TopStoriesFeed.jsm": {TopStoriesFeed: Fake} })); as = new ActivityStream(); sandbox.stub(as.store, "init"); @@ -118,6 +119,10 @@ describe("ActivityStream", () => { assert.instanceOf(feed, Fake); }); }); + it("should create a ManualMigration feed", () => { + const feed = as.feeds.get("feeds.migration")(); + assert.instanceOf(feed, Fake); + }); it("should create a Snippets feed", () => { const feed = as.feeds.get("feeds.snippets")(); assert.instanceOf(feed, Fake); diff --git a/browser/extensions/activity-stream/test/unit/lib/ManualMigration.test.js b/browser/extensions/activity-stream/test/unit/lib/ManualMigration.test.js new file mode 100644 index 000000000000..01d17dfe5dc9 --- /dev/null +++ b/browser/extensions/activity-stream/test/unit/lib/ManualMigration.test.js @@ -0,0 +1,207 @@ +const injector = require("inject!lib/ManualMigration.jsm"); +const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm"); +const {GlobalOverrider} = require("test/unit/utils"); + +describe("ManualMigration", () => { + let dispatch; + let store; + let instance; + let globals; + + let migrationWizardStub; + let fakeServices; + let fakePrefs; + + beforeEach(() => { + migrationWizardStub = sinon.stub(); + let fakeMigrationUtils = { + showMigrationWizard: migrationWizardStub, + MIGRATION_ENTRYPOINT_NEWTAB: "MIGRATION_ENTRYPOINT_NEWTAB" + }; + fakeServices = { + obs: { + addObserver: sinon.stub(), + removeObserver: sinon.stub() + } + }; + fakePrefs = function() {}; + fakePrefs.get = sinon.stub(); + fakePrefs.set = sinon.stub(); + + const {ManualMigration} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: fakePrefs}}); + + globals = new GlobalOverrider(); + globals.set("Services", fakeServices); + globals.set("MigrationUtils", fakeMigrationUtils); + + dispatch = sinon.stub(); + store = {dispatch}; + instance = new ManualMigration(); + instance.store = store; + }); + + afterEach(() => { + globals.restore(); + }); + + it("should set an event listener for Migration:Ended", () => { + assert.calledOnce(fakeServices.obs.addObserver); + assert.calledWith(fakeServices.obs.addObserver, instance, "Migration:Ended"); + }); + + describe("onAction", () => { + it("should call expireIfNecessary on PREFS_INITIAL_VALUE", () => { + const action = { + type: at.PREFS_INITIAL_VALUES, + data: {migrationExpired: true} + }; + + const expireStub = sinon.stub(instance, "expireIfNecessary"); + instance.onAction(action); + + assert.calledOnce(expireStub); + assert.calledWithExactly(expireStub, action.data.migrationExpired); + }); + it("should call launch the migration wizard on MIGRATION_START", () => { + const action = { + type: at.MIGRATION_START, + _target: {browser: {ownerGlobal: "browser.xul"}}, + data: {migrationExpired: false} + }; + + instance.onAction(action); + + assert.calledOnce(migrationWizardStub); + assert.calledWithExactly(migrationWizardStub, action._target.browser.ownerGlobal, ["MIGRATION_ENTRYPOINT_NEWTAB"]); + }); + it("should set migrationStatus to true on MIGRATION_CANCEL", () => { + const action = {type: at.MIGRATION_CANCEL}; + + const setStatusStub = sinon.spy(instance, "expireMigration"); + instance.onAction(action); + + assert.calledOnce(setStatusStub); + assert.calledOnce(dispatch); + assert.calledWithExactly(dispatch, ac.SetPref("migrationExpired", true)); + }); + it("should set migrationStatus when isMigrationMessageExpired is true", () => { + const setStatusStub = sinon.stub(instance, "expireMigration"); + const isExpiredStub = sinon.stub(instance, "isMigrationMessageExpired"); + isExpiredStub.returns(true); + + instance.expireIfNecessary(false); + assert.calledOnce(setStatusStub); + }); + it("should call isMigrationMessageExpired if migrationExpired is false", () => { + const action = { + type: at.PREFS_INITIAL_VALUES, + data: {migrationExpired: false} + }; + + const stub = sinon.stub(instance, "isMigrationMessageExpired"); + instance.onAction(action); + + assert.calledOnce(stub); + }); + describe("isMigrationMessageExpired", () => { + beforeEach(() => { + instance._prefs = fakePrefs; + }); + it("should check migrationLastShownDate (case: today)", () => { + const action = { + type: at.PREFS_INITIAL_VALUES, + data: {migrationExpired: false} + }; + let today = new Date(); + today = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + + const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired"); + fakePrefs.get.returns(today); + instance.onAction(action); + + assert.calledOnce(migrationSpy); + assert.calledOnce(fakePrefs.get); + assert.calledWithExactly(fakePrefs.get, "migrationLastShownDate"); + }); + it("should return false if lastShownDate is today", () => { + let today = new Date(); + today = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + + const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired"); + fakePrefs.get.returns(today); + const ret = instance.isMigrationMessageExpired(); + + assert.calledOnce(migrationSpy); + assert.calledOnce(fakePrefs.get); + assert.equal(ret, false); + }); + it("should check migrationLastShownDate (case: yesterday)", () => { + const action = { + type: at.PREFS_INITIAL_VALUES, + data: {migrationExpired: false} + }; + let today = new Date(); + let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); + + const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired"); + fakePrefs.get.withArgs("migrationLastShownDate").returns(yesterday.valueOf() / 1000); + fakePrefs.get.withArgs("migrationRemainingDays").returns(4); + instance.onAction(action); + + assert.calledOnce(migrationSpy); + assert.calledTwice(fakePrefs.get); + assert.calledWithExactly(fakePrefs.get, "migrationLastShownDate"); + assert.calledWithExactly(fakePrefs.get, "migrationRemainingDays"); + }); + it("should update the migration prefs", () => { + const action = { + type: at.PREFS_INITIAL_VALUES, + data: {migrationExpired: false} + }; + let today = new Date(); + let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); + today = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + + const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired"); + fakePrefs.get.withArgs("migrationLastShownDate").returns(yesterday.valueOf() / 1000); + fakePrefs.get.withArgs("migrationRemainingDays").returns(4); + instance.onAction(action); + + assert.calledOnce(migrationSpy); + assert.calledTwice(fakePrefs.set); + assert.calledWithExactly(fakePrefs.set, "migrationRemainingDays", 3); + assert.calledWithExactly(fakePrefs.set, "migrationLastShownDate", today.valueOf() / 1000); + }); + it("should return true if remainingDays reaches 0", () => { + let today = new Date(); + let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); + + const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired"); + fakePrefs.get.withArgs("migrationLastShownDate").returns(yesterday.valueOf() / 1000); + fakePrefs.get.withArgs("migrationRemainingDays").returns(1); + const ret = instance.isMigrationMessageExpired(); + + assert.calledOnce(migrationSpy); + assert.calledTwice(fakePrefs.set); + assert.calledWithExactly(fakePrefs.set, "migrationRemainingDays", 0); + assert.equal(ret, true); + }); + }); + }); + it("should have observe as a proxy for setMigrationStatus", () => { + const setStatusStub = sinon.stub(instance, "expireMigration"); + instance.observe(); + + assert.calledOnce(setStatusStub); + }); + it("should remove observer at uninit", () => { + const uninitSpy = sinon.spy(instance, "uninit"); + const action = {type: at.UNINIT}; + + instance.onAction(action); + + assert.calledOnce(uninitSpy); + assert.calledOnce(fakeServices.obs.removeObserver); + assert.calledWith(fakeServices.obs.removeObserver, instance, "Migration:Ended"); + }); +}); diff --git a/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js b/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js index a1a0421146d9..fca2270721aa 100644 --- a/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js +++ b/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js @@ -51,7 +51,7 @@ describe("Top Stories Feed", () => { title: {id: "header_recommended_by", values: {provider: "test-provider"}}, rows: [], maxCards: 3, - contextMenuOptions: ["SaveToPocket", "Separator", "CheckBookmark", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"], + contextMenuOptions: ["CheckBookmark", "SaveToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"], infoOption: { header: {id: "pocket_feedback_header"}, body: {id: "pocket_feedback_body"}, @@ -61,7 +61,7 @@ describe("Top Stories Feed", () => { } }, emptyState: { - message: {id: "empty_state_topstories"}, + message: {id: "topstories_empty_state", values: {provider: "test-provider"}}, icon: "check" } }; @@ -141,12 +141,11 @@ describe("Top Stories Feed", () => { }]}`; const stories = [{ "guid": "1", - "type": "trending", + "type": "now", "title": "title", "description": "description", "image": "image-url", - "url": "rec-url", - "lastVisitDate": "123" + "url": "rec-url" }]; instance.stories_endpoint = "stories-endpoint"; @@ -181,7 +180,7 @@ describe("Top Stories Feed", () => { it("should exclude blocked (dismissed) URLs", async () => { let fetchStub = globals.sandbox.stub(); globals.set("fetch", fetchStub); - globals.set("NewTabUtils", {blockedLinks: {isBlocked: url => url === "blocked"}}); + globals.set("NewTabUtils", {blockedLinks: {isBlocked: site => site.url === "blocked"}}); const response = `{"list": [{"dedupe_url" : "blocked"}, {"dedupe_url" : "not_blocked"}]}`; instance.stories_endpoint = "stories-endpoint"; @@ -193,6 +192,30 @@ describe("Top Stories Feed", () => { assert.equal(instance.store.dispatch.firstCall.args[0].data.rows.length, 1); assert.equal(instance.store.dispatch.firstCall.args[0].data.rows[0].url, "not_blocked"); }); + it("should mark stories as new", async () => { + let fetchStub = globals.sandbox.stub(); + globals.set("fetch", fetchStub); + globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}}); + clock.restore(); + const response = JSON.stringify({ + "list": [ + {"published_timestamp": Date.now() / 1000}, + {"published_timestamp": "0"}, + {"published_timestamp": (Date.now() - 2 * 24 * 60 * 60 * 1000) / 1000} + ] + }); + + instance.stories_endpoint = "stories-endpoint"; + fetchStub.resolves({ok: true, status: 200, text: () => response}); + + await instance.fetchStories(); + assert.calledOnce(instance.store.dispatch); + assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE); + assert.equal(instance.store.dispatch.firstCall.args[0].data.rows.length, 3); + assert.equal(instance.store.dispatch.firstCall.args[0].data.rows[0].type, "now"); + assert.equal(instance.store.dispatch.firstCall.args[0].data.rows[1].type, "trending"); + assert.equal(instance.store.dispatch.firstCall.args[0].data.rows[2].type, "trending"); + }); it("should fetch topics and send event", async () => { let fetchStub = globals.sandbox.stub(); globals.set("fetch", fetchStub);