Bug 1383917 - Add manual migration, bookmarking and bug fixes to Activity Stream. r=dmose

MozReview-Commit-ID: 3KfzoBGnv7P

--HG--
extra : rebase_source : 16c49d4482649797d3767ad782da769189705adc
This commit is contained in:
Ed Lee 2017-07-24 15:31:35 -07:00
Родитель 4619abb60c
Коммит f17eecfc62
15 изменённых файлов: 1001 добавлений и 165 удалений

Просмотреть файл

@ -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",

Просмотреть файл

@ -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 =>

Просмотреть файл

@ -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,
@ -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;
@ -1267,14 +1313,14 @@ class Card extends React.Component {
{ 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,
" "
@ -1296,10 +1342,7 @@ 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" },
@ -1308,11 +1351,11 @@ class Card extends React.Component {
),
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);
@ -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() {
@ -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,
@ -1947,7 +2096,7 @@ class TopSite extends React.Component {
{ 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 },
@ -1964,18 +2113,14 @@ class TopSite extends React.Component {
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" },
@ -1984,12 +2129,12 @@ class TopSite extends React.Component {
),
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";
@ -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";

Просмотреть файл

@ -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; }

Просмотреть файл

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm0-7a1 1 0 0 0-1 1v3a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1zm0-3.188A1.188 1.188 0 1 0 9.188 5 1.188 1.188 0 0 0 8 3.812z"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 533 B

Просмотреть файл

@ -0,0 +1,11 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="#4d4d4d">
<rect x="1" y="1" width="6" height="6" rx="1" ry="1"/>
<rect x="9" y="1" width="6" height="6" rx="1" ry="1"/>
<rect x="1" y="9" width="6" height="6" rx="1" ry="1"/>
<rect x="9" y="9" width="6" height="6" rx="1" ry="1"/>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 567 B

Просмотреть файл

@ -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 dont 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": "Youve caught up. Check back later for more top stories from Pocket. Cant wait? Select a popular topic to find more great stories from around the web."
"topstories_empty_state": "Youve caught up. Check back later for more top stories from {provider}. Cant 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": {

Просмотреть файл

@ -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}`;

Просмотреть файл

@ -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"];

Просмотреть файл

@ -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;
})

Просмотреть файл

@ -8,5 +8,8 @@ module.exports = {
"assert": true,
"sinon": true,
"chai": true
},
"rules": {
"react/jsx-no-bind": 0
}
};

Просмотреть файл

@ -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;

Просмотреть файл

@ -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);

Просмотреть файл

@ -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");
});
});

Просмотреть файл

@ -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);