Bug 1405539 - Add responsive bookmarking, info doorhanger and bug fixes to Activity Stream. r=k88hudson

MozReview-Commit-ID: C5LXhbuI0EA

--HG--
extra : rebase_source : 580d19a032d6d033317c88470256d864ea8bc529
This commit is contained in:
Ed Lee 2017-10-05 15:38:54 -07:00
Родитель 1272df551e
Коммит b6f0a8f2d8
19 изменённых файлов: 613 добавлений и 266 удалений

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

@ -87,6 +87,15 @@ let allowedImageReferences = [
{file: "chrome://devtools/skin/images/dock-bottom-maximize@2x.png",
from: "chrome://devtools/skin/toolbox.css",
isFromDevTools: true},
// Bug 1405539
{file: "chrome://global/skin/arrow/panelarrow-vertical@2x.png",
from: "resource://activity-stream/data/content/activity-stream.css",
isFromDevTools: false,
platforms: ["linux", "win"]},
{file: "chrome://global/skin/arrow/panelarrow-vertical-themed.svg",
from: "resource://activity-stream/data/content/activity-stream.css",
isFromDevTools: false,
platforms: ["macosx"]},
];
// Add suffix to stylesheets' URI so that we always load them here and

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

@ -4,6 +4,7 @@
"use strict";
const {actionTypes: at} = Components.utils.import("resource://activity-stream/common/Actions.jsm", {});
const {Dedupe} = Components.utils.import("resource://activity-stream/common/Dedupe.jsm", {});
// Locales that should be displayed RTL
const RTL_LIST = ["ar", "he", "fa", "ur"];
@ -11,6 +12,8 @@ const RTL_LIST = ["ar", "he", "fa", "ur"];
const TOP_SITES_DEFAULT_LENGTH = 6;
const TOP_SITES_SHOWMORE_LENGTH = 12;
const dedupe = new Dedupe(site => site && site.url);
const INITIAL_STATE = {
App: {
// Have we received real data from the app yet?
@ -230,7 +233,7 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
}
return newState;
case at.SECTION_UPDATE:
return prevState.map(section => {
newState = prevState.map(section => {
if (section && section.id === action.data.id) {
// If the action is updating rows, we should consider initialized to be true.
// This can be overridden if initialized is defined in the action.data
@ -239,6 +242,28 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
}
return section;
});
if (!action.data.dedupeConfigurations) {
return newState;
}
action.data.dedupeConfigurations.forEach(dedupeConf => {
newState = newState.map(section => {
if (section.id === dedupeConf.id) {
const dedupedRows = dedupeConf.dedupeFrom.reduce((rows, dedupeSectionId) => {
const dedupeSection = newState.find(s => s.id === dedupeSectionId);
const [, newRows] = dedupe.group(dedupeSection.rows, rows);
return newRows;
}, section.rows);
return Object.assign({}, section, {rows: dedupedRows});
}
return section;
});
});
return newState;
case at.SECTION_UPDATE_CARD:
return prevState.map(section => {
if (section && section.id === action.data.id && section.rows) {
@ -261,10 +286,12 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
// find the item within the rows that is attempted to be bookmarked
if (item.url === action.data.url) {
const {bookmarkGuid, bookmarkTitle, dateAdded} = action.data;
Object.assign(item, {bookmarkGuid, bookmarkTitle, bookmarkDateCreated: dateAdded});
if (!item.type || item.type === "history") {
item.type = "bookmark";
}
return Object.assign({}, item, {
bookmarkGuid,
bookmarkTitle,
bookmarkDateCreated: dateAdded,
type: "bookmark"
});
}
return item;
})

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -351,6 +351,7 @@ module.exports = {
const { actionTypes: at } = __webpack_require__(0);
const { Dedupe } = __webpack_require__(20);
// Locales that should be displayed RTL
const RTL_LIST = ["ar", "he", "fa", "ur"];
@ -358,6 +359,8 @@ const RTL_LIST = ["ar", "he", "fa", "ur"];
const TOP_SITES_DEFAULT_LENGTH = 6;
const TOP_SITES_SHOWMORE_LENGTH = 12;
const dedupe = new Dedupe(site => site && site.url);
const INITIAL_STATE = {
App: {
// Have we received real data from the app yet?
@ -580,7 +583,7 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
}
return newState;
case at.SECTION_UPDATE:
return prevState.map(section => {
newState = prevState.map(section => {
if (section && section.id === action.data.id) {
// If the action is updating rows, we should consider initialized to be true.
// This can be overridden if initialized is defined in the action.data
@ -589,6 +592,28 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
}
return section;
});
if (!action.data.dedupeConfigurations) {
return newState;
}
action.data.dedupeConfigurations.forEach(dedupeConf => {
newState = newState.map(section => {
if (section.id === dedupeConf.id) {
const dedupedRows = dedupeConf.dedupeFrom.reduce((rows, dedupeSectionId) => {
const dedupeSection = newState.find(s => s.id === dedupeSectionId);
const [, newRows] = dedupe.group(dedupeSection.rows, rows);
return newRows;
}, section.rows);
return Object.assign({}, section, { rows: dedupedRows });
}
return section;
});
});
return newState;
case at.SECTION_UPDATE_CARD:
return prevState.map(section => {
if (section && section.id === action.data.id && section.rows) {
@ -611,10 +636,12 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
// find the item within the rows that is attempted to be bookmarked
if (item.url === action.data.url) {
const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
Object.assign(item, { bookmarkGuid, bookmarkTitle, bookmarkDateCreated: dateAdded });
if (!item.type || item.type === "history") {
item.type = "bookmark";
}
return Object.assign({}, item, {
bookmarkGuid,
bookmarkTitle,
bookmarkDateCreated: dateAdded,
type: "bookmark"
});
}
return item;
})
@ -1099,9 +1126,9 @@ module.exports._unconnected = LinkMenu;
const React = __webpack_require__(1);
const { connect } = __webpack_require__(3);
const { injectIntl, FormattedMessage } = __webpack_require__(2);
const Card = __webpack_require__(20);
const Card = __webpack_require__(21);
const { PlaceholderCard } = Card;
const Topics = __webpack_require__(22);
const Topics = __webpack_require__(23);
const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
const VISIBLE = "visible";
@ -1387,10 +1414,10 @@ module.exports.InfoIntl = InfoIntl;
const ReactDOM = __webpack_require__(12);
const Base = __webpack_require__(13);
const { Provider } = __webpack_require__(3);
const initStore = __webpack_require__(29);
const initStore = __webpack_require__(30);
const { reducers } = __webpack_require__(6);
const DetectUserSessionStart = __webpack_require__(31);
const { addSnippetsSubscriber } = __webpack_require__(32);
const DetectUserSessionStart = __webpack_require__(32);
const { addSnippetsSubscriber } = __webpack_require__(33);
const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
new DetectUserSessionStart().sendEventOrAddListener();
@ -1427,13 +1454,13 @@ const React = __webpack_require__(1);
const { connect } = __webpack_require__(3);
const { addLocaleData, IntlProvider } = __webpack_require__(2);
const TopSites = __webpack_require__(14);
const Search = __webpack_require__(23);
const ConfirmDialog = __webpack_require__(25);
const ManualMigration = __webpack_require__(26);
const PreferencesPane = __webpack_require__(27);
const Search = __webpack_require__(24);
const ConfirmDialog = __webpack_require__(26);
const ManualMigration = __webpack_require__(27);
const PreferencesPane = __webpack_require__(28);
const Sections = __webpack_require__(10);
const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
const { PrerenderData } = __webpack_require__(28);
const { PrerenderData } = __webpack_require__(29);
// Add the locale data for pluralization and relative-time formatting for now,
// this just uses english locale data. We can make this more sophisticated if
@ -2291,12 +2318,57 @@ module.exports.CheckPinTopSite = (site, index) => site.isPinned ? module.exports
/***/ }),
/* 20 */
/***/ (function(module, exports) {
var Dedupe = class Dedupe {
constructor(createKey, compare) {
this.createKey = createKey || this.defaultCreateKey;
this.compare = compare || this.defaultCompare;
}
defaultCreateKey(item) {
return item;
}
defaultCompare() {
return false;
}
/**
* Dedupe any number of grouped elements favoring those from earlier groups.
*
* @param {Array} groups Contains an arbitrary number of arrays of elements.
* @returns {Array} A matching array of each provided group deduped.
*/
group(...groups) {
const globalKeys = new Set();
const result = [];
for (const values of groups) {
const valueMap = new Map();
for (const value of values) {
const key = this.createKey(value);
if (!globalKeys.has(key) && (!valueMap.has(key) || this.compare(valueMap.get(key), value))) {
valueMap.set(key, value);
}
}
result.push(valueMap);
valueMap.forEach((value, key) => globalKeys.add(key));
}
return result.map(m => Array.from(m.values()));
}
};
module.exports = {
Dedupe
};
/***/ }),
/* 21 */
/***/ (function(module, exports, __webpack_require__) {
const React = __webpack_require__(1);
const LinkMenu = __webpack_require__(9);
const { FormattedMessage } = __webpack_require__(2);
const cardContextTypes = __webpack_require__(21);
const cardContextTypes = __webpack_require__(22);
const { actionCreators: ac, actionTypes: at } = __webpack_require__(0);
// Keep track of pending image loads to only request once
@ -2492,7 +2564,7 @@ module.exports = Card;
module.exports.PlaceholderCard = PlaceholderCard;
/***/ }),
/* 21 */
/* 22 */
/***/ (function(module, exports) {
module.exports = {
@ -2515,7 +2587,7 @@ module.exports = {
};
/***/ }),
/* 22 */
/* 23 */
/***/ (function(module, exports, __webpack_require__) {
const React = __webpack_require__(1);
@ -2566,7 +2638,7 @@ module.exports._unconnected = Topics;
module.exports.Topic = Topic;
/***/ }),
/* 23 */
/* 24 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -2577,7 +2649,7 @@ const React = __webpack_require__(1);
const { connect } = __webpack_require__(3);
const { FormattedMessage, injectIntl } = __webpack_require__(2);
const { actionCreators: ac } = __webpack_require__(0);
const { IS_NEWTAB } = __webpack_require__(24);
const { IS_NEWTAB } = __webpack_require__(25);
class Search extends React.PureComponent {
constructor(props) {
@ -2677,7 +2749,7 @@ module.exports = connect(state => ({ locale: state.App.locale }))(injectIntl(Sea
module.exports._unconnected = Search;
/***/ }),
/* 24 */
/* 25 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {module.exports = {
@ -2687,7 +2759,7 @@ module.exports._unconnected = Search;
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 25 */
/* 26 */
/***/ (function(module, exports, __webpack_require__) {
const React = __webpack_require__(1);
@ -2792,7 +2864,7 @@ module.exports._unconnected = ConfirmDialog;
module.exports.Dialog = ConfirmDialog;
/***/ }),
/* 26 */
/* 27 */
/***/ (function(module, exports, __webpack_require__) {
const React = __webpack_require__(1);
@ -2856,7 +2928,7 @@ module.exports = connect()(ManualMigration);
module.exports._unconnected = ManualMigration;
/***/ }),
/* 27 */
/* 28 */
/***/ (function(module, exports, __webpack_require__) {
const React = __webpack_require__(1);
@ -2884,7 +2956,12 @@ const PreferencesInput = props => React.createElement(
"p",
{ className: "prefs-input-description" },
getFormattedMessage(props.descString)
)
),
React.Children.map(props.children, child => React.createElement(
"div",
{ className: `options${child.props.disabled ? " disabled" : ""}` },
child
))
);
class PreferencesPane extends React.PureComponent {
@ -2977,21 +3054,51 @@ class PreferencesPane extends React.PureComponent {
null,
React.createElement(FormattedMessage, { id: "settings_pane_body2" })
),
React.createElement(PreferencesInput, { className: "showSearch", prefName: "showSearch", value: prefs.showSearch, onChange: this.handlePrefChange,
titleString: { id: "settings_pane_search_header" }, descString: { id: "settings_pane_search_body" } }),
React.createElement(PreferencesInput, {
className: "showSearch",
prefName: "showSearch",
value: prefs.showSearch,
onChange: this.handlePrefChange,
titleString: { id: "settings_pane_search_header" },
descString: { id: "settings_pane_search_body" } }),
React.createElement("hr", null),
React.createElement(PreferencesInput, { className: "showTopSites", prefName: "showTopSites", value: prefs.showTopSites, onChange: this.handlePrefChange,
titleString: { id: "settings_pane_topsites_header" }, descString: { id: "settings_pane_topsites_body" } }),
React.createElement(
"div",
{ className: `options${prefs.showTopSites ? "" : " disabled"}` },
React.createElement(PreferencesInput, { className: "showMoreTopSites", prefName: "topSitesCount", disabled: !prefs.showTopSites,
value: prefs.topSitesCount !== TOP_SITES_DEFAULT_LENGTH, onChange: this.handlePrefChange,
titleString: { id: "settings_pane_topsites_options_showmore" }, labelClassName: "icon icon-topsites" })
PreferencesInput,
{
className: "showTopSites",
prefName: "showTopSites",
value: prefs.showTopSites,
onChange: this.handlePrefChange,
titleString: { id: "settings_pane_topsites_header" },
descString: { id: "settings_pane_topsites_body" } },
React.createElement(PreferencesInput, {
className: "showMoreTopSites",
prefName: "topSitesCount",
disabled: !prefs.showTopSites,
value: prefs.topSitesCount !== TOP_SITES_DEFAULT_LENGTH,
onChange: this.handlePrefChange,
titleString: { id: "settings_pane_topsites_options_showmore" },
labelClassName: "icon icon-topsites" })
),
sections.filter(section => !section.shouldHidePref).map(({ id, title, enabled, pref }) => React.createElement(PreferencesInput, { key: id, className: "showSection", prefName: pref && pref.feed || id,
value: enabled, onChange: pref && pref.feed ? this.handlePrefChange : this.handleSectionChange,
titleString: pref && pref.titleString || title, descString: pref && pref.descString })),
sections.filter(section => !section.shouldHidePref).map(({ id, title, enabled, pref }) => React.createElement(
PreferencesInput,
{
key: id,
className: "showSection",
prefName: pref && pref.feed || id,
value: enabled,
onChange: pref && pref.feed ? this.handlePrefChange : this.handleSectionChange,
titleString: pref && pref.titleString || title,
descString: pref && pref.descString },
pref.nestedPrefs && pref.nestedPrefs.map(nestedPref => React.createElement(PreferencesInput, {
key: nestedPref.name,
prefName: nestedPref.name,
disabled: !enabled,
value: prefs[nestedPref.name],
onChange: this.handlePrefChange,
titleString: nestedPref.titleString,
labelClassName: `icon ${nestedPref.icon}` }))
)),
React.createElement("hr", null),
React.createElement(PreferencesInput, { className: "showSnippets", prefName: "feeds.snippets",
value: prefs["feeds.snippets"], onChange: this.handlePrefChange,
@ -3018,7 +3125,7 @@ module.exports.PreferencesPane = PreferencesPane;
module.exports.PreferencesInput = PreferencesInput;
/***/ }),
/* 28 */
/* 29 */
/***/ (function(module, exports) {
class _PrerenderData {
@ -3108,12 +3215,12 @@ module.exports = {
};
/***/ }),
/* 29 */
/* 30 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {/* eslint-env mozilla/frame-script */
const { createStore, combineReducers, applyMiddleware } = __webpack_require__(30);
const { createStore, combineReducers, applyMiddleware } = __webpack_require__(31);
const { actionTypes: at, actionCreators: ac, actionUtils: au } = __webpack_require__(0);
const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
@ -3223,13 +3330,13 @@ module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 30 */
/* 31 */
/***/ (function(module, exports) {
module.exports = Redux;
/***/ }),
/* 31 */
/* 32 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {const { actionTypes: at } = __webpack_require__(0);
@ -3299,7 +3406,7 @@ module.exports = class DetectUserSessionStart {
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 32 */
/* 33 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {const DATABASE_NAME = "snippets_db";

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

@ -197,7 +197,7 @@ main {
.section-top-bar {
height: 16px;
margin-bottom: 12px; }
margin-bottom: 16px; }
.section-title {
font-size: 13px;
@ -421,11 +421,17 @@ main {
opacity: 1; }
.edit-topsites-wrapper .edit-topsites-button {
position: absolute;
offset-inline-end: 21px;
top: -2px;
border-right: 1px solid #D7D7DB;
line-height: 13px;
offset-inline-end: 24px;
opacity: 0;
padding: 0 10px;
position: absolute;
top: 2px;
transition: opacity 0.2s cubic-bezier(0.07, 0.95, 0, 1); }
.edit-topsites-wrapper .edit-topsites-button:dir(rtl) {
border-left: 1px solid #D7D7DB;
border-right: 0; }
.edit-topsites-wrapper .edit-topsites-button:focus, .edit-topsites-wrapper .edit-topsites-button:active {
opacity: 1; }
.edit-topsites-wrapper .edit-topsites-button button {
@ -550,23 +556,38 @@ section.top-sites:hover .edit-topsites-button {
.section-top-bar .info-option-icon:focus, .section-top-bar .info-option-icon:active {
opacity: 1; }
.section-top-bar .info-option-icon[aria-expanded="true"] {
background-color: rgba(12, 12, 13, 0.1);
border-radius: 1px;
box-shadow: 0 0 0 5px rgba(12, 12, 13, 0.1);
fill: rgba(12, 12, 13, 0.8); }
.section-top-bar .section-info-option .info-option {
visibility: hidden;
opacity: 0;
transition: visibility 0.2s, opacity 0.2s cubic-bezier(0.07, 0.95, 0, 1); }
.section-top-bar .section-info-option .info-option::before {
.section-top-bar .section-info-option .info-option::after, .section-top-bar .section-info-option .info-option::before {
content: "";
display: block;
offset-inline-end: 0;
position: absolute; }
.section-top-bar .section-info-option .info-option::before {
background-image: url(chrome://global/skin/arrow/panelarrow-vertical-themed.svg), url(chrome://global/skin/arrow/panelarrow-vertical@2x.png);
background-position: calc(100% - 7px) bottom;
background-repeat: no-repeat;
background-size: 18px 10px;
height: 32px;
left: 50%;
position: absolute;
right: 0;
top: -32px; }
top: -32px;
width: 43px; }
.section-top-bar .section-info-option .info-option:dir(rtl)::before {
background-position-x: 7px; }
.section-top-bar .section-info-option .info-option::after {
height: 10px;
offset-inline-start: 0;
top: -10px; }
.section-top-bar .info-option-icon[aria-expanded="true"] + .info-option {
visibility: visible;
opacity: 1;
transition: visibility 0.2s, opacity 0.2s cubic-bezier(0.07, 0.95, 0, 1); }
.section-top-bar .info-option-icon:not([aria-expanded="true"]) + .info-option {
pointer-events: none; }
.section-top-bar .info-option {
z-index: 9999;
position: absolute;
@ -577,7 +598,7 @@ section.top-sites:hover .edit-topsites-button {
line-height: 120%;
margin-inline-end: -9px;
offset-inline-end: 0;
top: 20px;
top: 26px;
width: 320px;
padding: 24px;
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
@ -758,7 +779,7 @@ section.section:hover .info-option-icon {
background-color: rgba(12, 12, 13, 0.1);
cursor: pointer; }
.search-wrapper .search-button:active {
background-color: rgba(12, 12, 13, 0.15); }
background-color: rgba(12, 12, 13, 0.2); }
.search-wrapper .search-button:dir(rtl) {
transform: scaleX(-1); }
.search-wrapper .contentSearchSuggestionTable {
@ -840,7 +861,7 @@ section.section:hover .info-option-icon {
.prefs-pane .prefs-modal-inner-wrapper section {
margin: 20px 0; }
.prefs-pane .prefs-modal-inner-wrapper section p {
margin: 5px 0 5px 30px; }
margin: 5px 0 20px 30px; }
.prefs-pane .prefs-modal-inner-wrapper section label {
display: inline-block;
position: relative;

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

@ -4,6 +4,7 @@
"default_label_loading": "Tye ka cano…",
"header_top_sites": "Kakube maloyo",
"header_stories": "Lok madito",
"header_highlights": "Wiye madito",
"header_visit_again": "Lim doki",
"header_bookmarks": "Alamabuk ma cok coki",
"header_recommended_by": "Lami tam obedo {provider}",
@ -35,6 +36,8 @@
"search_web_placeholder": "Yeny kakube",
"search_settings": "Lok ter me yeny",
"section_info_option": "Ngec",
"section_info_send_feedback": "Cwal adwogi",
"section_info_privacy_notice": "Ngec me mung",
"welcome_title": "Wajoli i dirica matidi manyen",
"welcome_body": "Firefox bi tic ki kabedo man me nyuto alamabukke mamegi, coc akwana, vidio, ki potbukke ma ilimo cokcoki ma pi gi tego loyo, wek i dok ii gi ma yot.",
"welcome_label": "Tye ka kube ki wiye madito mamegi",
@ -44,6 +47,7 @@
"time_label_day": "nino{number}",
"settings_pane_button_label": "Yub potbuk me dirica matidi mamegi manyen",
"settings_pane_header": "Ter me dirica matidi manyen",
"settings_pane_body2": "Yer ngo ma i neno i potbuk man.",
"settings_pane_search_header": "Yeny",
"settings_pane_search_body": "Yeny Kakube ki i dirica ni matidi manyen.",
"settings_pane_topsites_header": "Kakube ma gi loyo",
@ -53,6 +57,12 @@
"settings_pane_bookmarks_body": "Alamabukke ni ma kicweyo manyen i kabedo acel macek.",
"settings_pane_visit_again_header": "Lim Kidoco",
"settings_pane_visit_again_body": "Firefox bi nyuti but gin mukato me yeny mamegi ma itwero mito me poo ikome onyo dok cen iyie.",
"settings_pane_highlights_header": "Wiye madito",
"settings_pane_highlights_body2": "Nong yoo ni cen i jami mamit ma ilimo gi cokcokki onyo iketo alamabuk.",
"settings_pane_highlights_options_bookmarks": "Alamabuk",
"settings_pane_highlights_options_visited": "Kakube ma kilimo",
"settings_pane_snippets_header": "Kwena macek",
"settings_pane_snippets_body": "Kwan ngec manyen macego dok mamit ki bot Mozilla ikom Firefox, kwo me intanet, ki meme mabino atata.",
"settings_pane_done_button": "Otum",
"edit_topsites_button_text": "Yubi",
"edit_topsites_button_label": "Yub bute pi kakubi ni ma giloyo",
@ -75,8 +85,10 @@
"pocket_read_more": "Lok macuk gi lamal:",
"pocket_read_even_more": "Nen Lok mapol",
"pocket_feedback_header": "Kakube maber loyo, dano makato milion 25 aye oyubo.",
"pocket_description": "Nong jami me rwom ma lamal ma itwero keng woko, ki kony ma aa ki bot Pocket, dong tye but Mozilla.",
"highlights_empty_state": "Cak yeny, ka wa binyuto coc akwana mabeco, video, ki potbuk mukene ma ilimo cokcokki onyo ma kiketo alamabuk kany.",
"topstories_empty_state": "Ityeko weng. Rot doki lacen pi lok madito mapol ki bot {provider}. Pe itwero kuro? Yer lok macuke lamal me nongo lok mabeco mapol ki i but kakube.",
"manual_migration_explanation2": "Tem Firefox ki alamabuk, gin mukato ki mung me donyo ki ii layeny mukene.",
"manual_migration_cancel_button": "Pe Apwoyo",
"manual_migration_import_button": "Kel kombedi"
},
@ -163,10 +175,54 @@
"newtab_page_title": "Llingüeta nueva",
"default_label_loading": "Cargando…",
"header_top_sites": "Sitios destacaos",
"header_stories": "Histories destacaes",
"header_highlights": "Los destacaos",
"header_visit_again": "Visitar de nueves",
"header_bookmarks": "Marcadores recientes",
"header_bookmarks_placeholder": "Entá nun tienes dengún marcador.",
"header_stories_from": "de",
"type_label_visited": "Visitóse",
"type_label_bookmarked": "Amestóse a marcadores",
"type_label_synced": "Sincronizóse dende otru preséu",
"type_label_recommended": "Tendencia",
"type_label_open": "Abrir",
"type_label_topic": "Tema",
"welcome_title": "Bienllegáu/ada a la llingüeta nueva",
"welcome_body": "Firefox usará esti espaciu p'amosate los marcadores, artículos, vídeos y páxines más relevantes de los que visitares apocayá, asina pues volver a ello de mou cenciellu."
"type_label_now": "Agora",
"menu_action_bookmark": "Amestar a marcadores",
"menu_action_remove_bookmark": "Desaniciar marcador",
"menu_action_copy_address": "Copiar direición",
"menu_action_email_link": "Unviar enllaz per corréu…",
"menu_action_open_new_window": "Abrir nuna ventana nueva",
"menu_action_open_private_window": "Abrir nuna ventana privada nueva",
"menu_action_dismiss": "Escartar",
"menu_action_delete": "Desaniciar del historial",
"menu_action_pin": "Fixar",
"menu_action_unpin": "Desfixar",
"confirm_history_delete_p1": "¿De xuru que quies desaniciar cada instancia d'esta páxina del to historial?",
"confirm_history_delete_notice_p2": "Esta aición nun pue desfacese.",
"menu_action_save_to_pocket": "Guardar en Pocket",
"search_for_something_with": "Guetar {search_term} con:",
"search_button": "Guetar",
"search_header": "Gueta en {search_engine_name}",
"search_web_placeholder": "Guetar na web",
"search_settings": "Camudar axustes de gueta",
"section_info_option": "Información",
"section_info_send_feedback": "Unviar comentarios",
"section_info_privacy_notice": "Nota de privacidá",
"welcome_title": "Afáyate na llingüeta nueva",
"welcome_body": "Firefox usará esti espaciu p'amosate los marcadores, artículos, vídeos y páxines más relevantes que visitares apocayá, asina pues volver a ellos de mou cenciellu.",
"time_label_less_than_minute": "<1m",
"time_label_minute": "{number}m",
"time_label_hour": "{number}h",
"time_label_day": "{number}d",
"settings_pane_done_button": "Fecho",
"edit_topsites_showmore_button": "Amosar más",
"edit_topsites_done_button": "Fecho",
"topsites_form_add_button": "Amestar",
"topsites_form_save_button": "Guardar",
"topsites_form_cancel_button": "Encaboxar",
"pocket_read_more": "Temes populares:",
"pocket_read_even_more": "Ver más histories"
},
"az": {
"newtab_page_title": "Yeni Vərəq",
@ -999,7 +1055,7 @@
"default_label_loading": "Indlæser…",
"header_top_sites": "Mest besøgte websider",
"header_stories": "Tophistorier",
"header_highlights": "Højdepunkter",
"header_highlights": "Fremhævede",
"header_visit_again": "Besøg igen",
"header_bookmarks": "Seneste bogmærker",
"header_recommended_by": "Anbefalet af {provider}",
@ -1032,15 +1088,17 @@
"search_settings": "Skift søgeindstillinger",
"section_info_option": "Info",
"section_info_send_feedback": "Send feedback",
"section_info_privacy_notice": "Privatlivspolitik",
"welcome_title": "Velkommen til nyt faneblad",
"welcome_body": "Firefox vil bruge denne plads til at vise dine mest relevante bogmærker, artikler, videoer og sider, du har besøgt for nylig - så kan du nemmere finde dem.",
"welcome_label": "Finder dine højdepunkter",
"welcome_label": "Finder dine vigtigste sider",
"time_label_less_than_minute": "<1 m.",
"time_label_minute": "{number} m.",
"time_label_hour": "{number} t.",
"time_label_day": "{number} d.",
"settings_pane_button_label": "Tilpas siden Nyt faneblad",
"settings_pane_header": "Indstillinger for Nyt faneblad",
"settings_pane_body2": "Vælg, hvad du vil se på denne side.",
"settings_pane_search_header": "Søgning",
"settings_pane_search_body": "Søg på nettet fra Nyt faneblad.",
"settings_pane_topsites_header": "Mest besøgte websider",
@ -1050,6 +1108,12 @@
"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_highlights_header": "Fremhævede",
"settings_pane_highlights_body2": "Find tilbage til interessant indhold, du har besøgt eller gemt et bogmærke til for nylig.",
"settings_pane_highlights_options_bookmarks": "Bogmærker",
"settings_pane_highlights_options_visited": "Besøgte websider",
"settings_pane_snippets_header": "Notitser",
"settings_pane_snippets_body": "Læs korte opdateringer fra Mozilla om Firefox, internet-kultur og lidt underholdning fra tid til anden.",
"settings_pane_done_button": "Færdig",
"edit_topsites_button_text": "Rediger",
"edit_topsites_button_label": "Tilpas afsnittet Mest besøgte websider",
@ -1072,7 +1136,10 @@
"pocket_read_more": "Populære emner:",
"pocket_read_even_more": "Se flere historier",
"pocket_feedback_header": "Det bedste fra nettet, udvalgt af mere end 25 millioner mennesker.",
"pocket_description": "Opdag indhold af høj kvalitet, som du måske ellers ikke ville have opdaget. Indholdet kommer fra Pocket, der nu er en del af Mozilla.",
"highlights_empty_state": "Gå i gang med at browse, så vil vi vise dig nogle af de artikler, videoer og andre sider, du har besøgt eller gemt et bogmærke til for nylig.",
"topstories_empty_state": "Der er ikke flere nye historier. Kom tilbage senere for at se flere tophistorier fra {provider}. Kan du ikke vente? Vælg et populært emne og find flere spændende historier fra hele verden.",
"manual_migration_explanation2": "Prøv Firefox med bogmærkerne, historikken og adgangskoderne fra en anden browser.",
"manual_migration_cancel_button": "Nej tak",
"manual_migration_import_button": "Importer nu"
},
@ -2404,7 +2471,7 @@
"time_label_day": "{number} j",
"settings_pane_button_label": "Personnaliser la page Nouvel onglet",
"settings_pane_header": "Préférences Nouvel onglet",
"settings_pane_body2": "Choisissez les éléments à afficher sur cette page.",
"settings_pane_body2": "Choisissez les éléments à afficher sur la page.",
"settings_pane_search_header": "Recherche",
"settings_pane_search_body": "Effectuez une recherche sur le Web depuis le nouvel onglet.",
"settings_pane_topsites_header": "Sites les plus visités",
@ -3285,19 +3352,19 @@
"header_stories": "Historias popular",
"header_highlights": "In evidentia",
"header_visit_again": "Visita de novo",
"header_bookmarks": "Paginas marcate recentemente",
"header_bookmarks": "Marcapaginas recente",
"header_recommended_by": "Recommendate per {provider}",
"header_bookmarks_placeholder": "Tu ha ancora nulle paginas marcate.",
"header_bookmarks_placeholder": "Tu ha ancora nulle marcapaginas.",
"header_stories_from": "de",
"type_label_visited": "Visitate",
"type_label_bookmarked": "Marcate",
"type_label_bookmarked": "Marcapaginas addite",
"type_label_synced": "Synchronisate de altere apparato",
"type_label_recommended": "Tendentias",
"type_label_open": "Aperite",
"type_label_topic": "Subjecto",
"type_label_now": "Ora",
"menu_action_bookmark": "Marcar le pagina",
"menu_action_remove_bookmark": "Dismarcar le pagina",
"menu_action_bookmark": "Adder marcapaginas",
"menu_action_remove_bookmark": "Remover le marcapaginas",
"menu_action_copy_address": "Copiar le adresse",
"menu_action_email_link": "Inviar le ligamine per email…",
"menu_action_open_new_window": "Aperir in un nove fenestra",
@ -3318,7 +3385,7 @@
"section_info_send_feedback": "Inviar feedback",
"section_info_privacy_notice": "Advertentia de privacitate",
"welcome_title": "Benvenite al nove scheda",
"welcome_body": "Firefox usara iste spatio pro monstrar tu paginas marcate le plus relevante, articulos, videos e paginas que tu ha visitate recentemente, de sorta que tu pote revider los facilemente.",
"welcome_body": "Firefox usara iste spatio pro monstrar tu marcapaginas le plus relevante, articulos, videos e paginas que tu ha visitate recentemente, de sorta que tu pote revider los facilemente.",
"welcome_label": "Identificante tu evidentias",
"time_label_less_than_minute": "<1 min",
"time_label_minute": "{number} min",
@ -3332,13 +3399,13 @@
"settings_pane_topsites_header": "Sitos popular",
"settings_pane_topsites_body": "Acceder al sitos web que tu plus visita.",
"settings_pane_topsites_options_showmore": "Monstrar duo lineas",
"settings_pane_bookmarks_header": "Paginas marcate recentemente",
"settings_pane_bookmarks_body": "Tu paginas marcate recentemente a un sol loco.",
"settings_pane_bookmarks_header": "Marcapaginas recente",
"settings_pane_bookmarks_body": "Tu marcapaginas le plus recente a un sol loco.",
"settings_pane_visit_again_header": "Visitar de novo",
"settings_pane_visit_again_body": "Firefox te monstrara partes de tu chronologia de navigation que tu pote voler rememorar o visitar novemente.",
"settings_pane_highlights_header": "In evidentia",
"settings_pane_highlights_body2": "Retrova cosas interessante que tu ha recentemente visitate o marcate.",
"settings_pane_highlights_options_bookmarks": "Paginas marcate",
"settings_pane_highlights_body2": "Retrova cosas interessante que tu ha recentemente visitate o addite marcapaginas.",
"settings_pane_highlights_options_bookmarks": "Marcapaginas",
"settings_pane_highlights_options_visited": "Sitos visitate",
"settings_pane_snippets_header": "Breve novas",
"settings_pane_snippets_body": "Lege breve e legier novas de Mozilla super Firefox, cultura internet e occasionalmente super alcun meme.",
@ -3365,9 +3432,9 @@
"pocket_read_even_more": "Vider plus historias",
"pocket_feedback_header": "Le melior del web, selectionate per 25 milliones de personas.",
"pocket_description": "Discoperir contento de alte qualitate que tu poterea alteremente non cognoscer, con le adjuta de Pocket, ora parte de Mozilla.",
"highlights_empty_state": "Comencia navigar e nos te monstrara alcun del grande articulos, videos e altere paginas que tu ha recentemente visitate o marcate hic.",
"highlights_empty_state": "Comencia navigar e nos te monstrara alcun del grande articulos, videos e altere paginas que tu ha recentemente visitate o addite marcapaginas hic.",
"topstories_empty_state": "Tu ja es in die con toto. Reveni plus tarde pro plus historias popular de {provider}. Non vole attender? Selectiona un subjecto popular pro trovar plus altere historias interessante del web.",
"manual_migration_explanation2": "Essaya Firefox con le paginas marcate, le chronologia e le contrasignos de altere navigator.",
"manual_migration_explanation2": "Essaya Firefox con le marcapaginas, le chronologia e le contrasignos de un altere navigator.",
"manual_migration_cancel_button": "No, gratias",
"manual_migration_import_button": "Importar ora"
},
@ -3655,7 +3722,7 @@
"default_label_loading": "იტვირთება…",
"header_top_sites": "რჩეული საიტები",
"header_stories": "რჩეული სტატიები",
"header_highlights": "მნიშნველოვანი სიახლეები",
"header_highlights": "მნიშვნელოვანი საიტები",
"header_visit_again": "ხელახლა ნახვა",
"header_bookmarks": "ბოლოს ჩანიშნულები",
"header_recommended_by": "რეკომენდებულია {provider}-ის მიერ",
@ -3691,7 +3758,7 @@
"section_info_privacy_notice": "პირადი მონაცემების დაცვა",
"welcome_title": "მოგესალმებით ახალ ჩანართზე",
"welcome_body": "Firefox ამ სივრცეს გამოიყენებს თქვენთვის ყველაზე საჭირო სანიშნეების, სტატიების, ვიდეოებისა და ბოლოს მონახულებული გვერდებისთვის, რომ ადვილად შეძლოთ მათზე დაბრუნება.",
"welcome_label": "რჩეული ვებგვერდების დადგენა",
"welcome_label": "მნიშვნელოვანი საიტების დადგენა",
"time_label_less_than_minute": "<1წთ",
"time_label_minute": "{number}წთ",
"time_label_hour": "{number}სთ",
@ -3708,8 +3775,8 @@
"settings_pane_bookmarks_body": "ახლად შექმნილი სანიშნეები, ერთი ხელის გაწვდენაზე.",
"settings_pane_visit_again_header": "ხელახლა ნახვა",
"settings_pane_visit_again_body": "Firefox გაჩვენებთ მონახულებული გვერდების ისტორიიდან იმას, რისი გახსენებაც ან რაზე დაბრუნებაც გენდომებათ.",
"settings_pane_highlights_header": "მნიშნველოვანი სიახლეები",
"settings_pane_highlights_body2": "მარტივად დაუბრუნდით ბოლოს მონახულებულ, ან ჩანიშნულ საიტებს.",
"settings_pane_highlights_header": "მნიშვნელოვანი საიტები",
"settings_pane_highlights_body2": "მარტივად დაუბრუნდით ბოლოს მონახულებულ, ან ჩანიშნულ გვერდებს.",
"settings_pane_highlights_options_bookmarks": "სანიშნეები",
"settings_pane_highlights_options_visited": "მონახულებული საიტები",
"settings_pane_snippets_header": "ცნობები",
@ -3717,8 +3784,8 @@
"settings_pane_done_button": "მზადაა",
"edit_topsites_button_text": "ჩასწორება",
"edit_topsites_button_label": "მოირგეთ რჩეული საიტების განყოფილება",
"edit_topsites_showmore_button": "მეტის ენება",
"edit_topsites_showless_button": "ნაკლების ენება",
"edit_topsites_showmore_button": "მეტის გამოჩენა",
"edit_topsites_showless_button": "ნაკლების გამოჩენა",
"edit_topsites_done_button": "მზადაა",
"edit_topsites_pin_button": "საიტის მიმაგრება",
"edit_topsites_unpin_button": "მიმაგრების მოხსნა",
@ -4308,7 +4375,7 @@
"settings_pane_body2": "Изберете што ќе гледате на оваа страница.",
"settings_pane_search_header": "Пребарување",
"settings_pane_search_body": "Пребарајте низ Интернет од вашето ново јазиче.",
"settings_pane_topsites_header": "Врвни мрежни места",
"settings_pane_topsites_header": "Популарни мрежни места",
"settings_pane_topsites_body": "Пристапете до мрежните места што ги посетувате најмногу.",
"settings_pane_topsites_options_showmore": "Прикажи два реда",
"settings_pane_bookmarks_header": "Скорешни обележувачи",
@ -4354,8 +4421,11 @@
"newtab_page_title": "പുതിയ ടാബ്",
"default_label_loading": "ലോഡ്ചെയ്യുന്നു…",
"header_top_sites": "മികച്ച സൈറ്റുകൾ",
"header_highlights": "ഹൈലൈറ്റുകൾ",
"header_stories": "മികച്ച ലേഖനങ്ങൾ",
"header_highlights": "ഹൈലൈറ്റുകൾ",
"header_visit_again": "വീണ്ടും സന്ദർശിക്കുക",
"header_bookmarks": "അടുത്തിടെയുള്ള ബുക്ക്മാർക്കുകൾ",
"header_bookmarks_placeholder": "നിങ്ങൾക്ക് ഇതുവരെ ബുക്ക്മാർക്കുകൾ ഇല്ല.",
"header_stories_from": "എവിടെ നിന്നും",
"type_label_visited": "സന്ദർശിച്ചത്‌",
"type_label_bookmarked": "അടയാളപ്പെടുത്തിയത്",
@ -4363,6 +4433,7 @@
"type_label_recommended": "ട്രെൻഡിംഗ്",
"type_label_open": "തുറക്കുക",
"type_label_topic": "വിഷയം",
"type_label_now": "ഇപ്പോൾ",
"menu_action_bookmark": "അടയാളം",
"menu_action_remove_bookmark": "അടയാളം മാറ്റുക",
"menu_action_copy_address": "വിലാസം പകർത്തുക",
@ -4371,12 +4442,17 @@
"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": "പോക്കറ്റിലേയ്ക്ക് സംരക്ഷിയ്ക്കുക",
"search_for_something_with": "തിരയാൻ {search_term} : എന്നത് ഉപയോഗിയ്ക്കുക",
"search_button": "തിരയുക",
"search_header": "{search_engine_name} തിരയുക",
"search_web_placeholder": "ഇൻറർനെറ്റിൽ തിരയുക",
"search_settings": "തിരയാനുള്ള രീതികൾ മാറ്റുക",
"section_info_option": "വിവരം",
"welcome_title": "പുതിയ ജാലകത്തിലേക്കു സ്വാഗതം",
"welcome_body": "നിങ്ങളുടെ ഏറ്റവും ശ്രദ്ധേയമായ അടയാളങ്ങൾ, ലേഖനങ്ങൾ, വീഡിയോകൾ, കൂടാതെ നിങ്ങൾ സമീപകാലത്ത് സന്ദർശിച്ച താളുകൾ എന്നിവ കാണിക്കുന്നതിനായി ഫയർഫോക്സ് ഈ ഇടം ഉപയോഗിക്കും, അതിനാൽ നിങ്ങൾക്ക് എളുപ്പത്തിൽ അവയിലേക്ക് തിരിച്ചു പോകാം.",
"welcome_label": "താങ്കളുടെ ഹൈലൈറ്റ്സ് തിരിച്ചറിയുന്നു",
@ -4386,16 +4462,15 @@
"time_label_day": "{number} മിനിറ്റ്",
"settings_pane_button_label": "നിങ്ങളുടെ പുതിയ ടാബ് താള് ഇഷ്ടാനുസൃതമാക്കുക",
"settings_pane_header": "പുതിയ ടാബിന്റെ മുൻഗണനകൾ",
"settings_pane_body": "പുതിയ ടാബ് തുറക്കുമ്പോൾ എന്ത് കാണണമെന്ന് തീരുമാനിക്കുക.",
"settings_pane_search_header": "തിരയുക",
"settings_pane_search_body": "പുതിയ ടാബിൽ നിന്ന് ഇന്റർനെറ്റിൽ തിരയുക.",
"settings_pane_topsites_header": "മുന്നേറിയ സൈറ്റുകൾ",
"settings_pane_topsites_body": "നിങ്ങൾ കൂടുതൽ സന്ദർശിക്കുന്ന വെബ്‌സൈറ്റുകളിൽ പ്രവേശിക്കുക.",
"settings_pane_topsites_options_showmore": "രണ്ടു വരികൾ കാണിയ്ക്കുക",
"settings_pane_bookmarks_header": "അടുത്തിടെയുള്ള ബുക്ക്മാർക്കുകൾ",
"settings_pane_bookmarks_body": "നിങ്ങളുടെ പുതിയതായി സൃഷ്ടിച്ച ബുക്ക്മാർക്കുകൾ ഒരു സ്ഥലത്ത്.",
"settings_pane_visit_again_header": "വീണ്ടും സന്ദർശിക്കുക",
"settings_pane_highlights_header": "ഹൈലൈറ്റുകൾ",
"settings_pane_highlights_body": "നിങ്ങളുടെ സമീപകാല ബ്രൗസിംഗ് ചരിത്രവും പുതുതായി സൃഷ്ടിച്ച അടയാളങ്ങളും കാണുക.",
"settings_pane_pocketstories_header": "മികച്ച ലേഖനങ്ങൾ",
"settings_pane_pocketstories_body": "മോസില്ല‌ കുടുംബാംഗമായ പോക്കറ്റ്, വിട്ടുപോയേയ്ക്കാവുന്ന മികച്ച ലേഖനങ്ങൾ നിങ്ങളുടെ ശ്രദ്ധയിൽ എത്തിയ്ക്കുന്നു.",
"settings_pane_done_button": "തീർന്നു",
"edit_topsites_button_text": "തിരുത്തുക",
"edit_topsites_button_label": "നിങ്ങളുടെ മുന്നേറിയ സൈറ്റുകളുടെ വിഭാഗം ഇഷ്ടാനുസൃതമാക്കുക",
@ -4418,8 +4493,8 @@
"pocket_read_more": "ജനപ്രിയ വിഷയങ്ങൾ:",
"pocket_read_even_more": "കൂടുതൽ ലേഖനങ്ങൾ കാണുക",
"pocket_feedback_header": "250 ലക്ഷം പേരാൽ തെരഞ്ഞെടുക്കപ്പെട്ട വെബിലെ ഏറ്റവും മികച്ചവയാണിവ.",
"pocket_feedback_body": "മോസില്ല‌ കുടുംബാംഗമായ പോക്കറ്റ്, വിട്ടുപോയേയ്ക്കാവുന്ന മികച്ച ലേഖനങ്ങൾ നിങ്ങളുടെ ശ്രദ്ധയിൽ എത്തിയ്ക്കുന്നു.",
"pocket_send_feedback": "പ്രതികരണം അയയ്ക്കുക"
"manual_migration_cancel_button": "വേണ്ട, നന്ദി",
"manual_migration_import_button": "ഇപ്പോൾ ഇറക്കുമതി ചെയ്യുക"
},
"mr": {
"newtab_page_title": "नवीन टॅब",
@ -4742,6 +4817,7 @@
"time_label_day": "{number} दिन",
"settings_pane_button_label": "तपाईंको नयाँ ट्याब पृष्ठ अनुकूलन गर्नुहोस्",
"settings_pane_header": "नयाँ ट्याब प्राथमिकताहरू",
"settings_pane_body2": "तपाईँले यो पृष्ठमा के देख्नुभयो छनौट गर्नुहोस् ।",
"settings_pane_search_header": "खोजी गर्नुहोस्",
"settings_pane_search_body": "तपाईंको नयाँ ट्याबबाट वेबमा खोज्नुहोस् ।",
"settings_pane_topsites_header": "शीर्ष साइटहरू",
@ -6220,10 +6296,13 @@
"settings_pane_bookmarks_header": "ที่คั่นหน้าล่าสุด",
"settings_pane_bookmarks_body": "ที่คั่นหน้าที่สร้างใหม่ของคุณในตำแหน่งที่ตั้งเดียวที่สะดวก",
"settings_pane_visit_again_header": "เยี่ยมชมอีกครั้ง",
"settings_pane_visit_again_body": "Firefox จะแสดงประวัติการท่องเว็บที่คุณอาจต้องการให้จดจำหรือกลับไปเยี่ยมชมอีกครั้งที่นี่",
"settings_pane_highlights_header": "รายการเด่น",
"settings_pane_highlights_body2": "ค้นหาทางของคุณกลับไปยังสิ่งที่น่าสนใจที่คุณได้เยี่ยมชมหรือเพิ่มที่คั่นหน้าไว้เมื่อเร็ว ๆ นี้",
"settings_pane_highlights_options_bookmarks": "ที่คั่นหน้า",
"settings_pane_highlights_options_visited": "ไซต์ที่เยี่ยมชมแล้ว",
"settings_pane_snippets_header": "ส่วนย่อย",
"settings_pane_snippets_body": "อ่านบทความข่าวสารสั้น ๆ ที่น่าสนใจจาก Mozilla เกี่ยวกับ Firefox, วัฒนธรรมอินเทอร์เน็ต, และมีมแบบสุ่มในบางครั้ง",
"settings_pane_done_button": "เสร็จสิ้น",
"edit_topsites_button_text": "แก้ไข",
"edit_topsites_button_label": "ปรับแต่งส่วนไซต์เด่นของคุณ",
@ -6246,7 +6325,9 @@
"pocket_read_more": "หัวข้อยอดนิยม:",
"pocket_read_even_more": "ดูเรื่องราวเพิ่มเติม",
"pocket_feedback_header": "ที่สุดของเว็บ จัดรายการโดยผู้คนกว่า 25 ล้านคน",
"highlights_empty_state": "เริ่มการท่องเว็บและเราจะแสดงบทความ, วิดีโอ และหน้าอื่น ๆ บางส่วนที่ยอดเยี่ยมที่คุณได้เยี่ยมชมหรือเพิ่มที่คั่นหน้าไว้เมื่อเร็ว ๆ นี้ที่นี่",
"pocket_description": "ค้นพบเนื้อหาคุณภาพสูงที่คุณอาจจะพลาดไปด้วยความช่วยเหลือจาก Pocket ซึ่งขณะนี้เป็นส่วนหนึ่งของ Mozilla",
"highlights_empty_state": "เริ่มการท่องเว็บและเราจะแสดงบทความ, วิดีโอ และหน้าอื่น ๆ บางส่วนที่ยอดเยี่ยมที่คุณได้เยี่ยมชมหรือเพิ่มที่คั่นหน้าไว้ล่าสุดที่นี่",
"topstories_empty_state": "คุณได้อ่านเรื่องราวครบทั้งหมดแล้ว คุณสามารถกลับมาตรวจดูเรื่องราวเด่นจาก {provider} ได้ภายหลัง อดใจรอไม่ได้งั้นหรือ? เลือกหัวข้อยอดนิยมเพื่อค้นหาเรื่องราวที่ยอดเยี่ยมจากเว็บต่าง ๆ",
"manual_migration_explanation2": "ลอง Firefox ด้วยที่คั่นหน้า, ประวัติ และรหัสผ่านจากเบราว์เซอร์อื่น",
"manual_migration_cancel_button": "ไม่ ขอบคุณ",
"manual_migration_import_button": "นำเข้าตอนนี้"

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

@ -8,7 +8,7 @@
<em:type>2</em:type>
<em:bootstrap>true</em:bootstrap>
<em:unpack>false</em:unpack>
<em:version>2017.10.05.1066-43726d35</em:version>
<em:version>2017.10.05.1357-d2d5439b</em:version>
<em:name>Activity Stream</em:name>
<em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
<em:multiprocessCompatible>true</em:multiprocessCompatible>

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

@ -21,11 +21,16 @@ XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Screenshots",
"resource://activity-stream/lib/Screenshots.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
"resource://gre/modules/ProfileAge.jsm");
const HIGHLIGHTS_MAX_LENGTH = 9;
const HIGHLIGHTS_UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
const MANY_EXTRA_LENGTH = HIGHLIGHTS_MAX_LENGTH * 5 + TOP_SITES_SHOWMORE_LENGTH;
const SECTION_ID = "highlights";
const BOOKMARKS_THRESHOLD = 4000; // 3 seconds to milliseconds.
// Some default seconds ago for Activity Stream recent requests
const ACTIVITY_STREAM_DEFAULT_RECENT = 5 * 24 * 60 * 60;
this.HighlightsFeed = class HighlightsFeed {
constructor() {
@ -33,15 +38,8 @@ this.HighlightsFeed = class HighlightsFeed {
this.highlightsLength = 0;
this.dedupe = new Dedupe(this._dedupeKey);
this.linksCache = new LinksCache(NewTabUtils.activityStreamLinks,
"getHighlights", (oldLink, newLink) => {
// Migrate any pending images or images to the new link
for (const property of ["__fetchingScreenshot", "image"]) {
const oldValue = oldLink[property];
if (oldValue) {
newLink[property] = oldValue;
}
}
});
"getHighlights", ["image"]);
this._profileAge = 0;
}
_dedupeKey(site) {
@ -61,6 +59,20 @@ this.HighlightsFeed = class HighlightsFeed {
SectionsManager.disableSection(SECTION_ID);
}
/**
* Timeframe used to select recent bookmarks, in seconds.
* Looks back 5 days while also taking into account new profiles
* not to include default bookmarks.
*/
async _getBookmarksThreshold() {
if (this._profileAge === 0) {
// Value in milliseconds.
this._profileAge = await (new ProfileAge()).created;
}
const defaultsThreshold = Date.now() - this._profileAge - BOOKMARKS_THRESHOLD;
return Math.min(ACTIVITY_STREAM_DEFAULT_RECENT, defaultsThreshold / 1000);
}
async fetchHighlights(broadcast = false) {
// We broadcast when we want to force an update, so get fresh links
if (broadcast) {
@ -81,7 +93,10 @@ this.HighlightsFeed = class HighlightsFeed {
// Request more than the expected length to allow for items being removed by
// deduping against Top Sites or multiple history from the same domain, etc.
const manyPages = await this.linksCache.request({numItems: MANY_EXTRA_LENGTH});
const manyPages = await this.linksCache.request({
numItems: MANY_EXTRA_LENGTH,
bookmarkSecondsAgo: await this._getBookmarksThreshold()
});
// Remove adult highlights if we need to
const checkedAdult = this.store.getState().Prefs.values.filterAdult ?
@ -117,9 +132,8 @@ this.HighlightsFeed = class HighlightsFeed {
highlights.push(page);
hosts.add(hostname);
// Remove any internal properties
delete page.__fetchingScreenshot;
delete page.__updateCache;
// Remove internal properties that might be updated after dispatch
delete page.__sharedCache;
// Skip the rest if we have enough items
if (highlights.length === HIGHLIGHTS_MAX_LENGTH) {
@ -139,7 +153,7 @@ this.HighlightsFeed = class HighlightsFeed {
async fetchImage(page) {
// Request a screenshot if we don't already have one pending
const {preview_image_url: imageUrl, url} = page;
Screenshots.maybeGetAndSetScreenshot(page, imageUrl || url, "image", image => {
Screenshots.maybeCacheScreenshot(page, imageUrl || url, "image", image => {
SectionsManager.updateSectionCard(SECTION_ID, url, {image}, true);
});
}
@ -166,6 +180,9 @@ this.HighlightsFeed = class HighlightsFeed {
break;
case at.PLACES_BOOKMARK_ADDED:
case at.PLACES_BOOKMARK_REMOVED:
this.linksCache.expire();
this.fetchHighlights(false);
break;
case at.TOP_SITES_UPDATED:
this.fetchHighlights(false);
break;

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

@ -19,19 +19,21 @@ this.LinksCache = class LinksCache {
*
* @param {object} linkObject Object containing the link property
* @param {string} linkProperty Name of property on object to access
* @param {function} migrator Optional callback receiving the old and new link
* to allow custom migrating data from old to new.
* @param {array} properties Optional properties list to migrate to new links.
* @param {function} shouldRefresh Optional callback receiving the old and new
* options to refresh even when not expired.
*/
constructor(linkObject, linkProperty, migrator = () => {}, shouldRefresh = () => {}) {
constructor(linkObject, linkProperty, properties = [], shouldRefresh = () => {}) {
this.clear();
// Allow getting links from both methods and array properties
this.linkGetter = options => {
const ret = linkObject[linkProperty];
return typeof ret === "function" ? ret.call(linkObject, options) : ret;
};
this.migrator = migrator;
// Always migrate the shared cache data in addition to any custom properties
this.migrateProperties = ["__sharedCache", ...properties];
this.shouldRefresh = shouldRefresh;
}
@ -78,37 +80,38 @@ this.LinksCache = class LinksCache {
}
}
// Make a shallow copy of each resulting link to allow direct edits
const copied = (await this.linkGetter(options)).map(link => link &&
Object.assign({}, link));
// Update the cache with migrated links without modifying source objects
resolve((await this.linkGetter(options)).map(link => {
// Keep original array hole positions
if (!link) {
return link;
}
// Migrate data to the new link if we have an old link
for (const newLink of copied) {
if (newLink) {
const oldLink = toMigrate.get(newLink.url);
if (oldLink) {
this.migrator(oldLink, newLink);
// Migrate data to the new link copy if we have an old link
const newLink = Object.assign({}, link);
const oldLink = toMigrate.get(newLink.url);
if (oldLink) {
for (const property of this.migrateProperties) {
const oldValue = oldLink[property];
if (oldValue) {
newLink[property] = oldValue;
}
}
// Add a method that can be copied to cloned links that will update
// the original cached link's property with the current one
newLink.__updateCache = function(prop) {
const val = this[prop];
if (val === undefined) {
delete newLink[prop];
} else {
newLink[prop] = val;
} else {
// Share data among link copies and new links from future requests
newLink.__sharedCache = {
// Provide a helper to update the cached link
updateLink(property, value) {
newLink[property] = value;
}
};
}
}
// Update cache with the copied links migrated
resolve(copied);
return newLink;
}));
});
}
// Return the promise of the links array
return this.cache;
// Provide a shallow copy of the cached link objects for callers to modify
return (await this.cache).map(link => link && Object.assign({}, link));
}
};

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

@ -65,36 +65,31 @@ this.Screenshots = {
/**
* Conditionally get a screenshot for a link if there's no existing pending
* screenshot. Updates the link object's desired property with the result.
* screenshot. Updates the cached link's desired property with the result.
*
* @param link {object} Link object to update
* @param url {string} Url to get a screenshot of
* @param property {string} Name of property on object to set
@ @param onScreenshot {function} Callback for when the screenshot loads
*/
async maybeGetAndSetScreenshot(link, url, property, onScreenshot) {
// Make a link copy so we can stash internal properties to cache
const updateCache = link.__updateCache ? link.__updateCache.bind(link) :
() => {};
// Request a screenshot if we don't already have one pending
if (!link.__fetchingScreenshot) {
link.__fetchingScreenshot = this.getScreenshotForURL(url);
updateCache("__fetchingScreenshot");
// Trigger this callback only when first requesting
link.__fetchingScreenshot.then(onScreenshot).catch();
async maybeCacheScreenshot(link, url, property, onScreenshot) {
// Nothing to do if we already have a pending screenshot
const cache = link.__sharedCache;
if (cache.fetchingScreenshot) {
return;
}
// Clean up now that we got the screenshot
const screenshot = await link.__fetchingScreenshot;
delete link.__fetchingScreenshot;
updateCache("__fetchingScreenshot");
// Save the promise to the cache so other links get it immediately
cache.fetchingScreenshot = this.getScreenshotForURL(url);
// Update the link so the screenshot is in the cache
// Clean up now that we got the screenshot
const screenshot = await cache.fetchingScreenshot;
delete cache.fetchingScreenshot;
// Update the cache for future links and call back for existing content
if (screenshot) {
link[property] = screenshot;
updateCache(property);
cache.updateLink(property, screenshot);
onScreenshot(screenshot);
}
}
};

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

@ -6,8 +6,10 @@
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/EventEmitter.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
/*
* Generators for built in sections, keyed by the pref name for their feed.
@ -21,7 +23,7 @@ const BUILT_IN_SECTIONS = {
titleString: {id: "header_recommended_by", values: {provider: options.provider_name}},
descString: {id: options.provider_description || "pocket_feedback_body"}
},
shouldHidePref: options.hidden,
shouldHidePref: options.hidden,
eventSource: "TOP_STORIES",
icon: options.provider_icon,
title: {id: "header_recommended_by", values: {provider: options.provider_name}},
@ -74,13 +76,21 @@ const SectionsManager = {
for (const feedPrefName of Object.keys(BUILT_IN_SECTIONS)) {
const optionsPrefName = `${feedPrefName}.options`;
this.addBuiltInSection(feedPrefName, prefs[optionsPrefName]);
this._dedupeConfiguration = [];
this.sections.forEach(section => {
if (section.dedupeFrom) {
this._dedupeConfiguration.push({
id: section.id,
dedupeFrom: section.dedupeFrom
});
}
});
}
Object.keys(this.CONTEXT_MENU_PREFS).forEach(k =>
Services.prefs.addObserver(this.CONTEXT_MENU_PREFS[k], this));
this.dedupe = new Dedupe(site => site && site.url);
this.initialized = true;
this.emit(this.INIT);
},
@ -129,33 +139,28 @@ const SectionsManager = {
},
updateSection(id, options, shouldBroadcast) {
this.updateSectionContextMenuOptions(options);
if (this.sections.has(id)) {
const dedupedOptions = this.dedupeRows(id, options);
this.sections.set(id, Object.assign(this.sections.get(id), dedupedOptions));
this.emit(this.UPDATE_SECTION, id, dedupedOptions, shouldBroadcast);
// Update any sections that dedupe from the updated section
this.sections.forEach(section => {
if (section.dedupeFrom && section.dedupeFrom.includes(id)) {
this.updateSection(section.id, section, shouldBroadcast);
}
});
const optionsWithDedupe = Object.assign({}, options, {dedupeConfigurations: this._dedupeConfiguration});
this.sections.set(id, Object.assign(this.sections.get(id), options));
this.emit(this.UPDATE_SECTION, id, optionsWithDedupe, shouldBroadcast);
}
},
dedupeRows(id, options) {
const newOptions = Object.assign({}, options);
const dedupeFrom = this.sections.get(id).dedupeFrom;
if (dedupeFrom && dedupeFrom.length > 0 && options.rows) {
for (const sectionId of dedupeFrom) {
const section = this.sections.get(sectionId);
if (section && section.rows) {
const [, newRows] = this.dedupe.group(section.rows, options.rows);
newOptions.rows = newRows;
}
updateBookmarkMetadata({url}) {
this.sections.forEach(section => {
if (section.rows) {
section.rows.forEach(card => {
if (card.url === url && card.description && card.title && card.image) {
PlacesUtils.history.update({
url: card.url,
title: card.title,
description: card.description,
previewImageURL: card.image
});
}
});
}
}
return newOptions;
});
},
/**
@ -292,6 +297,9 @@ class SectionsFeed {
}
break;
}
case at.PLACES_BOOKMARK_ADDED:
SectionsManager.updateBookmarkMetadata(action.data);
break;
case at.SECTION_DISABLE:
SectionsManager.disableSection(action.data);
break;

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

@ -28,7 +28,9 @@ const ACTIVITY_STREAM_ENDPOINT_PREF = "browser.newtabpage.activity-stream.teleme
const USER_PREFS_ENCODING = {
"showSearch": 1 << 0,
"showTopSites": 1 << 1,
"feeds.section.topstories": 1 << 2
"feeds.section.topstories": 1 << 2,
"feeds.section.highlights": 1 << 3,
"feeds.snippets": 1 << 4
};
const IMPRESSION_STATS_RESET_TIME = 60 * 60 * 1000; // 60 minutes

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

@ -33,11 +33,11 @@ this.TopSitesFeed = class TopSitesFeed {
this._tippyTopProvider = new TippyTopProvider();
this.dedupe = new Dedupe(this._dedupeKey);
this.frecentCache = new LinksCache(NewTabUtils.activityStreamLinks,
"getTopSites", this.getLinkMigrator(), (oldOptions, newOptions) =>
"getTopSites", ["screenshot"], (oldOptions, newOptions) =>
// Refresh if no old options or requesting more items
!(oldOptions.numItems >= newOptions.numItems));
this.pinnedCache = new LinksCache(NewTabUtils.pinnedLinks,
"links", this.getLinkMigrator(["favicon", "faviconSize"]));
this.pinnedCache = new LinksCache(NewTabUtils.pinnedLinks, "links",
["favicon", "faviconSize", "screenshot"]);
}
_dedupeKey(site) {
return site && site.hostname;
@ -59,22 +59,6 @@ this.TopSitesFeed = class TopSitesFeed {
}
}
/**
* Make a cached link data migrator by copying over screenshots and others.
*
* @param others {array} Optional extra properties to copy
*/
getLinkMigrator(others = []) {
const properties = ["__fetchingScreenshot", "screenshot", ...others];
return (oldLink, newLink) => {
for (const property of properties) {
const oldValue = oldLink[property];
if (oldValue) {
newLink[property] = oldValue;
}
}
};
}
async getLinksWithDefaults(action) {
// Get at least SHOWMORE amount so toggling between 1 and 2 rows has sites
const numItems = Math.max(this.store.getState().Prefs.values.topSitesCount,
@ -107,8 +91,8 @@ this.TopSitesFeed = class TopSitesFeed {
try {
NewTabUtils.activityStreamProvider._faviconBytesToDataURI(await
NewTabUtils.activityStreamProvider._addFavicons([copy]));
copy.__updateCache("favicon");
copy.__updateCache("faviconSize");
copy.__sharedCache.updateLink("favicon", copy.favicon);
copy.__sharedCache.updateLink("faviconSize", copy.faviconSize);
} catch (e) {
// Some issue with favicon, so just continue without one
}
@ -134,9 +118,8 @@ this.TopSitesFeed = class TopSitesFeed {
if (link) {
this._fetchIcon(link);
// Remove any internal properties
delete link.__fetchingScreenshot;
delete link.__updateCache;
// Remove internal properties that might be updated after dispatch
delete link.__sharedCache;
}
}
@ -176,12 +159,11 @@ this.TopSitesFeed = class TopSitesFeed {
(!link.favicon || link.faviconSize < MIN_FAVICON_SIZE) &&
!link.screenshot) {
const {url} = link;
Screenshots.maybeGetAndSetScreenshot(link, url, "screenshot", screenshot => {
this.store.dispatch(ac.BroadcastToContent({
await Screenshots.maybeCacheScreenshot(link, url, "screenshot",
screenshot => this.store.dispatch(ac.BroadcastToContent({
data: {screenshot, url},
type: at.SCREENSHOT_UPDATED
}));
});
})));
}
}

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

@ -232,7 +232,8 @@ this.TopStoriesFeed = class TopStoriesFeed {
// Create a new array with a spoc inserted at index 2
// For now we're using the top scored spoc until we can support viewability based rotation
let rows = this.stories.slice(0, this.stories.length);
const position = SectionsManager.sections.get(SECTION_ID).order;
let rows = this.store.getState().Sections[position].rows.slice(0, this.stories.length);
rows.splice(2, 0, this.spocs[0]);
// Send a content update to the target tab
@ -282,14 +283,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
if (this.spocs) {
this.spocs = this.spocs.filter(s => s.url !== action.data.url);
}
if (this.stories) {
const prevStoriesLength = this.stories.length;
this.stories = this.stories.filter(s => s.url !== action.data.url);
if (prevStoriesLength !== this.stories.length) {
SectionsManager.updateSection(SECTION_ID, {rows: this.stories}, true);
}
}
break;
}
}

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

@ -356,6 +356,19 @@ describe("Reducers", () => {
const updatedSection = newState.find(section => section.id === "foo_bar_2");
assert.propertyVal(updatedSection, "initialized", false);
});
it("should dedupe based on dedupeConfigurations", () => {
const site = {url: "foo.com"};
const highlights = {rows: [site], id: "highlights"};
const topstories = {rows: [site], id: "topstories"};
const dedupeConfigurations = [{id: "topstories", dedupeFrom: ["highlights"]}];
const action = {data: {dedupeConfigurations}, type: "SECTION_UPDATE"};
const state = [highlights, topstories];
const nextState = Sections(state, action);
assert.equal(nextState.find(s => s.id === "highlights").rows.length, 1);
assert.equal(nextState.find(s => s.id === "topstories").rows.length, 0);
});
it("should remove blocked and deleted urls from all rows in all sections", () => {
const blockAction = {type: at.PLACES_LINK_BLOCKED, data: {url: "www.foo.bar"}};
const deleteAction = {type: at.PLACES_LINKS_DELETED, data: ["www.foo.bar"]};

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

@ -22,6 +22,7 @@ describe("Highlights Feed", () => {
let filterAdultStub;
let sectionsManagerStub;
let shortURLStub;
let profileAgeCreatedStub;
const fetchHighlights = async() => {
await feed.fetchHighlights();
@ -42,11 +43,18 @@ describe("Highlights Feed", () => {
};
fakeScreenshot = {
getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_IMAGE)),
maybeGetAndSetScreenshot: Screenshots.maybeGetAndSetScreenshot
maybeCacheScreenshot: Screenshots.maybeCacheScreenshot
};
filterAdultStub = sinon.stub().returns([]);
shortURLStub = sinon.stub().callsFake(site => site.url.match(/\/([^/]+)/)[1]);
const fakeProfileAgePromise = {};
const fakeProfileAge = function() { return fakeProfileAgePromise; };
profileAgeCreatedStub = sinon.stub().callsFake(() => Promise.resolve(42));
sinon.stub(fakeProfileAgePromise, "created").get(profileAgeCreatedStub);
globals.set("NewTabUtils", fakeNewTabUtils);
globals.set("ProfileAge", fakeProfileAge);
({HighlightsFeed, HIGHLIGHTS_UPDATE_TIME, SECTION_ID} = injector({
"lib/FilterAdult.jsm": {filterAdult: filterAdultStub},
"lib/ShortURL.jsm": {shortURL: shortURLStub},
@ -109,6 +117,7 @@ describe("Highlights Feed", () => {
const subscribeCallback = feed.store.subscribe.firstCall.args[0];
await subscribeCallback();
await firstFetch;
await feed._getBookmarksThreshold();
assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights);
// If TopSites is initialised in the first place it shouldn't wait
@ -117,6 +126,7 @@ describe("Highlights Feed", () => {
fakeNewTabUtils.activityStreamLinks.getHighlights.reset();
await feed.fetchHighlights();
assert.notCalled(feed.store.subscribe);
await feed._getBookmarksThreshold();
assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights);
});
it("should add hostname and hasImage to each link", async () => {
@ -215,8 +225,12 @@ describe("Highlights Feed", () => {
describe("#fetchImage", () => {
const FAKE_URL = "https://mozilla.org";
const FAKE_IMAGE_URL = "https://mozilla.org/preview.jpg";
function fetchImage(page) {
return feed.fetchImage(Object.assign({__sharedCache: {updateLink() {}}},
page));
}
it("should capture the image, if available", async () => {
await feed.fetchImage({
await fetchImage({
preview_image_url: FAKE_IMAGE_URL,
url: FAKE_URL
});
@ -225,13 +239,13 @@ describe("Highlights Feed", () => {
assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_IMAGE_URL);
});
it("should fall back to capturing a screenshot", async () => {
await feed.fetchImage({url: FAKE_URL});
await fetchImage({url: FAKE_URL});
assert.calledOnce(fakeScreenshot.getScreenshotForURL);
assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_URL);
});
it("should call SectionsManager.updateSectionCard with the right arguments", async () => {
await feed.fetchImage({
await fetchImage({
preview_image_url: FAKE_IMAGE_URL,
url: FAKE_URL
});
@ -239,7 +253,7 @@ describe("Highlights Feed", () => {
assert.calledOnce(sectionsManagerStub.updateSectionCard);
assert.calledWith(sectionsManagerStub.updateSectionCard, "highlights", FAKE_URL, {image: FAKE_IMAGE}, true);
});
it("should update the card with the image", async () => {
it("should not update the card with the image", async () => {
const card = {
preview_image_url: FAKE_IMAGE_URL,
url: FAKE_URL
@ -247,7 +261,29 @@ describe("Highlights Feed", () => {
await feed.fetchImage(card);
assert.propertyVal(card, "image", FAKE_IMAGE);
assert.notProperty(card, "image");
});
});
describe("#_getBookmarksThreshold", () => {
it("should have the correct default", () => {
assert.equal(feed._profileAge, 0);
});
it("should not call ProfileAge if _profileAge is set", async () => {
feed._profileAge = 10;
await feed._getBookmarksThreshold();
assert.notCalled(profileAgeCreatedStub);
});
it("should call ProfileAge if _profileAge is not set", async () => {
await feed._getBookmarksThreshold();
assert.calledOnce(profileAgeCreatedStub);
});
it("should set _profileAge", async () => {
await feed._getBookmarksThreshold();
assert.notEqual(feed._profileAge, 0);
});
});
describe("#uninit", () => {

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

@ -12,11 +12,14 @@ const FAKE_CARD_OPTIONS = {title: "Some fake title"};
describe("SectionsManager", () => {
let globals;
let fakeServices;
let fakePlacesUtils;
beforeEach(() => {
globals = new GlobalOverrider();
fakeServices = {prefs: {getBoolPref: sinon.spy(), addObserver: sinon.spy(), removeObserver: sinon.spy()}};
fakePlacesUtils = {history: {update: sinon.stub()}};
globals.set("Services", fakeServices);
globals.set("PlacesUtils", fakePlacesUtils);
});
afterEach(() => {
@ -132,10 +135,11 @@ describe("SectionsManager", () => {
it("should emit an UPDATE_SECTION event with correct arguments", () => {
SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);
const spy = sinon.spy();
const dedupeConfigurations = [{id: "topstories", dedupeFrom: ["highlights"]}];
SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);
SectionsManager.updateSection(FAKE_ID, {rows: FAKE_ROWS}, true);
assert.calledOnce(spy);
assert.calledWith(spy, SectionsManager.UPDATE_SECTION, FAKE_ID, {rows: FAKE_ROWS}, true);
assert.calledWith(spy, SectionsManager.UPDATE_SECTION, FAKE_ID, {rows: FAKE_ROWS, dedupeConfigurations}, true);
});
it("should do nothing if the section doesn't exist", () => {
SectionsManager.removeSection(FAKE_ID);
@ -205,25 +209,33 @@ describe("SectionsManager", () => {
assert.notCalled(spy);
});
});
describe("#dedupe", () => {
it("should dedupe stories from highlights", () => {
SectionsManager.init();
// Add some rows to highlights
SectionsManager.updateSection("highlights", {rows: [{url: "https://highlight.com/abc"}, {url: "https://shared.com/def"}]});
// Add some rows to top stories
SectionsManager.updateSection("topstories", {rows: [{url: "https://topstory.com/ghi"}, {url: "https://shared.com/def"}]});
// Verify deduping
assert.deepEqual(SectionsManager.sections.get("topstories").rows, [{url: "https://topstory.com/ghi"}]);
describe("#updateBookmarkMetadata", () => {
let rows;
beforeEach(() => {
rows = [{
url: "bar",
title: "title",
description: "description",
image: "image"
}];
SectionsManager.addSection(FAKE_ID, {rows});
});
it("should dedupe stories from highlights when updating highlights", () => {
SectionsManager.init();
// Add some rows to top stories
SectionsManager.updateSection("topstories", {rows: [{url: "https://topstory.com/ghi"}, {url: "https://shared.com/def"}]});
assert.deepEqual(SectionsManager.sections.get("topstories").rows, [{url: "https://topstory.com/ghi"}, {url: "https://shared.com/def"}]);
// Add some rows to highlights
SectionsManager.updateSection("highlights", {rows: [{url: "https://highlight.com/abc"}, {url: "https://shared.com/def"}]});
// Verify deduping
assert.deepEqual(SectionsManager.sections.get("topstories").rows, [{url: "https://topstory.com/ghi"}]);
it("shouldn't call PlacesUtils if no story", () => {
SectionsManager.updateBookmarkMetadata({url: "foo"});
assert.notCalled(fakePlacesUtils.history.update);
});
it("should call PlacesUtils", () => {
SectionsManager.updateBookmarkMetadata({url: "bar"});
assert.calledOnce(fakePlacesUtils.history.update);
assert.calledWithExactly(fakePlacesUtils.history.update, {
url: "bar",
title: "title",
description: "description",
previewImageURL: "image"
});
});
});
});
@ -426,5 +438,12 @@ describe("SectionsFeed", () => {
assert.neverCalledWith(spy, "ACTION_DISPATCHED", action);
}
});
it("should call updateBookmarkMetadata on PLACES_BOOKMARK_ADDED", () => {
const stub = sinon.stub(SectionsManager, "updateBookmarkMetadata");
feed.onAction({type: "PLACES_BOOKMARK_ADDED", data: {}});
assert.calledOnce(stub);
});
});
});

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

@ -61,7 +61,7 @@ describe("Top Sites Feed", () => {
};
fakeScreenshot = {
getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT)),
maybeGetAndSetScreenshot: Screenshots.maybeGetAndSetScreenshot
maybeCacheScreenshot: Screenshots.maybeCacheScreenshot
};
filterAdultStub = sinon.stub().returns([]);
shortURLStub = sinon.stub().callsFake(site => site.url);
@ -95,6 +95,10 @@ describe("Top Sites Feed", () => {
clock.restore();
});
function stubFaviconsToUseScreenshots() {
fakeNewTabUtils.activityStreamProvider._addFavicons = sandbox.stub();
}
describe("#refreshDefaults", () => {
it("should add defaults on PREFS_INITIAL_VALUES", () => {
feed.onAction({type: at.PREFS_INITIAL_VALUES, data: {"default.sites": "https://foo.com"}});
@ -136,7 +140,7 @@ describe("Top Sites Feed", () => {
describe("general", () => {
beforeEach(() => {
sandbox.stub(fakeScreenshot, "maybeGetAndSetScreenshot");
sandbox.stub(fakeScreenshot, "maybeCacheScreenshot");
});
it("should get the links from NewTabUtils", async () => {
const result = await feed.getLinksWithDefaults();
@ -241,8 +245,7 @@ describe("Top Sites Feed", () => {
assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites);
});
it("should migrate frecent screenshot data without getting screenshots again", async () => {
// Don't add favicons so we fall back to screenshots
fakeNewTabUtils.activityStreamProvider._addFavicons = sandbox.stub();
stubFaviconsToUseScreenshots();
await feed.getLinksWithDefaults();
const {callCount} = fakeScreenshot.getScreenshotForURL;
feed.frecentCache.expire();
@ -271,6 +274,36 @@ describe("Top Sites Feed", () => {
const internal = Object.keys(result[0]).filter(key => key.startsWith("__"));
assert.equal(internal.join(""), "");
});
describe("concurrency", () => {
let resolvers;
beforeEach(() => {
stubFaviconsToUseScreenshots();
resolvers = [];
fakeScreenshot.getScreenshotForURL = sandbox.spy(() => new Promise(
resolve => resolvers.push(resolve)));
});
const getTwice = () => Promise.all([feed.getLinksWithDefaults(), feed.getLinksWithDefaults()]);
const resolveAll = () => resolvers.forEach(resolve => resolve(FAKE_SCREENSHOT));
it("should call the backing data once", async () => {
await getTwice();
assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
});
it("should get screenshots once per link", async () => {
await getTwice();
assert.callCount(fakeScreenshot.getScreenshotForURL, FAKE_LINKS.length);
});
it("should dispatch once per link screenshot fetched", async () => {
await getTwice();
await resolveAll();
assert.callCount(feed.store.dispatch, FAKE_LINKS.length);
});
});
});
describe("deduping", () => {
beforeEach(() => {
@ -376,26 +409,26 @@ describe("Top Sites Feed", () => {
assert.propertyVal(link, "screenshot", "reuse.png");
});
it("should reuse existing fetching screenshot on the link", async () => {
const link = {__fetchingScreenshot: Promise.resolve("fetching.png")};
const link = {__sharedCache: {fetchingScreenshot: Promise.resolve("fetching.png")}};
await feed._fetchIcon(link);
assert.notCalled(fakeScreenshot.getScreenshotForURL);
assert.propertyVal(link, "screenshot", "fetching.png");
});
it("should get a screenshot if the link is missing it", () => {
feed._fetchIcon(FAKE_LINKS[0]);
feed._fetchIcon(Object.assign({__sharedCache: {}}, FAKE_LINKS[0]));
assert.calledOnce(fakeScreenshot.getScreenshotForURL);
assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_LINKS[0].url);
});
it("should update the link's cache if it can be updated", () => {
const link = {__updateCache: sandbox.stub()};
it("should update the link's cache with a screenshot", async () => {
const updateLink = sandbox.stub();
const link = {__sharedCache: {updateLink}};
feed._fetchIcon(link);
await feed._fetchIcon(link);
assert.calledOnce(link.__updateCache);
assert.calledWith(link.__updateCache, "__fetchingScreenshot");
assert.calledOnce(updateLink);
assert.calledWith(updateLink, "screenshot", FAKE_SCREENSHOT);
});
it("should skip getting a screenshot if there is a tippy top icon", () => {
feed._tippyTopProvider.processSite = site => {

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

@ -40,7 +40,7 @@ describe("Top Stories Feed", () => {
enableSection: sinon.spy(),
disableSection: sinon.spy(),
updateSection: sinon.spy(),
sections: new Map([["topstories", {options: FAKE_OPTIONS}]])
sections: new Map([["topstories", {order: 0, options: FAKE_OPTIONS}]])
};
class FakeUserDomainAffinityProvider {}
@ -377,7 +377,7 @@ describe("Top Stories Feed", () => {
const response = {
"settings": {"spocsPerNewTabs": 2},
"recommendations": [{"id": "rec1"}, {"id": "rec2"}, {"id": "rec3"}],
"recommendations": [{"guid": "rec1"}, {"guid": "rec2"}, {"guid": "rec3"}],
"spocs": [{"id": "spoc1"}, {"id": "spoc2"}]
};
@ -387,9 +387,12 @@ describe("Top Stories Feed", () => {
fetchStub.resolves({ok: true, status: 200, json: () => Promise.resolve(response)});
await instance.fetchStories();
instance.store.getState = () => ({Sections: [{rows: response.recommendations}]});
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
assert.calledOnce(instance.store.dispatch);
let action = instance.store.dispatch.firstCall.args[0];
assert.equal(at.SECTION_UPDATE, action.type);
assert.equal(true, action.meta.skipMain);
assert.equal(action.data.rows[0].guid, "rec1");
@ -429,6 +432,8 @@ describe("Top Stories Feed", () => {
assert.notCalled(instance.store.dispatch);
assert.equal(instance.contentUpdateQueue.length, 1);
instance.store.getState = () => ({Sections: [{rows: response.recommendations}]});
await instance.fetchStories();
assert.equal(instance.contentUpdateQueue.length, 0);
assert.calledOnce(instance.store.dispatch);
@ -538,15 +543,11 @@ describe("Top Stories Feed", () => {
assert.equal(instance.topicsLastUpdated, 0);
assert.equal(instance.affinityLastUpdated, 0);
});
it("should filter recs and spocs when link is blocked", () => {
instance.stories = [{"url": "not_blocked"}, {"url": "blocked"}];
it("should filter spocs when link is blocked", () => {
instance.spocs = [{"url": "not_blocked"}, {"url": "blocked"}];
instance.onAction({type: at.PLACES_LINK_BLOCKED, data: {url: "blocked"}});
assert.deepEqual(instance.stories, [{"url": "not_blocked"}]);
assert.deepEqual(instance.spocs, [{"url": "not_blocked"}]);
assert.calledOnce(sectionsManagerStub.updateSection);
assert.calledWith(sectionsManagerStub.updateSection, SECTION_ID, {rows: instance.stories});
});
});
});