Bug 1376984 - Add confirm dialog, startup perf improvements, other fixes to Activity Stream r=Mardak

MozReview-Commit-ID: 3730Mntj1XX
This commit is contained in:
k88hudson
2017-06-28 16:47:23 -07:00
parent 7613f6ff64
commit b2c38e1dcd
31 changed files with 1380 additions and 569 deletions

View File

@@ -7,4 +7,4 @@ via the browser.newtabpage.activity-stream.enabled pref.
The files in this directory, including vendor dependencies, are imported from the The files in this directory, including vendor dependencies, are imported from the
system-addon directory in https://github.com/mozilla/activity-stream. system-addon directory in https://github.com/mozilla/activity-stream.
Read [docs/v2-system-addon](https://github.com/mozilla/activity-stream/tree/master/docs/v2-system-addon) for more detail. Read [docs/v2-system-addon](https://github.com/mozilla/activity-stream/tree/master/docs/v2-system-addon/1.GETTING_STARTED.md) for more detail.

View File

@@ -4,17 +4,18 @@
"use strict"; "use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components; const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.importGlobalProperties(["fetch"]);
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["fetch"]);
XPCOMUtils.defineLazyModuleGetter(this, "Preferences", XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm"); "resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm"); "resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled"; const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
const BROWSER_READY_NOTIFICATION = "browser-delayed-startup-finished"; const BROWSER_READY_NOTIFICATION = "sessionstore-windows-restored";
const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF"; const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
const REASON_STARTUP_ON_PREF_CHANGE = "PREF_ON"; const REASON_STARTUP_ON_PREF_CHANGE = "PREF_ON";
const RESOURCE_BASE = "resource://activity-stream"; const RESOURCE_BASE = "resource://activity-stream";
@@ -62,7 +63,11 @@ function init(reason) {
} }
const options = Object.assign({}, startupData || {}, ACTIVITY_STREAM_OPTIONS); const options = Object.assign({}, startupData || {}, ACTIVITY_STREAM_OPTIONS);
activityStream = new ActivityStream(options); activityStream = new ActivityStream(options);
activityStream.init(reason); try {
activityStream.init(reason);
} catch (e) {
Cu.reportError(e);
}
} }
/** /**
@@ -113,7 +118,8 @@ function observe(subject, topic, data) {
switch (topic) { switch (topic) {
case BROWSER_READY_NOTIFICATION: case BROWSER_READY_NOTIFICATION:
Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION); Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION);
onBrowserReady(); // Avoid running synchronously during this event that's used for timing
setTimeout(() => onBrowserReady());
break; break;
} }
} }

View File

@@ -17,11 +17,20 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS :
// Export for tests // Export for tests
this.globalImportContext = globalImportContext; this.globalImportContext = globalImportContext;
const actionTypes = [ // Create an object that avoids accidental differing key/value pairs:
// {
// INIT: "INIT",
// UNINIT: "UNINIT"
// }
const actionTypes = {};
for (const type of [
"BLOCK_URL", "BLOCK_URL",
"BOOKMARK_URL", "BOOKMARK_URL",
"DELETE_BOOKMARK_BY_ID", "DELETE_BOOKMARK_BY_ID",
"DELETE_HISTORY_URL", "DELETE_HISTORY_URL",
"DELETE_HISTORY_URL_CONFIRM",
"DIALOG_CANCEL",
"DIALOG_OPEN",
"INIT", "INIT",
"LOCALE_UPDATED", "LOCALE_UPDATED",
"NEW_TAB_INITIAL_STATE", "NEW_TAB_INITIAL_STATE",
@@ -45,13 +54,9 @@ const actionTypes = [
"TELEMETRY_USER_EVENT", "TELEMETRY_USER_EVENT",
"TOP_SITES_UPDATED", "TOP_SITES_UPDATED",
"UNINIT" "UNINIT"
// The line below creates an object like this: ]) {
// { actionTypes[type] = type;
// INIT: "INIT", }
// UNINIT: "UNINIT"
// }
// It prevents accidentally adding a different key/value name.
].reduce((obj, type) => { obj[type] = type; return obj; }, {});
// Helper function for creating routed actions between content and main // Helper function for creating routed actions between content and main
// Not intended to be used by consumers // Not intended to be used by consumers

View File

@@ -25,6 +25,10 @@ const INITIAL_STATE = {
Prefs: { Prefs: {
initialized: false, initialized: false,
values: {} values: {}
},
Dialog: {
visible: false,
data: {}
} }
}; };
@@ -95,6 +99,19 @@ function TopSites(prevState = INITIAL_STATE.TopSites, action) {
} }
} }
function Dialog(prevState = INITIAL_STATE.Dialog, action) {
switch (action.type) {
case at.DIALOG_OPEN:
return Object.assign({}, prevState, {visible: true, data: action.data});
case at.DIALOG_CANCEL:
return Object.assign({}, prevState, {visible: false});
case at.DELETE_HISTORY_URL:
return Object.assign({}, INITIAL_STATE.Dialog);
default:
return prevState;
}
}
function Prefs(prevState = INITIAL_STATE.Prefs, action) { function Prefs(prevState = INITIAL_STATE.Prefs, action) {
let newValues; let newValues;
switch (action.type) { switch (action.type) {
@@ -110,6 +127,6 @@ function Prefs(prevState = INITIAL_STATE.Prefs, action) {
} }
this.INITIAL_STATE = INITIAL_STATE; this.INITIAL_STATE = INITIAL_STATE;
this.reducers = {TopSites, App, Prefs}; this.reducers = {TopSites, App, Prefs, Dialog};
this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"]; this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];

View File

@@ -97,16 +97,15 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS :
// Export for tests // Export for tests
const actionTypes = ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SCREENSHOT_UPDATED", "SET_PREF", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_UPDATED", "UNINIT" // Create an object that avoids accidental differing key/value pairs:
// The line below creates an object like this:
// { // {
// INIT: "INIT", // INIT: "INIT",
// UNINIT: "UNINIT" // UNINIT: "UNINIT"
// } // }
// It prevents accidentally adding a different key/value name. const actionTypes = {};
].reduce((obj, type) => { for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SCREENSHOT_UPDATED", "SET_PREF", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_UPDATED", "UNINIT"]) {
obj[type] = type;return obj; actionTypes[type] = type;
}, {}); }
// Helper function for creating routed actions between content and main // Helper function for creating routed actions between content and main
// Not intended to be used by consumers // Not intended to be used by consumers
@@ -312,9 +311,10 @@ var _require2 = __webpack_require__(3);
const addLocaleData = _require2.addLocaleData, const addLocaleData = _require2.addLocaleData,
IntlProvider = _require2.IntlProvider; IntlProvider = _require2.IntlProvider;
const TopSites = __webpack_require__(13); const TopSites = __webpack_require__(14);
const Search = __webpack_require__(12); const Search = __webpack_require__(13);
const PreferencesPane = __webpack_require__(11); const ConfirmDialog = __webpack_require__(9);
const PreferencesPane = __webpack_require__(12);
// Locales that should be displayed RTL // Locales that should be displayed RTL
const RTL_LIST = ["ar", "he", "fa", "ur"]; const RTL_LIST = ["ar", "he", "fa", "ur"];
@@ -373,7 +373,8 @@ class Base extends React.Component {
"main", "main",
null, null,
prefs.showSearch && React.createElement(Search, null), prefs.showSearch && React.createElement(Search, null),
prefs.showTopSites && React.createElement(TopSites, null) prefs.showTopSites && React.createElement(TopSites, null),
React.createElement(ConfirmDialog, null)
), ),
React.createElement(PreferencesPane, null) React.createElement(PreferencesPane, null)
) )
@@ -394,7 +395,7 @@ var _require = __webpack_require__(1);
const at = _require.actionTypes; const at = _require.actionTypes;
var _require2 = __webpack_require__(15); var _require2 = __webpack_require__(16);
const perfSvc = _require2.perfService; const perfSvc = _require2.perfService;
@@ -573,6 +574,10 @@ const INITIAL_STATE = {
Prefs: { Prefs: {
initialized: false, initialized: false,
values: {} values: {}
},
Dialog: {
visible: false,
data: {}
} }
}; };
@@ -657,6 +662,22 @@ function TopSites() {
} }
} }
function Dialog() {
let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Dialog;
let action = arguments[1];
switch (action.type) {
case at.DIALOG_OPEN:
return Object.assign({}, prevState, { visible: true, data: action.data });
case at.DIALOG_CANCEL:
return Object.assign({}, prevState, { visible: false });
case at.DELETE_HISTORY_URL:
return Object.assign({}, INITIAL_STATE.Dialog);
default:
return prevState;
}
}
function Prefs() { function Prefs() {
let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Prefs; let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Prefs;
let action = arguments[1]; let action = arguments[1];
@@ -674,7 +695,7 @@ function Prefs() {
} }
} }
var reducers = { TopSites, App, Prefs }; var reducers = { TopSites, App, Prefs, Dialog };
module.exports = { module.exports = {
reducers, reducers,
INITIAL_STATE INITIAL_STATE
@@ -693,6 +714,125 @@ module.exports = ReactDOM;
"use strict"; "use strict";
const React = __webpack_require__(0);
var _require = __webpack_require__(2);
const connect = _require.connect;
var _require2 = __webpack_require__(3);
const FormattedMessage = _require2.FormattedMessage;
var _require3 = __webpack_require__(1);
const actionTypes = _require3.actionTypes,
ac = _require3.actionCreators;
/**
* ConfirmDialog component.
* One primary action button, one cancel button.
*
* Content displayed is controlled by `data` prop the component receives.
* Example:
* data: {
* // Any sort of data needed to be passed around by actions.
* payload: site.url,
* // Primary button SendToMain action.
* action: "DELETE_HISTORY_URL",
* // Primary button USerEvent action.
* userEvent: "DELETE",
* // Array of locale ids to display.
* message_body: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"],
* // Text for primary button.
* confirm_button_string_id: "menu_action_delete"
* },
*/
const ConfirmDialog = React.createClass({
displayName: "ConfirmDialog",
getDefaultProps() {
return {
visible: false,
data: {}
};
},
_handleCancelBtn() {
this.props.dispatch({ type: actionTypes.DIALOG_CANCEL });
this.props.dispatch(ac.UserEvent({ event: actionTypes.DIALOG_CANCEL }));
},
_handleConfirmBtn() {
this.props.data.onConfirm.forEach(this.props.dispatch);
},
_renderModalMessage() {
const message_body = this.props.data.body_string_id;
if (!message_body) {
return null;
}
return React.createElement(
"span",
null,
message_body.map(msg => React.createElement(
"p",
{ key: msg },
React.createElement(FormattedMessage, { id: msg })
))
);
},
render() {
if (!this.props.visible) {
return null;
}
return React.createElement(
"div",
{ className: "confirmation-dialog" },
React.createElement("div", { className: "modal-overlay", onClick: this._handleCancelBtn }),
React.createElement(
"div",
{ className: "modal", ref: "modal" },
React.createElement(
"section",
{ className: "modal-message" },
this._renderModalMessage()
),
React.createElement(
"section",
{ className: "actions" },
React.createElement(
"button",
{ ref: "cancelButton", onClick: this._handleCancelBtn },
React.createElement(FormattedMessage, { id: "topsites_form_cancel_button" })
),
React.createElement(
"button",
{ ref: "confirmButton", className: "done", onClick: this._handleConfirmBtn },
React.createElement(FormattedMessage, { id: this.props.data.confirm_button_string_id })
)
)
)
);
}
});
module.exports = connect(state => state.Dialog)(ConfirmDialog);
module.exports._unconnected = ConfirmDialog;
module.exports.Dialog = ConfirmDialog;
/***/ }),
/* 10 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
const React = __webpack_require__(0); const React = __webpack_require__(0);
class ContextMenu extends React.Component { class ContextMenu extends React.Component {
@@ -770,7 +910,7 @@ class ContextMenu extends React.Component {
module.exports = ContextMenu; module.exports = ContextMenu;
/***/ }), /***/ }),
/* 10 */ /* 11 */
/***/ (function(module, exports, __webpack_require__) { /***/ (function(module, exports, __webpack_require__) {
"use strict"; "use strict";
@@ -782,95 +922,120 @@ var _require = __webpack_require__(3);
const injectIntl = _require.injectIntl; const injectIntl = _require.injectIntl;
const ContextMenu = __webpack_require__(9); const ContextMenu = __webpack_require__(10);
var _require2 = __webpack_require__(1); var _require2 = __webpack_require__(1);
const actionTypes = _require2.actionTypes, const at = _require2.actionTypes,
ac = _require2.actionCreators; ac = _require2.actionCreators;
class LinkMenu extends React.Component { const RemoveBookmark = site => ({
getBookmarkStatus(site) { id: "menu_action_remove_bookmark",
return site.bookmarkGuid ? { icon: "bookmark-remove",
id: "menu_action_remove_bookmark", action: ac.SendToMain({
icon: "bookmark-remove", type: at.DELETE_BOOKMARK_BY_ID,
action: "DELETE_BOOKMARK_BY_ID", data: site.bookmarkGuid
data: site.bookmarkGuid, }),
userEvent: "BOOKMARK_DELETE" userEvent: "BOOKMARK_DELETE"
} : { });
id: "menu_action_bookmark",
icon: "bookmark",
action: "BOOKMARK_URL",
data: site.url,
userEvent: "BOOKMARK_ADD"
};
}
getDefaultContextMenu(site) {
return [{
id: "menu_action_open_new_window",
icon: "new-window",
action: "OPEN_NEW_WINDOW",
data: { url: site.url },
userEvent: "OPEN_NEW_WINDOW"
}, {
id: "menu_action_open_private_window",
icon: "new-window-private",
action: "OPEN_PRIVATE_WINDOW",
data: { url: site.url },
userEvent: "OPEN_PRIVATE_WINDOW"
}];
}
getOptions() {
var _props = this.props;
const dispatch = _props.dispatch,
site = _props.site,
index = _props.index,
source = _props.source;
// default top sites have a limited set of context menu options const AddBookmark = site => ({
id: "menu_action_bookmark",
icon: "bookmark",
action: ac.SendToMain({
type: at.BOOKMARK_URL,
data: site.url
}),
userEvent: "BOOKMARK_ADD"
});
let options = this.getDefaultContextMenu(site); const OpenInNewWindow = site => ({
id: "menu_action_open_new_window",
icon: "new-window",
action: ac.SendToMain({
type: at.OPEN_NEW_WINDOW,
data: { url: site.url }
}),
userEvent: "OPEN_NEW_WINDOW"
});
// all other top sites have all the following context menu options const OpenInPrivateWindow = site => ({
if (!site.isDefault) { id: "menu_action_open_private_window",
options = [this.getBookmarkStatus(site), { type: "separator" }, ...options, { type: "separator" }, { icon: "new-window-private",
id: "menu_action_dismiss", action: ac.SendToMain({
icon: "dismiss", type: at.OPEN_PRIVATE_WINDOW,
action: "BLOCK_URL", data: { url: site.url }
data: site.url, }),
userEvent: "BLOCK" userEvent: "OPEN_PRIVATE_WINDOW"
}, { });
id: "menu_action_delete",
icon: "delete", const BlockUrl = site => ({
action: "DELETE_HISTORY_URL", id: "menu_action_dismiss",
data: site.url, icon: "dismiss",
userEvent: "DELETE" action: ac.SendToMain({
}]; type: at.BLOCK_URL,
data: site.url
}),
userEvent: "BLOCK"
});
const DeleteUrl = site => ({
id: "menu_action_delete",
icon: "delete",
action: {
type: at.DIALOG_OPEN,
data: {
onConfirm: [ac.SendToMain({ type: at.DELETE_HISTORY_URL, data: site.url }), ac.UserEvent({ event: "DELETE" })],
body_string_id: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"],
confirm_button_string_id: "menu_action_delete"
} }
options.forEach(option => { },
userEvent: "DIALOG_OPEN"
});
class LinkMenu extends React.Component {
getOptions() {
const props = this.props;
const site = props.site;
const isBookmark = site.bookmarkGuid;
const isDefault = site.isDefault;
const options = [
// Bookmarks
!isDefault && (isBookmark ? RemoveBookmark(site) : AddBookmark(site)), !isDefault && { type: "separator" },
// Menu items for all sites
OpenInNewWindow(site), OpenInPrivateWindow(site),
// Blocking and deleting
!isDefault && { type: "separator" }, !isDefault && BlockUrl(site), !isDefault && DeleteUrl(site)].filter(o => o).map(option => {
const action = option.action, const action = option.action,
data = option.data,
id = option.id, id = option.id,
type = option.type, type = option.type,
userEvent = option.userEvent; userEvent = option.userEvent;
// Convert message ids to localized labels and add onClick function
if (!type && id) { if (!type && id) {
option.label = this.props.intl.formatMessage(option); option.label = props.intl.formatMessage(option);
option.onClick = () => { option.onClick = () => {
dispatch(ac.SendToMain({ type: actionTypes[action], data })); props.dispatch(action);
dispatch(ac.UserEvent({ if (userEvent) {
event: userEvent, props.dispatch(ac.UserEvent({
source, event: userEvent,
action_position: index source: props.source,
})); action_position: props.index
}));
}
}; };
} }
return option;
}); });
// this is for a11y - we want to know which item is the first and which item // This is for accessibility to support making each item tabbable.
// is the last, so we can close the context menu accordingly // We want to know which item is the first and which item
// is the last, so we can close the context menu accordingly.
options[0].first = true; options[0].first = true;
options[options.length - 1].last = true; options[options.length - 1].last = true;
return options; return options;
@@ -887,7 +1052,7 @@ module.exports = injectIntl(LinkMenu);
module.exports._unconnected = LinkMenu; module.exports._unconnected = LinkMenu;
/***/ }), /***/ }),
/* 11 */ /* 12 */
/***/ (function(module, exports, __webpack_require__) { /***/ (function(module, exports, __webpack_require__) {
"use strict"; "use strict";
@@ -904,8 +1069,6 @@ var _require2 = __webpack_require__(3);
const injectIntl = _require2.injectIntl, const injectIntl = _require2.injectIntl,
FormattedMessage = _require2.FormattedMessage; FormattedMessage = _require2.FormattedMessage;
const classNames = __webpack_require__(16);
var _require3 = __webpack_require__(1); var _require3 = __webpack_require__(1);
const ac = _require3.actionCreators; const ac = _require3.actionCreators;
@@ -967,7 +1130,7 @@ class PreferencesPane extends React.Component {
"div", "div",
{ className: "prefs-pane-button" }, { className: "prefs-pane-button" },
React.createElement("button", { React.createElement("button", {
className: classNames("prefs-button icon", isVisible ? "icon-dismiss" : "icon-settings"), className: `prefs-button icon ${isVisible ? "icon-dismiss" : "icon-settings"}`,
title: props.intl.formatMessage({ id: isVisible ? "settings_pane_done_button" : "settings_pane_button_label" }), title: props.intl.formatMessage({ id: isVisible ? "settings_pane_done_button" : "settings_pane_button_label" }),
onClick: this.togglePane }) onClick: this.togglePane })
), ),
@@ -976,7 +1139,7 @@ class PreferencesPane extends React.Component {
{ className: "prefs-pane" }, { className: "prefs-pane" },
React.createElement( React.createElement(
"div", "div",
{ className: classNames("sidebar", { hidden: !isVisible }) }, { className: `sidebar ${isVisible ? "" : "hidden"}` },
React.createElement( React.createElement(
"div", "div",
{ className: "prefs-modal-inner-wrapper" }, { className: "prefs-modal-inner-wrapper" },
@@ -1015,7 +1178,7 @@ module.exports.PreferencesPane = PreferencesPane;
module.exports.PreferencesInput = PreferencesInput; module.exports.PreferencesInput = PreferencesInput;
/***/ }), /***/ }),
/* 12 */ /* 13 */
/***/ (function(module, exports, __webpack_require__) { /***/ (function(module, exports, __webpack_require__) {
"use strict"; "use strict";
@@ -1114,7 +1277,7 @@ module.exports = connect()(injectIntl(Search));
module.exports._unconnected = Search; module.exports._unconnected = Search;
/***/ }), /***/ }),
/* 13 */ /* 14 */
/***/ (function(module, exports, __webpack_require__) { /***/ (function(module, exports, __webpack_require__) {
"use strict"; "use strict";
@@ -1130,8 +1293,8 @@ var _require2 = __webpack_require__(3);
const FormattedMessage = _require2.FormattedMessage; const FormattedMessage = _require2.FormattedMessage;
const shortURL = __webpack_require__(14); const shortURL = __webpack_require__(15);
const LinkMenu = __webpack_require__(10); const LinkMenu = __webpack_require__(11);
var _require3 = __webpack_require__(1); var _require3 = __webpack_require__(1);
@@ -1161,7 +1324,7 @@ class TopSite extends React.Component {
dispatch = _props.dispatch; dispatch = _props.dispatch;
const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index; const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index;
const title = shortURL(link); const title = link.pinTitle || shortURL(link);
const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`; const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`;
const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`; const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`;
const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" }; const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
@@ -1183,8 +1346,13 @@ class TopSite extends React.Component {
), ),
React.createElement( React.createElement(
"div", "div",
{ className: "title" }, { className: `title ${link.isPinned ? "pinned" : ""}` },
title link.isPinned && React.createElement("div", { className: "icon icon-pin-small" }),
React.createElement(
"span",
null,
title
)
) )
), ),
React.createElement( React.createElement(
@@ -1235,7 +1403,7 @@ module.exports._unconnected = TopSites;
module.exports.TopSite = TopSite; module.exports.TopSite = TopSite;
/***/ }), /***/ }),
/* 14 */ /* 15 */
/***/ (function(module, exports, __webpack_require__) { /***/ (function(module, exports, __webpack_require__) {
"use strict"; "use strict";
@@ -1270,7 +1438,7 @@ module.exports = function shortURL(link) {
}; };
/***/ }), /***/ }),
/* 15 */ /* 16 */
/***/ (function(module, exports, __webpack_require__) { /***/ (function(module, exports, __webpack_require__) {
"use strict"; "use strict";
@@ -1374,61 +1542,6 @@ module.exports = {
perfService perfService
}; };
/***/ }),
/* 16 */
/***/ (function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*!
Copyright (c) 2016 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/* global define */
(function () {
'use strict';
var hasOwn = {}.hasOwnProperty;
function classNames () {
var classes = [];
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg) continue;
var argType = typeof arg;
if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
classes.push(classNames.apply(null, arg));
} else if (argType === 'object') {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
return classes.join(' ');
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = classNames;
} else if (true) {
// register as 'classnames', consistent with npm package name
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function () {
return classNames;
}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
} else {
window.classNames = classNames;
}
}());
/***/ }), /***/ }),
/* 17 */ /* 17 */
/***/ (function(module, exports) { /***/ (function(module, exports) {

View File

@@ -44,6 +44,11 @@ input {
background-image: url("assets/glyph-newWindow-private-16.svg"); } background-image: url("assets/glyph-newWindow-private-16.svg"); }
.icon.icon-settings { .icon.icon-settings {
background-image: url("assets/glyph-settings-16.svg"); } background-image: url("assets/glyph-settings-16.svg"); }
.icon.icon-pin-small {
background-image: url("assets/glyph-pin-12.svg");
background-size: 12px;
height: 12px;
width: 12px; }
html, html,
body, body,
@@ -222,7 +227,7 @@ main {
background-color: #FFF; background-color: #FFF;
border-radius: 6px; border-radius: 6px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
background-size: 250%; background-size: cover;
background-position: top left; background-position: top left;
transition: opacity 1s; transition: opacity 1s;
opacity: 0; } opacity: 0; }
@@ -232,11 +237,20 @@ main {
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
text-align: center; text-align: center;
white-space: nowrap;
font-size: 11px; font-size: 11px;
overflow: hidden; width: 96px;
text-overflow: ellipsis; position: relative; }
width: 96px; } .top-sites-list .top-site-outer .title .icon {
offset-inline-start: 0;
position: absolute;
top: 10px; }
.top-sites-list .top-site-outer .title span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.top-sites-list .top-site-outer .title.pinned span {
padding: 0 13px; }
.search-wrapper { .search-wrapper {
cursor: default; cursor: default;
@@ -422,13 +436,16 @@ main {
width: 21px; } width: 21px; }
.prefs-pane [type='checkbox']:not(:checked) + label::after, .prefs-pane [type='checkbox']:not(:checked) + label::after,
.prefs-pane [type='checkbox']:checked + label::after { .prefs-pane [type='checkbox']:checked + label::after {
background: url("chrome://global/skin/in-content/check.svg#check") no-repeat center center; background: url("chrome://global/skin/in-content/check.svg") no-repeat center center;
content: ''; content: '';
height: 21px; height: 21px;
offset-inline-start: 0; offset-inline-start: 0;
position: absolute; position: absolute;
top: 0; top: 0;
width: 21px; } width: 21px;
-moz-context-properties: fill, stroke;
fill: #1691D2;
stroke: none; }
.prefs-pane [type='checkbox']:not(:checked) + label::after { .prefs-pane [type='checkbox']:not(:checked) + label::after {
opacity: 0; } opacity: 0; }
.prefs-pane [type='checkbox']:checked + label::after { .prefs-pane [type='checkbox']:checked + label::after {
@@ -454,3 +471,42 @@ main {
.prefs-pane-button button:dir(rtl) { .prefs-pane-button button:dir(rtl) {
left: 5px; left: 5px;
right: auto; } right: auto; }
.confirmation-dialog .modal {
position: fixed;
width: 400px;
top: 20%;
left: 50%;
margin-left: -200px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.08); }
.confirmation-dialog section {
margin: 0; }
.confirmation-dialog .modal-message {
padding: 24px; }
.confirmation-dialog .actions {
justify-content: flex-end; }
.confirmation-dialog .actions button {
margin-inline-end: 16px; }
.confirmation-dialog .actions button.done {
margin-inline-start: 0;
margin-inline-end: 0; }
.modal-overlay {
background: #FBFBFB;
height: 100%;
left: 0;
opacity: 0.8;
position: fixed;
top: 0;
width: 100%;
z-index: 11001; }
.modal {
background: #FFF;
border: solid 1px rgba(0, 0, 0, 0.1);
border-radius: 3px;
font-size: 14px;
z-index: 11002; }

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<style>
path {
fill: #D7D7DB;
}
</style>
<path d="M10.53,9.47,8.25,7.19,9.8,5.643a.694.694,0,0,0,0-.98,3.04,3.04,0,0,0-2.161-.894H7.517A1.673,1.673,0,0,1,5.846,2.1V1.692A.693.693,0,0,0,4.664,1.2L1.2,4.664a.693.693,0,0,0,.49,1.182H2.1A1.672,1.672,0,0,1,3.769,7.517v.117a2.8,2.8,0,0,0,.925,2.192A.693.693,0,0,0,5.643,9.8L7.19,8.251l2.28,2.28A.75.75,0,0,0,10.53,9.47Z"/>
</svg>

After

Width:  |  Height:  |  Size: 479 B

View File

@@ -56,9 +56,12 @@
"default_label_loading": "يُحمّل…", "default_label_loading": "يُحمّل…",
"header_top_sites": "المواقع الأكثر زيارة", "header_top_sites": "المواقع الأكثر زيارة",
"header_highlights": "أهم الأحداث", "header_highlights": "أهم الأحداث",
"header_stories": "أهم الأخبار",
"header_stories_from": "من",
"type_label_visited": "مُزارة", "type_label_visited": "مُزارة",
"type_label_bookmarked": "معلّمة", "type_label_bookmarked": "معلّمة",
"type_label_synced": "مُزامنة من جهاز آخر", "type_label_synced": "مُزامنة من جهاز آخر",
"type_label_recommended": "مُتداول",
"type_label_open": "مفتوحة", "type_label_open": "مفتوحة",
"type_label_topic": "الموضوع", "type_label_topic": "الموضوع",
"menu_action_bookmark": "علّم", "menu_action_bookmark": "علّم",
@@ -69,6 +72,7 @@
"menu_action_open_private_window": "افتح في نافذة خاصة جديدة", "menu_action_open_private_window": "افتح في نافذة خاصة جديدة",
"menu_action_dismiss": "ألغِ", "menu_action_dismiss": "ألغِ",
"menu_action_delete": "احذف من التأريخ", "menu_action_delete": "احذف من التأريخ",
"menu_action_save_to_pocket": "احفظ في Pocket",
"search_for_something_with": "ابحث عن {search_term} مستخدما:", "search_for_something_with": "ابحث عن {search_term} مستخدما:",
"search_button": "ابحث", "search_button": "ابحث",
"search_header": "بحث {search_engine_name}", "search_header": "بحث {search_engine_name}",
@@ -91,6 +95,8 @@
"settings_pane_topsites_options_showmore": "اعرض صفّين", "settings_pane_topsites_options_showmore": "اعرض صفّين",
"settings_pane_highlights_header": "أهم الأحداث", "settings_pane_highlights_header": "أهم الأحداث",
"settings_pane_highlights_body": "اطّلع على تأريخ التصفح الأحدث، و العلامات المنشأة حديثًا.", "settings_pane_highlights_body": "اطّلع على تأريخ التصفح الأحدث، و العلامات المنشأة حديثًا.",
"settings_pane_pocketstories_header": "أهم المواضيع",
"settings_pane_pocketstories_body": "يساعدك Pocket –عضو في أسرة موزيلا– على الوصول إلى محتوى عالِ الجودة ربما لم يُكن ليتاح لك بدونه.",
"settings_pane_done_button": "تمّ", "settings_pane_done_button": "تمّ",
"edit_topsites_button_text": "حرِّر", "edit_topsites_button_text": "حرِّر",
"edit_topsites_button_label": "خصص قسم المواقع الأكثر زيارة", "edit_topsites_button_label": "خصص قسم المواقع الأكثر زيارة",
@@ -98,8 +104,23 @@
"edit_topsites_showless_button": "اعرض أقل", "edit_topsites_showless_button": "اعرض أقل",
"edit_topsites_done_button": "تمّ", "edit_topsites_done_button": "تمّ",
"edit_topsites_pin_button": "ثبّت هذا الموقع", "edit_topsites_pin_button": "ثبّت هذا الموقع",
"edit_topsites_unpin_button": "افصل هذا الموقع",
"edit_topsites_edit_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": "أفضل ما في الوِب، انتقاها أكثر من ٢٥ مليون شخص.",
"pocket_feedback_body": "يساعدك Pocket –عضو في أسرة موزيلا– على الوصول إلى محتوى عالِ الجودة ربما لم يُكن ليتاح لك بدونه.",
"pocket_send_feedback": "أرسل انطباعك"
}, },
"as": {}, "as": {},
"ast": { "ast": {
@@ -533,10 +554,12 @@
"default_label_loading": "Indlæser…", "default_label_loading": "Indlæser…",
"header_top_sites": "Mest besøgte websider", "header_top_sites": "Mest besøgte websider",
"header_highlights": "Højdepunkter", "header_highlights": "Højdepunkter",
"header_stories": "Tophistorier",
"header_stories_from": "fra", "header_stories_from": "fra",
"type_label_visited": "Besøgt", "type_label_visited": "Besøgt",
"type_label_bookmarked": "Bogmærket", "type_label_bookmarked": "Bogmærket",
"type_label_synced": "Synkroniseret fra en anden enhed", "type_label_synced": "Synkroniseret fra en anden enhed",
"type_label_recommended": "Populært",
"type_label_open": "Åben", "type_label_open": "Åben",
"type_label_topic": "Emne", "type_label_topic": "Emne",
"menu_action_bookmark": "Bogmærk", "menu_action_bookmark": "Bogmærk",
@@ -570,6 +593,8 @@
"settings_pane_topsites_options_showmore": "Vis to rækker", "settings_pane_topsites_options_showmore": "Vis to rækker",
"settings_pane_highlights_header": "Højdepunkter", "settings_pane_highlights_header": "Højdepunkter",
"settings_pane_highlights_body": "Se tilbage på din seneste browserhistorik og nyligt oprettede bogmærker.", "settings_pane_highlights_body": "Se tilbage på din seneste browserhistorik og nyligt oprettede bogmærker.",
"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", "settings_pane_done_button": "Færdig",
"edit_topsites_button_text": "Rediger", "edit_topsites_button_text": "Rediger",
"edit_topsites_button_label": "Tilpas afsnittet Mest besøgte websider", "edit_topsites_button_label": "Tilpas afsnittet Mest besøgte websider",
@@ -581,6 +606,7 @@
"edit_topsites_edit_button": "Rediger denne webside", "edit_topsites_edit_button": "Rediger denne webside",
"edit_topsites_dismiss_button": "Afvis denne webside", "edit_topsites_dismiss_button": "Afvis denne webside",
"edit_topsites_add_button": "Tilføj", "edit_topsites_add_button": "Tilføj",
"topsites_form_add_header": "Ny webside",
"topsites_form_edit_header": "Rediger mest besøgte webside", "topsites_form_edit_header": "Rediger mest besøgte webside",
"topsites_form_title_placeholder": "Indtast en titel", "topsites_form_title_placeholder": "Indtast en titel",
"topsites_form_url_placeholder": "Indtast eller indsæt en URL", "topsites_form_url_placeholder": "Indtast eller indsæt en URL",
@@ -590,6 +616,8 @@
"topsites_form_url_validation": "Gyldig URL påkrævet", "topsites_form_url_validation": "Gyldig URL påkrævet",
"pocket_read_more": "Populære emner:", "pocket_read_more": "Populære emner:",
"pocket_read_even_more": "Se flere historier", "pocket_read_even_more": "Se flere historier",
"pocket_feedback_header": "Det bedste fra nettet, udvalgt af mere end 25 millioner mennesker.",
"pocket_feedback_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.",
"pocket_send_feedback": "Send feedback" "pocket_send_feedback": "Send feedback"
}, },
"de": { "de": {
@@ -859,7 +887,6 @@
"newtab_page_title": "New Tab", "newtab_page_title": "New Tab",
"default_label_loading": "Loading…", "default_label_loading": "Loading…",
"header_top_sites": "Top Sites", "header_top_sites": "Top Sites",
"header_highlights": "Highlights",
"header_stories": "Top Stories", "header_stories": "Top Stories",
"header_visit_again": "Visit Again", "header_visit_again": "Visit Again",
"header_bookmarks": "Recent Bookmarks", "header_bookmarks": "Recent Bookmarks",
@@ -879,6 +906,8 @@
"menu_action_open_private_window": "Open in a New Private Window", "menu_action_open_private_window": "Open in a New Private Window",
"menu_action_dismiss": "Dismiss", "menu_action_dismiss": "Dismiss",
"menu_action_delete": "Delete from History", "menu_action_delete": "Delete from History",
"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", "menu_action_save_to_pocket": "Save to Pocket",
"search_for_something_with": "Search for {search_term} with:", "search_for_something_with": "Search for {search_term} with:",
"search_button": "Search", "search_button": "Search",
@@ -900,8 +929,6 @@
"settings_pane_topsites_header": "Top Sites", "settings_pane_topsites_header": "Top Sites",
"settings_pane_topsites_body": "Access the websites you visit most.", "settings_pane_topsites_body": "Access the websites you visit most.",
"settings_pane_topsites_options_showmore": "Show two rows", "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_header": "Recent Bookmarks",
"settings_pane_bookmarks_body": "Your newly created bookmarks in one handy location.", "settings_pane_bookmarks_body": "Your newly created bookmarks in one handy location.",
"settings_pane_visit_again_header": "Visit Again", "settings_pane_visit_again_header": "Visit Again",
@@ -971,9 +998,12 @@
"default_label_loading": "Cargando…", "default_label_loading": "Cargando…",
"header_top_sites": "Más visitados", "header_top_sites": "Más visitados",
"header_highlights": "Destacados", "header_highlights": "Destacados",
"header_stories": "Historias principales",
"header_stories_from": "de",
"type_label_visited": "Visitados", "type_label_visited": "Visitados",
"type_label_bookmarked": "Marcados", "type_label_bookmarked": "Marcados",
"type_label_synced": "Sincronizados de otro dispositivo", "type_label_synced": "Sincronizados de otro dispositivo",
"type_label_recommended": "Tendencias",
"type_label_open": "Abrir", "type_label_open": "Abrir",
"type_label_topic": "Tópico", "type_label_topic": "Tópico",
"menu_action_bookmark": "Marcador", "menu_action_bookmark": "Marcador",
@@ -984,6 +1014,7 @@
"menu_action_open_private_window": "Abrir en nueva ventana privada", "menu_action_open_private_window": "Abrir en nueva ventana privada",
"menu_action_dismiss": "Descartar", "menu_action_dismiss": "Descartar",
"menu_action_delete": "Borrar del historial", "menu_action_delete": "Borrar del historial",
"menu_action_save_to_pocket": "Guardar en Pocket",
"search_for_something_with": "Buscar {search_term} con:", "search_for_something_with": "Buscar {search_term} con:",
"search_button": "Buscar", "search_button": "Buscar",
"search_header": "Buscar con {search_engine_name}", "search_header": "Buscar con {search_engine_name}",
@@ -1006,6 +1037,8 @@
"settings_pane_topsites_options_showmore": "Mostrar dos filas", "settings_pane_topsites_options_showmore": "Mostrar dos filas",
"settings_pane_highlights_header": "Destacados", "settings_pane_highlights_header": "Destacados",
"settings_pane_highlights_body": "Mirar hacia atrás el historial de navegación reciente y los marcadores recién creados.", "settings_pane_highlights_body": "Mirar hacia atrás el historial de navegación reciente y los marcadores recién creados.",
"settings_pane_pocketstories_header": "Historias principales",
"settings_pane_pocketstories_body": "Pocket, parte de la familia Mozilla, ayudará a conectarte con contenido de alta calidad que no podrías haber encontrado de otra forma.",
"settings_pane_done_button": "Listo", "settings_pane_done_button": "Listo",
"edit_topsites_button_text": "Editar", "edit_topsites_button_text": "Editar",
"edit_topsites_button_label": "Personalizar la sección de sitios más visitados", "edit_topsites_button_label": "Personalizar la sección de sitios más visitados",
@@ -1013,8 +1046,23 @@
"edit_topsites_showless_button": "Mostrar menos", "edit_topsites_showless_button": "Mostrar menos",
"edit_topsites_done_button": "Listo", "edit_topsites_done_button": "Listo",
"edit_topsites_pin_button": "Pegar este sitio", "edit_topsites_pin_button": "Pegar este sitio",
"edit_topsites_unpin_button": "Despegar este sitio",
"edit_topsites_edit_button": "Editar este sitio", "edit_topsites_edit_button": "Editar este sitio",
"edit_topsites_dismiss_button": "Descartar este sitio" "edit_topsites_dismiss_button": "Descartar este sitio",
"edit_topsites_add_button": "Agregar",
"topsites_form_add_header": "Nuevo sitio más visitado",
"topsites_form_edit_header": "Editar sitio más visitado",
"topsites_form_title_placeholder": "Ingresar un título",
"topsites_form_url_placeholder": "Escribir o pegar URL",
"topsites_form_add_button": "Agregar",
"topsites_form_save_button": "Guardar",
"topsites_form_cancel_button": "Cancelar",
"topsites_form_url_validation": "Se requiere URL válida",
"pocket_read_more": "Tópicos populares:",
"pocket_read_even_more": "Ver más historias",
"pocket_feedback_header": "Lo mejor de la web, seleccionado por más de 25 millones de personas.",
"pocket_feedback_body": "Pocket, parte de la familia Mozilla, ayudará a conectarte con contenido de alta calidad que no podrías haber encontrado de otra forma.",
"pocket_send_feedback": "Enviar opinión"
}, },
"es-CL": { "es-CL": {
"newtab_page_title": "Nueva pestaña", "newtab_page_title": "Nueva pestaña",
@@ -1142,9 +1190,12 @@
"default_label_loading": "Cargando…", "default_label_loading": "Cargando…",
"header_top_sites": "Sitios favoritos", "header_top_sites": "Sitios favoritos",
"header_highlights": "Destacados", "header_highlights": "Destacados",
"header_stories": "Historias populares",
"header_stories_from": "de",
"type_label_visited": "Visitados", "type_label_visited": "Visitados",
"type_label_bookmarked": "Marcados", "type_label_bookmarked": "Marcados",
"type_label_synced": "Sincronizado desde otro dispositivo", "type_label_synced": "Sincronizado desde otro dispositivo",
"type_label_recommended": "Tendencias",
"type_label_open": "Abrir", "type_label_open": "Abrir",
"type_label_topic": "Tema", "type_label_topic": "Tema",
"menu_action_bookmark": "Marcador", "menu_action_bookmark": "Marcador",
@@ -1155,6 +1206,7 @@
"menu_action_open_private_window": "Abrir en una Nueva Ventana Privada", "menu_action_open_private_window": "Abrir en una Nueva Ventana Privada",
"menu_action_dismiss": "Descartar", "menu_action_dismiss": "Descartar",
"menu_action_delete": "Eliminar del historial", "menu_action_delete": "Eliminar del historial",
"menu_action_save_to_pocket": "Guardar en Pocket",
"search_for_something_with": "Buscar {search_term} con:", "search_for_something_with": "Buscar {search_term} con:",
"search_button": "Buscar", "search_button": "Buscar",
"search_header": "Buscar {search_engine_name}", "search_header": "Buscar {search_engine_name}",
@@ -1177,6 +1229,8 @@
"settings_pane_topsites_options_showmore": "Mostrar dos filas", "settings_pane_topsites_options_showmore": "Mostrar dos filas",
"settings_pane_highlights_header": "Destacados", "settings_pane_highlights_header": "Destacados",
"settings_pane_highlights_body": "Ve tu historial de navegación reciente y tus marcadores recién creados.", "settings_pane_highlights_body": "Ve tu historial de navegación reciente y tus marcadores recién creados.",
"settings_pane_pocketstories_header": "Historias populares",
"settings_pane_pocketstories_body": "Pocket, miembro de la familia Mozilla, te ayuda a conectarte con contenido de alta calidad que tal vez no hubieras encontrado de otra forma.",
"settings_pane_done_button": "Listo", "settings_pane_done_button": "Listo",
"edit_topsites_button_text": "Editar", "edit_topsites_button_text": "Editar",
"edit_topsites_button_label": "Personalizar la sección de tus sitios preferidos", "edit_topsites_button_label": "Personalizar la sección de tus sitios preferidos",
@@ -1184,17 +1238,35 @@
"edit_topsites_showless_button": "Mostrar menos", "edit_topsites_showless_button": "Mostrar menos",
"edit_topsites_done_button": "Listo", "edit_topsites_done_button": "Listo",
"edit_topsites_pin_button": "Fijar este sitio", "edit_topsites_pin_button": "Fijar este sitio",
"edit_topsites_unpin_button": "Despegar este sitio",
"edit_topsites_edit_button": "Editar este sitio", "edit_topsites_edit_button": "Editar este sitio",
"edit_topsites_dismiss_button": "Descartar este sitio" "edit_topsites_dismiss_button": "Descartar este sitio",
"edit_topsites_add_button": "Agregar",
"topsites_form_add_header": "Nuevo sitio popular",
"topsites_form_edit_header": "Editar sitio popular",
"topsites_form_title_placeholder": "Introducir un título",
"topsites_form_url_placeholder": "Escribir o pegar una URL",
"topsites_form_add_button": "Agregar",
"topsites_form_save_button": "Guardar",
"topsites_form_cancel_button": "Cancelar",
"topsites_form_url_validation": "Se requiere una URL válida",
"pocket_read_more": "Temas populares:",
"pocket_read_even_more": "Ver más historias",
"pocket_feedback_header": "Lo mejor de la web, seleccionado por más 25 millones de personas.",
"pocket_feedback_body": "Pocket, miembro de la familia Mozilla, te ayuda a conectarte con contenido de alta calidad que tal vez no hubieras encontrado de otra forma.",
"pocket_send_feedback": "Enviar opinión"
}, },
"et": { "et": {
"newtab_page_title": "Uus kaart", "newtab_page_title": "Uus kaart",
"default_label_loading": "Laadimine…", "default_label_loading": "Laadimine…",
"header_top_sites": "Top saidid", "header_top_sites": "Top saidid",
"header_highlights": "Esiletõstetud", "header_highlights": "Esiletõstetud",
"header_stories": "Top lood",
"header_stories_from": "allikast",
"type_label_visited": "Külastatud", "type_label_visited": "Külastatud",
"type_label_bookmarked": "Järjehoidjatest", "type_label_bookmarked": "Järjehoidjatest",
"type_label_synced": "Sünkroniseeritud teisest seadmest", "type_label_synced": "Sünkroniseeritud teisest seadmest",
"type_label_recommended": "Menukad",
"type_label_open": "Avatud", "type_label_open": "Avatud",
"type_label_topic": "Teema", "type_label_topic": "Teema",
"menu_action_bookmark": "Lisa järjehoidjatesse", "menu_action_bookmark": "Lisa järjehoidjatesse",
@@ -1205,6 +1277,7 @@
"menu_action_open_private_window": "Ava uues privaatses aknas", "menu_action_open_private_window": "Ava uues privaatses aknas",
"menu_action_dismiss": "Peida", "menu_action_dismiss": "Peida",
"menu_action_delete": "Kustuta ajaloost", "menu_action_delete": "Kustuta ajaloost",
"menu_action_save_to_pocket": "Salvesta Pocketisse",
"search_for_something_with": "Otsi fraasi {search_term}, kasutades otsingumootorit:", "search_for_something_with": "Otsi fraasi {search_term}, kasutades otsingumootorit:",
"search_button": "Otsi", "search_button": "Otsi",
"search_header": "{search_engine_name}", "search_header": "{search_engine_name}",
@@ -1227,6 +1300,8 @@
"settings_pane_topsites_options_showmore": "Kuvatakse kahel real", "settings_pane_topsites_options_showmore": "Kuvatakse kahel real",
"settings_pane_highlights_header": "Esiletõstetud", "settings_pane_highlights_header": "Esiletõstetud",
"settings_pane_highlights_body": "Tagasivaade hiljutisele lehitsemisajaloole ning lisatud järjehoidjatele.", "settings_pane_highlights_body": "Tagasivaade hiljutisele lehitsemisajaloole ning lisatud järjehoidjatele.",
"settings_pane_pocketstories_header": "Top lood",
"settings_pane_pocketstories_body": "Pocket, osana Mozilla perekonnast, aitab sul leida kvaliteetset sisu, mida sa muidu poleks ehk leidnud.",
"settings_pane_done_button": "Valmis", "settings_pane_done_button": "Valmis",
"edit_topsites_button_text": "Muuda", "edit_topsites_button_text": "Muuda",
"edit_topsites_button_label": "Kohanda top saitide osa", "edit_topsites_button_label": "Kohanda top saitide osa",
@@ -1245,7 +1320,11 @@
"topsites_form_add_button": "Lisa", "topsites_form_add_button": "Lisa",
"topsites_form_save_button": "Salvesta", "topsites_form_save_button": "Salvesta",
"topsites_form_cancel_button": "Tühista", "topsites_form_cancel_button": "Tühista",
"topsites_form_url_validation": "URL peab olema korrektne",
"pocket_read_more": "Populaarsed teemad:", "pocket_read_more": "Populaarsed teemad:",
"pocket_read_even_more": "Rohkem lugusid",
"pocket_feedback_header": "Parim osa veebist, mille on kokku pannud rohkem kui 25 miljonit inimest.",
"pocket_feedback_body": "Pocket, osana Mozilla perekonnast, aitab sul leida kvaliteetset sisu, mida sa muidu poleks ehk leidnud.",
"pocket_send_feedback": "Saada tagasisidet" "pocket_send_feedback": "Saada tagasisidet"
}, },
"eu": {}, "eu": {},
@@ -1509,9 +1588,12 @@
"default_label_loading": "Á Luchtú…", "default_label_loading": "Á Luchtú…",
"header_top_sites": "Barrshuímh", "header_top_sites": "Barrshuímh",
"header_highlights": "Buaicphointí", "header_highlights": "Buaicphointí",
"header_stories": "Barrscéalta",
"header_stories_from": "ó",
"type_label_visited": "Feicthe", "type_label_visited": "Feicthe",
"type_label_bookmarked": "Leabharmharcáilte", "type_label_bookmarked": "Leabharmharcáilte",
"type_label_synced": "Sioncronaithe ó ghléas eile", "type_label_synced": "Sioncronaithe ó ghléas eile",
"type_label_recommended": "Treochtáil",
"type_label_open": "Oscailte", "type_label_open": "Oscailte",
"type_label_topic": "Ábhar", "type_label_topic": "Ábhar",
"menu_action_bookmark": "Cruthaigh leabharmharc", "menu_action_bookmark": "Cruthaigh leabharmharc",
@@ -1522,7 +1604,9 @@
"menu_action_open_private_window": "Oscail i bhFuinneog Nua Phríobháideach", "menu_action_open_private_window": "Oscail i bhFuinneog Nua Phríobháideach",
"menu_action_dismiss": "Ruaig", "menu_action_dismiss": "Ruaig",
"menu_action_delete": "Scrios ón Stair", "menu_action_delete": "Scrios ón Stair",
"menu_action_save_to_pocket": "Sábháil in Pocket",
"search_for_something_with": "Déan cuardach ar {search_term} le:", "search_for_something_with": "Déan cuardach ar {search_term} le:",
"search_button": "Cuardach",
"search_header": "Cuardach {search_engine_name}", "search_header": "Cuardach {search_engine_name}",
"search_web_placeholder": "Cuardaigh an Gréasán", "search_web_placeholder": "Cuardaigh an Gréasán",
"search_settings": "Socruithe Cuardaigh", "search_settings": "Socruithe Cuardaigh",
@@ -1532,7 +1616,43 @@
"time_label_less_than_minute": "< 1 n", "time_label_less_than_minute": "< 1 n",
"time_label_minute": "{number}n", "time_label_minute": "{number}n",
"time_label_hour": "{number}u", "time_label_hour": "{number}u",
"time_label_day": "{number}l" "time_label_day": "{number}l",
"settings_pane_button_label": "Saincheap an Leathanach do Chluaisín Nua",
"settings_pane_header": "Sainroghanna do Chluaisín Nua",
"settings_pane_body": "Roghnaigh na rudaí a fheicfidh tú nuair a osclóidh tú cluaisín nua.",
"settings_pane_search_header": "Cuardach",
"settings_pane_search_body": "Cuardaigh an Gréasán go díreach ón gcluaisín nua.",
"settings_pane_topsites_header": "Barrshuímh",
"settings_pane_topsites_body": "Na suímh Ghréasáin a dtugann tú cuairt orthu is minice.",
"settings_pane_topsites_options_showmore": "Taispeáin dhá shraith",
"settings_pane_highlights_header": "Buaicphointí",
"settings_pane_highlights_body": "Caith súil siar ar do stair bhrabhsála agus leabharmharcanna a chruthaigh tú le déanaí.",
"settings_pane_pocketstories_header": "Barrscéalta",
"settings_pane_pocketstories_body": "Le Pocket, ball de theaghlach Mozilla, beidh tú ábalta teacht ar ábhar den chéad scoth go héasca.",
"settings_pane_done_button": "Déanta",
"edit_topsites_button_text": "Eagar",
"edit_topsites_button_label": "Saincheap na Barrshuímh",
"edit_topsites_showmore_button": "Taispeáin níos mó",
"edit_topsites_showless_button": "Taispeáin níos lú",
"edit_topsites_done_button": "Déanta",
"edit_topsites_pin_button": "Greamaigh an suíomh seo",
"edit_topsites_unpin_button": "Díghreamaigh an suíomh seo",
"edit_topsites_edit_button": "Cuir an suíomh seo in eagar",
"edit_topsites_dismiss_button": "Ruaig an suíomh seo",
"edit_topsites_add_button": "Cuir leis",
"topsites_form_add_header": "Barrshuíomh Nua",
"topsites_form_edit_header": "Cuir an Barrshuíomh in Eagar",
"topsites_form_title_placeholder": "Cuir teideal isteach",
"topsites_form_url_placeholder": "Clóscríobh nó greamaigh URL",
"topsites_form_add_button": "Cuir leis",
"topsites_form_save_button": "Sábháil",
"topsites_form_cancel_button": "Cealaigh",
"topsites_form_url_validation": "URL neamhbhailí",
"pocket_read_more": "Topaicí i mbéal an phobail:",
"pocket_read_even_more": "Tuilleadh Scéalta",
"pocket_feedback_header": "Ábhar den chéad scoth ón Ghréasán, le níos mó ná 25 milliún duine i mbun coimeádaíochta.",
"pocket_feedback_body": "Le Pocket, ball de theaghlach Mozilla, beidh tú ábalta teacht ar ábhar den chéad scoth go héasca.",
"pocket_send_feedback": "Tabhair Aiseolas Dúinn"
}, },
"gd": { "gd": {
"newtab_page_title": "Taba ùr", "newtab_page_title": "Taba ùr",
@@ -1923,9 +2043,12 @@
"default_label_loading": "Memuat…", "default_label_loading": "Memuat…",
"header_top_sites": "Situs Teratas", "header_top_sites": "Situs Teratas",
"header_highlights": "Sorotan", "header_highlights": "Sorotan",
"header_stories": "Cerita Utama",
"header_stories_from": "dari",
"type_label_visited": "Dikunjungi", "type_label_visited": "Dikunjungi",
"type_label_bookmarked": "Dimarkahi", "type_label_bookmarked": "Dimarkahi",
"type_label_synced": "Disinkronkan dari perangkat lain", "type_label_synced": "Disinkronkan dari perangkat lain",
"type_label_recommended": "Trending",
"type_label_open": "Buka", "type_label_open": "Buka",
"type_label_topic": "Topik", "type_label_topic": "Topik",
"menu_action_bookmark": "Markah", "menu_action_bookmark": "Markah",
@@ -1936,6 +2059,7 @@
"menu_action_open_private_window": "Buka di Jendela Penjelajahan Pribadi Baru", "menu_action_open_private_window": "Buka di Jendela Penjelajahan Pribadi Baru",
"menu_action_dismiss": "Tutup", "menu_action_dismiss": "Tutup",
"menu_action_delete": "Hapus dari Riwayat", "menu_action_delete": "Hapus dari Riwayat",
"menu_action_save_to_pocket": "Simpan ke Pocket",
"search_for_something_with": "Cari {search_term} lewat:", "search_for_something_with": "Cari {search_term} lewat:",
"search_button": "Cari", "search_button": "Cari",
"search_header": "Pencarian {search_engine_name}", "search_header": "Pencarian {search_engine_name}",
@@ -1958,6 +2082,8 @@
"settings_pane_topsites_options_showmore": "Tampilkan dua baris", "settings_pane_topsites_options_showmore": "Tampilkan dua baris",
"settings_pane_highlights_header": "Sorotan", "settings_pane_highlights_header": "Sorotan",
"settings_pane_highlights_body": "Melihat kembali pada riwayat peramban terbaru dan markah yang baru dibuat.", "settings_pane_highlights_body": "Melihat kembali pada riwayat peramban terbaru dan markah yang baru dibuat.",
"settings_pane_pocketstories_header": "Cerita Utama",
"settings_pane_pocketstories_body": "Pocket, bagian dari keluarga Mozilla, akan membantu hubungkan Anda dengan konten berkualitas tinggi yang tak dapat Anda temukan di tempat lain.",
"settings_pane_done_button": "Selesai", "settings_pane_done_button": "Selesai",
"edit_topsites_button_text": "Sunting", "edit_topsites_button_text": "Sunting",
"edit_topsites_button_label": "Ubahsuai bagian Situs Teratas Anda", "edit_topsites_button_label": "Ubahsuai bagian Situs Teratas Anda",
@@ -1965,8 +2091,23 @@
"edit_topsites_showless_button": "Tampilkan lebih sedikit", "edit_topsites_showless_button": "Tampilkan lebih sedikit",
"edit_topsites_done_button": "Selesai", "edit_topsites_done_button": "Selesai",
"edit_topsites_pin_button": "Sematkan situs ini", "edit_topsites_pin_button": "Sematkan situs ini",
"edit_topsites_unpin_button": "Lepaskan situs ini",
"edit_topsites_edit_button": "Sunting situs ini", "edit_topsites_edit_button": "Sunting situs ini",
"edit_topsites_dismiss_button": "Abaikan situs ini" "edit_topsites_dismiss_button": "Abaikan situs ini",
"edit_topsites_add_button": "Tambah",
"topsites_form_add_header": "Situs Pilihan Baru",
"topsites_form_edit_header": "Ubah Situs Pilihan",
"topsites_form_title_placeholder": "Masukkan judul",
"topsites_form_url_placeholder": "Ketik atau tempel URL",
"topsites_form_add_button": "Tambah",
"topsites_form_save_button": "Simpan",
"topsites_form_cancel_button": "Batalkan",
"topsites_form_url_validation": "URL valid diperlukan",
"pocket_read_more": "Topik Populer:",
"pocket_read_even_more": "Lihat Cerita Lainnya",
"pocket_feedback_header": "Yang terbaik dari Web, dikurasi lebih dari 25 juta orang.",
"pocket_feedback_body": "Pocket, bagian dari keluarga Mozilla, akan membantu hubungkan Anda dengan konten berkualitas tinggi yang tak dapat Anda temukan di tempat lain.",
"pocket_send_feedback": "Kirim Umpanbalik"
}, },
"is": {}, "is": {},
"it": { "it": {
@@ -2111,7 +2252,77 @@
"pocket_feedback_body": "Mozilla ファミリーの一員となった Pocket は、他では見つからなかったかもしれない高品質なコンテンツとあなたを結び付ける手助けをします。", "pocket_feedback_body": "Mozilla ファミリーの一員となった Pocket は、他では見つからなかったかもしれない高品質なコンテンツとあなたを結び付ける手助けをします。",
"pocket_send_feedback": "フィードバックを送る" "pocket_send_feedback": "フィードバックを送る"
}, },
"ka": {}, "ka": {
"newtab_page_title": "ახალი ჩანართი",
"default_label_loading": "იტვირთება…",
"header_top_sites": "მთავარი საიტები",
"header_highlights": "გამორჩეულები",
"header_stories": "მთავარი სიახლეები",
"header_stories_from": "-იდან",
"type_label_visited": "მონახულებული",
"type_label_bookmarked": "ჩანიშნული",
"type_label_synced": "სხვა მოწყობილობიდან დასინქრონებული",
"type_label_recommended": "პოპულარული",
"type_label_open": "გახსნა",
"type_label_topic": "თემა",
"menu_action_bookmark": "ჩანიშვნა",
"menu_action_remove_bookmark": "სანიშნეებიდან ამოღება",
"menu_action_copy_address": "მისამართის დაკოპირება",
"menu_action_email_link": "ბმულის გაგზავნა…",
"menu_action_open_new_window": "ახალ ფანჯარაში გახსნა",
"menu_action_open_private_window": "ახალ პირად ფანჯარაში გახსნა",
"menu_action_dismiss": "დახურვა",
"menu_action_delete": "ისტორიიდან ამოშლა",
"menu_action_save_to_pocket": "Pocket-ში შენახვა",
"search_for_something_with": "{search_term} -ის ძიება:",
"search_button": "ძიება",
"search_header": "{search_engine_name} -ში ძიება",
"search_web_placeholder": "ინტერნეტში ძიება",
"search_settings": "ძიების პარამეტრების შეცვლა",
"welcome_title": "მოგესალმებით ახალ ჩანართზე",
"welcome_body": "Firefox ამ სივრცეს გამოიყენებს თქვენთვის ყველაზე საჭირო სანიშნეების, სტატიების, ვიდეოებისა და ბოლოს მონახულებული გვერდებისთვის, რომ ადვილად შეძლოთ მათზე დაბრუნება.",
"welcome_label": "რჩეული ვებ-გვერდების დადგენა",
"time_label_less_than_minute": "<1წთ",
"time_label_minute": "{number}წთ",
"time_label_hour": "{number}სთ",
"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_highlights_header": "გამორჩეულები",
"settings_pane_highlights_body": "ნახეთ ბოლოს მონახულებული გვერდების ისტორია და ახალი შექმნილი სანიშნეები.",
"settings_pane_pocketstories_header": "მთავარი სიახლეები",
"settings_pane_pocketstories_body": "Pocket არის Mozilla-ს ოჯახის ნაწილი, რომელიც დაგეხმარებათ ისეთი მაღალი ხარისხის კონტენტის მოძიებაში, რომელიც სხვა გზებით, შეიძლება ვერ მოგენახათ.",
"settings_pane_done_button": "მზადაა",
"edit_topsites_button_text": "ჩასწორება",
"edit_topsites_button_label": "მოირგეთ რჩეული საიტების განყოფილება",
"edit_topsites_showmore_button": "მეტის ჩვენება",
"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_add_button": "დამატება",
"topsites_form_add_header": "ახალი საიტი რჩეულებში",
"topsites_form_edit_header": "რჩეული საიტების ჩასწორება",
"topsites_form_title_placeholder": "სათაურის შეყვანა",
"topsites_form_url_placeholder": "აკრიფეთ ან ჩასვით URL",
"topsites_form_add_button": "დამატება",
"topsites_form_save_button": "შენახვა",
"topsites_form_cancel_button": "გაუქმება",
"topsites_form_url_validation": "საჭიროა მართებული URL",
"pocket_read_more": "პოპულარული თემები:",
"pocket_read_even_more": "მეტი სიახლის ნახვა",
"pocket_feedback_header": "საუკეთესოები ინტერნეტიდან, 25 მილიონზე მეტი ადამიანის მიერ არჩეული.",
"pocket_feedback_body": "Pocket არის Mozilla-ს ოჯახის ნაწილი, რომელიც დაგეხმარებათ ისეთი მაღალი ხარისხის კონტენტის მოძიებაში, რომელიც სხვა გზებით, შეიძლება ვერ მოგენახათ.",
"pocket_send_feedback": "უკუკავშირი"
},
"kab": { "kab": {
"newtab_page_title": "Iccer amaynut", "newtab_page_title": "Iccer amaynut",
"default_label_loading": "Asali…", "default_label_loading": "Asali…",
@@ -2176,6 +2387,7 @@
"topsites_form_url_validation": "Tansa URL tameɣtut tettwasra", "topsites_form_url_validation": "Tansa URL tameɣtut tettwasra",
"pocket_read_more": "Isental ittwasnen aṭas:", "pocket_read_more": "Isental ittwasnen aṭas:",
"pocket_read_even_more": "Wali ugar n teqsiḍin", "pocket_read_even_more": "Wali ugar n teqsiḍin",
"pocket_feedback_header": "D amezwaru n Web, ittwafren sγur ugar 25 imelyan n imdanen.",
"pocket_send_feedback": "Azen tikti" "pocket_send_feedback": "Azen tikti"
}, },
"kk": { "kk": {
@@ -2285,9 +2497,12 @@
"default_label_loading": "읽는 중…", "default_label_loading": "읽는 중…",
"header_top_sites": "상위 사이트", "header_top_sites": "상위 사이트",
"header_highlights": "하이라이트", "header_highlights": "하이라이트",
"header_stories": "상위 이야기",
"header_stories_from": "출처",
"type_label_visited": "방문한 사이트", "type_label_visited": "방문한 사이트",
"type_label_bookmarked": "즐겨찾기", "type_label_bookmarked": "즐겨찾기",
"type_label_synced": "다른 기기에서 동기화", "type_label_synced": "다른 기기에서 동기화",
"type_label_recommended": "트랜드",
"type_label_open": "열기", "type_label_open": "열기",
"type_label_topic": "주제", "type_label_topic": "주제",
"menu_action_bookmark": "즐겨찾기", "menu_action_bookmark": "즐겨찾기",
@@ -2298,6 +2513,7 @@
"menu_action_open_private_window": "새 사생활 보호 창에서 열기", "menu_action_open_private_window": "새 사생활 보호 창에서 열기",
"menu_action_dismiss": "닫기", "menu_action_dismiss": "닫기",
"menu_action_delete": "방문 기록에서 삭제", "menu_action_delete": "방문 기록에서 삭제",
"menu_action_save_to_pocket": "Pocket에 저장",
"search_for_something_with": "다음에서 {search_term} 검색:", "search_for_something_with": "다음에서 {search_term} 검색:",
"search_button": "검색", "search_button": "검색",
"search_header": "{search_engine_name} 검색", "search_header": "{search_engine_name} 검색",
@@ -2320,6 +2536,7 @@
"settings_pane_topsites_options_showmore": "두 줄로 보기", "settings_pane_topsites_options_showmore": "두 줄로 보기",
"settings_pane_highlights_header": "하이라이트", "settings_pane_highlights_header": "하이라이트",
"settings_pane_highlights_body": "최근 방문 기록과 북마크를 살펴보세요.", "settings_pane_highlights_body": "최근 방문 기록과 북마크를 살펴보세요.",
"settings_pane_pocketstories_header": "상위 이야기",
"settings_pane_done_button": "완료", "settings_pane_done_button": "완료",
"edit_topsites_button_text": "수정", "edit_topsites_button_text": "수정",
"edit_topsites_button_label": "상위 사이트 영역 꾸미기", "edit_topsites_button_label": "상위 사이트 영역 꾸미기",
@@ -2436,9 +2653,12 @@
"default_label_loading": "Įkeliama…", "default_label_loading": "Įkeliama…",
"header_top_sites": "Lankomiausios svetainės", "header_top_sites": "Lankomiausios svetainės",
"header_highlights": "Akcentai", "header_highlights": "Akcentai",
"header_stories": "Populiariausi straipsniai",
"header_stories_from": "iš",
"type_label_visited": "Aplankyti", "type_label_visited": "Aplankyti",
"type_label_bookmarked": "Adresyne", "type_label_bookmarked": "Adresyne",
"type_label_synced": "Sinchronizuoti iš kito įrenginio", "type_label_synced": "Sinchronizuoti iš kito įrenginio",
"type_label_recommended": "Populiaru",
"type_label_open": "Atviri", "type_label_open": "Atviri",
"type_label_topic": "Tema", "type_label_topic": "Tema",
"menu_action_bookmark": "Įrašyti į adresyną", "menu_action_bookmark": "Įrašyti į adresyną",
@@ -2449,6 +2669,7 @@
"menu_action_open_private_window": "Atverti naujame privačiajame lange", "menu_action_open_private_window": "Atverti naujame privačiajame lange",
"menu_action_dismiss": "Paslėpti", "menu_action_dismiss": "Paslėpti",
"menu_action_delete": "Pašalinti iš istorijos", "menu_action_delete": "Pašalinti iš istorijos",
"menu_action_save_to_pocket": "Įrašyti į „Pocket“",
"search_for_something_with": "Ieškoti „{search_term}“ per:", "search_for_something_with": "Ieškoti „{search_term}“ per:",
"search_button": "Ieškoti", "search_button": "Ieškoti",
"search_header": "{search_engine_name} paieška", "search_header": "{search_engine_name} paieška",
@@ -2471,6 +2692,8 @@
"settings_pane_topsites_options_showmore": "Rodyti dvi eilutes", "settings_pane_topsites_options_showmore": "Rodyti dvi eilutes",
"settings_pane_highlights_header": "Akcentai", "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_highlights_body": "Pažvelkite į savo naujausią naršymo istoriją bei paskiausiai pridėtus adresyno įrašus.",
"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", "settings_pane_done_button": "Atlikta",
"edit_topsites_button_text": "Keisti", "edit_topsites_button_text": "Keisti",
"edit_topsites_button_label": "Tinkinkite savo lankomiausių svetainių skiltį", "edit_topsites_button_label": "Tinkinkite savo lankomiausių svetainių skiltį",
@@ -2478,8 +2701,23 @@
"edit_topsites_showless_button": "Rodyti mažiau", "edit_topsites_showless_button": "Rodyti mažiau",
"edit_topsites_done_button": "Atlikta", "edit_topsites_done_button": "Atlikta",
"edit_topsites_pin_button": "Įsegti šią svetainę", "edit_topsites_pin_button": "Įsegti šią svetainę",
"edit_topsites_unpin_button": "Išsegti šią svetainę",
"edit_topsites_edit_button": "Redaguoti šią svetainę", "edit_topsites_edit_button": "Redaguoti šią svetainę",
"edit_topsites_dismiss_button": "Paslėpti šią svetainę" "edit_topsites_dismiss_button": "Paslėpti šią svetainę",
"edit_topsites_add_button": "Pridėti",
"topsites_form_add_header": "Nauja mėgstama svetainė",
"topsites_form_edit_header": "Redaguoti mėgstamą svetainę",
"topsites_form_title_placeholder": "Įveskite pavadinimą",
"topsites_form_url_placeholder": "Įveskite arba įklijuokite URL",
"topsites_form_add_button": "Pridėti",
"topsites_form_save_button": "Įrašyti",
"topsites_form_cancel_button": "Atsisakyti",
"topsites_form_url_validation": "Reikalingas tinkamas URL",
"pocket_read_more": "Populiarios temos:",
"pocket_read_even_more": "Rodyti daugiau straipsnių",
"pocket_feedback_header": "Geriausi dalykai internete, kuruojami daugiau nei 25 milijonų žmonių.",
"pocket_feedback_body": "„Pocket“, „Mozillos“ šeimos dalis, padės jums atrasti kokybišką turinį, kurio kitaip gal nebūtumėte radę.",
"pocket_send_feedback": "Siųsti atsiliepimą"
}, },
"ltg": {}, "ltg": {},
"lv": { "lv": {
@@ -2487,9 +2725,115 @@
}, },
"mai": {}, "mai": {},
"mk": {}, "mk": {},
"ml": {}, "ml": {
"newtab_page_title": "പുതിയ ടാബ്",
"default_label_loading": "ലോഡ്ചെയ്യുന്നു…",
"header_top_sites": "മികച്ച സൈറ്റുകൾ",
"header_highlights": "ഹൈലൈറ്റുകൾ",
"header_stories": "മികച്ച ലേഖനങ്ങൾ",
"header_stories_from": "എവിടെ നിന്നും",
"type_label_visited": "സന്ദർശിച്ചത്‌",
"type_label_bookmarked": "അടയാളപ്പെടുത്തിയത്",
"type_label_synced": "മറ്റു ഉപകരണങ്ങളുമായി സാമ്യപ്പെടുക",
"type_label_recommended": "ട്രെൻഡിംഗ്",
"type_label_open": "തുറക്കുക",
"type_label_topic": "വിഷയം",
"menu_action_bookmark": "അടയാളം",
"menu_action_remove_bookmark": "അടയാളം മാറ്റുക",
"menu_action_copy_address": "വിലാസം പകർത്തുക",
"menu_action_email_link": "ഇമെയിൽ വിലാസം…",
"menu_action_open_new_window": "പുതിയ ജാലകത്തിൽ തുറക്കുക",
"menu_action_open_private_window": "പുതിയ രസഹ്യജാലകത്തിൽ തുറക്കുക",
"menu_action_dismiss": "പുറത്താക്കുക",
"menu_action_delete": "ചരിത്രത്തിൽ നിന്ന് ഒഴിവാക്കുക",
"menu_action_save_to_pocket": "പോക്കറ്റിലേയ്ക്ക് സംരക്ഷിയ്ക്കുക",
"search_for_something_with": "തിരയാൻ {search_term} : എന്നത് ഉപയോഗിയ്ക്കുക",
"search_button": "തിരയുക",
"search_header": "{search_engine_name} തിരയുക",
"search_web_placeholder": "ഇൻറർനെറ്റിൽ തിരയുക",
"search_settings": "തിരയാനുള്ള രീതികൾ മാറ്റുക",
"welcome_title": "പുതിയ ജാലകത്തിലേക്കു സ്വാഗതം",
"welcome_body": "നിങ്ങളുടെ ഏറ്റവും ശ്രദ്ധേയമായ അടയാളങ്ങൾ, ലേഖനങ്ങൾ, വീഡിയോകൾ, കൂടാതെ നിങ്ങൾ സമീപകാലത്ത് സന്ദർശിച്ച താളുകൾ എന്നിവ കാണിക്കുന്നതിനായി ഫയർഫോക്സ് ഈ ഇടം ഉപയോഗിക്കും, അതിനാൽ നിങ്ങൾക്ക് എളുപ്പത്തിൽ അവയിലേക്ക് തിരിച്ചു പോകാം.",
"welcome_label": "താങ്കളുടെ ഹൈലൈറ്റ്സ് തിരിച്ചറിയുന്നു",
"time_label_less_than_minute": "<1 മിനിറ്റ്",
"time_label_minute": "{number} മിനിറ്റ്",
"time_label_hour": "{number} മിനിറ്റ്",
"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_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": "നിങ്ങളുടെ മുന്നേറിയ സൈറ്റുകളുടെ വിഭാഗം ഇഷ്ടാനുസൃതമാക്കുക",
"edit_topsites_showmore_button": "കൂടുതൽ കാണിക്കുക",
"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_add_button": "ചേര്‍ക്കുക",
"topsites_form_add_header": "പുതിയ മികച്ച സൈറ്റുകൾ",
"topsites_form_edit_header": "മികച്ച സൈറ്റ് ലിസ്റ്റ് തിരുത്തൂ",
"topsites_form_title_placeholder": "തലക്കെട്ട് നൽകൂ",
"topsites_form_url_placeholder": "വെബ്URLനൽകൂ",
"topsites_form_add_button": "ചേർക്കൂ",
"topsites_form_save_button": "സംരക്ഷിയ്ക്കൂ",
"topsites_form_cancel_button": "ഒഴിവാക്കൂ",
"topsites_form_url_validation": "പ്രവർത്തിയ്ക്കുന്ന URL ആവശ്യമാണ്",
"pocket_read_more": "ജനപ്രിയ വിഷയങ്ങൾ:",
"pocket_read_even_more": "കൂടുതൽ ലേഖനങ്ങൾ കാണുക",
"pocket_feedback_header": "250 ലക്ഷം പേരാൽ തെരഞ്ഞെടുക്കപ്പെട്ട വെബിലെ ഏറ്റവും മികച്ചവയാണിവ.",
"pocket_feedback_body": "മോസില്ല‌ കുടുംബാംഗമായ പോക്കറ്റ്, വിട്ടുപോയേയ്ക്കാവുന്ന മികച്ച ലേഖനങ്ങൾ നിങ്ങളുടെ ശ്രദ്ധയിൽ എത്തിയ്ക്കുന്നു.",
"pocket_send_feedback": "പ്രതികരണം അയയ്ക്കുക"
},
"mn": {}, "mn": {},
"mr": {}, "mr": {
"newtab_page_title": "नवीन टॅब",
"default_label_loading": "दाखल करीत आहे…",
"header_top_sites": "खास साईट्स",
"header_highlights": "ठळक",
"header_stories": "महत्वाच्या गोष्टी",
"header_stories_from": "कडून",
"type_label_visited": "भेट दिलेले",
"type_label_bookmarked": "वाचनखुण लावले",
"type_label_synced": "इतर साधनावरुन ताळमेळ केले",
"type_label_open": "उघडा",
"type_label_topic": "विषय",
"menu_action_bookmark": "वाचनखुण",
"menu_action_remove_bookmark": "वाचनखुण काढा",
"menu_action_copy_address": "पत्त्याची प्रत बनवा",
"menu_action_email_link": "दुवा इमेल करा…",
"menu_action_open_new_window": "नवीन पटलात उघडा",
"menu_action_open_private_window": "नवीन खाजगी पटलात उघडा",
"menu_action_dismiss": "रद्द करा",
"menu_action_delete": "इतिहासातून नष्ट करा",
"menu_action_save_to_pocket": "Pocket मध्ये जतन करा",
"search_for_something_with": "शोधा {search_term} सोबत:",
"search_button": "शोधा",
"search_header": "{search_engine_name} शोध",
"search_web_placeholder": "वेबवर शोधा",
"search_settings": "शोध सेटिंग बदला",
"welcome_title": "नवीन टॅबवर स्वागत आहे",
"time_label_less_than_minute": "<1मि",
"time_label_minute": "{number}मि",
"time_label_hour": "{number}ता",
"time_label_day": "{number}दि",
"settings_pane_button_label": "आपले नवीन टॅब पृष्ठ सानुकूलित करा",
"settings_pane_header": "नवीन टॅब प्राधान्ये",
"settings_pane_body": "नवीन टॅब उघडल्यानंतर काय दिसायला हवे ते निवडा.",
"settings_pane_search_header": "शोध",
"settings_pane_search_body": "आपल्या नवीन टॅब वरून वेबवर शोधा."
},
"ms": { "ms": {
"newtab_page_title": "Tab Baru", "newtab_page_title": "Tab Baru",
"default_label_loading": "Memuatkan…", "default_label_loading": "Memuatkan…",
@@ -3021,12 +3365,12 @@
"settings_pane_highlights_body": "Veja o seu histórico de navegação recente e favoritos recentemente criados.", "settings_pane_highlights_body": "Veja o seu histórico de navegação recente e favoritos recentemente criados.",
"settings_pane_pocketstories_header": "Histórias populares", "settings_pane_pocketstories_header": "Histórias populares",
"settings_pane_pocketstories_body": "O Pocket, parte da família Mozilla, irá ajudar a conecta-se a conteúdo de alta qualidade que talvez não tenha encontrado de outra forma.", "settings_pane_pocketstories_body": "O Pocket, parte da família Mozilla, irá ajudar a conecta-se a conteúdo de alta qualidade que talvez não tenha encontrado de outra forma.",
"settings_pane_done_button": "Concluir", "settings_pane_done_button": "Concluído",
"edit_topsites_button_text": "Editar", "edit_topsites_button_text": "Editar",
"edit_topsites_button_label": "Personalizar a sua seção de sites preferidos", "edit_topsites_button_label": "Personalizar a sua seção de sites preferidos",
"edit_topsites_showmore_button": "Mostrar mais", "edit_topsites_showmore_button": "Mostrar mais",
"edit_topsites_showless_button": "Mostrar menos", "edit_topsites_showless_button": "Mostrar menos",
"edit_topsites_done_button": "Concluir", "edit_topsites_done_button": "Concluído",
"edit_topsites_pin_button": "Fixar este site", "edit_topsites_pin_button": "Fixar este site",
"edit_topsites_unpin_button": "Desafixar este site", "edit_topsites_unpin_button": "Desafixar este site",
"edit_topsites_edit_button": "Editar este site", "edit_topsites_edit_button": "Editar este site",
@@ -3172,6 +3516,7 @@
"default_label_loading": "Se încarcă…", "default_label_loading": "Se încarcă…",
"header_top_sites": "Site-uri de top", "header_top_sites": "Site-uri de top",
"header_highlights": "Evidențieri", "header_highlights": "Evidențieri",
"header_stories_from": "de la",
"type_label_visited": "Vizitate", "type_label_visited": "Vizitate",
"type_label_bookmarked": "Însemnat", "type_label_bookmarked": "Însemnat",
"type_label_synced": "Sincronizat de pe alt dispozitiv", "type_label_synced": "Sincronizat de pe alt dispozitiv",
@@ -3198,7 +3543,7 @@
"time_label_hour": "{number}h", "time_label_hour": "{number}h",
"time_label_day": "{number}d", "time_label_day": "{number}d",
"settings_pane_button_label": "Particularizează pagina de filă nouă", "settings_pane_button_label": "Particularizează pagina de filă nouă",
"settings_pane_header": "Preferințe filă nouă", "settings_pane_header": "Preferințe pentru filă nouă",
"settings_pane_body": "Alege ce să vezi la deschiderea unei noi file.", "settings_pane_body": "Alege ce să vezi la deschiderea unei noi file.",
"settings_pane_search_header": "Caută", "settings_pane_search_header": "Caută",
"settings_pane_search_body": "Caută pe web din noua filă.", "settings_pane_search_body": "Caută pe web din noua filă.",
@@ -3215,7 +3560,18 @@
"edit_topsites_done_button": "Gata", "edit_topsites_done_button": "Gata",
"edit_topsites_pin_button": "Fixează acest site", "edit_topsites_pin_button": "Fixează acest site",
"edit_topsites_edit_button": "Editează acest site", "edit_topsites_edit_button": "Editează acest site",
"edit_topsites_dismiss_button": "Înlătură acest site" "edit_topsites_dismiss_button": "Înlătură acest site",
"edit_topsites_add_button": "Adaugă",
"topsites_form_add_header": "Site de top nou",
"topsites_form_edit_header": "Editează site-ul de top",
"topsites_form_title_placeholder": "Introdu un titlu",
"topsites_form_url_placeholder": "Tastează sau lipește un URL",
"topsites_form_add_button": "Adaugă",
"topsites_form_save_button": "Salvează",
"topsites_form_cancel_button": "Renunță",
"topsites_form_url_validation": "URL valid necesar",
"pocket_read_more": "Subiecte populare:",
"pocket_send_feedback": "Trimite feedback"
}, },
"ru": { "ru": {
"newtab_page_title": "Новая вкладка", "newtab_page_title": "Новая вкладка",
@@ -3630,9 +3986,11 @@
"header_top_sites": "சிறந்த தளங்கள்", "header_top_sites": "சிறந்த தளங்கள்",
"header_highlights": "சிறப்பம்சங்கள்", "header_highlights": "சிறப்பம்சங்கள்",
"header_stories": "முக்கிய கதைகள்", "header_stories": "முக்கிய கதைகள்",
"header_stories_from": "அனுப்பியவர்",
"type_label_visited": "பார்த்தவை", "type_label_visited": "பார்த்தவை",
"type_label_bookmarked": "புத்தகக்குறியிடப்பட்டது", "type_label_bookmarked": "புத்தகக்குறியிடப்பட்டது",
"type_label_synced": "இன்னொரு சாதனத்திலிருந்து ஒத்திசைக்கப்பட்டது", "type_label_synced": "இன்னொரு சாதனத்திலிருந்து ஒத்திசைக்கப்பட்டது",
"type_label_recommended": "பிரபலமான",
"type_label_open": "திற", "type_label_open": "திற",
"type_label_topic": "தலைப்பு", "type_label_topic": "தலைப்பு",
"menu_action_bookmark": "புத்தகக்குறி", "menu_action_bookmark": "புத்தகக்குறி",
@@ -3643,7 +4001,8 @@
"menu_action_open_private_window": "ஒரு புதிய அந்தரங்க சாளரத்தில் திற", "menu_action_open_private_window": "ஒரு புதிய அந்தரங்க சாளரத்தில் திற",
"menu_action_dismiss": "வெளியேற்று", "menu_action_dismiss": "வெளியேற்று",
"menu_action_delete": "வரலாற்றிலருந்து அழி", "menu_action_delete": "வரலாற்றிலருந்து அழி",
"search_for_something_with": "{search_term} என்பதற்காகத் தேடு:", "menu_action_save_to_pocket": "பாக்கட்டில் சேமி",
"search_for_something_with": "{search_term} சொல்லிற்காகத் தேடு:",
"search_button": "தேடு", "search_button": "தேடு",
"search_header": "{search_engine_name} தேடுபொறியில் தேடு", "search_header": "{search_engine_name} தேடுபொறியில் தேடு",
"search_web_placeholder": "இணையத்தில் தேடு", "search_web_placeholder": "இணையத்தில் தேடு",
@@ -3666,6 +4025,7 @@
"settings_pane_highlights_header": "முக்கியம்சங்கள்", "settings_pane_highlights_header": "முக்கியம்சங்கள்",
"settings_pane_highlights_body": "உங்கள் சமீபத்திய உலாவல் வரலாற்றையும் புதிதாகச் சேர்த்த புக்மார்க்குகளையும் திரும்பப் பார்க்கவும்.", "settings_pane_highlights_body": "உங்கள் சமீபத்திய உலாவல் வரலாற்றையும் புதிதாகச் சேர்த்த புக்மார்க்குகளையும் திரும்பப் பார்க்கவும்.",
"settings_pane_pocketstories_header": "முக்கிய கதைகள்", "settings_pane_pocketstories_header": "முக்கிய கதைகள்",
"settings_pane_pocketstories_body": "Pocket, ஒரு மொசில்லா குடும்ப உறுப்பினராக, உயர்தர உள்ளடக்கங்களுடன் இணைய உதவுகிறது, இது இல்லையேல் அது சாத்தியமாகது.",
"settings_pane_done_button": "முடிந்தது", "settings_pane_done_button": "முடிந்தது",
"edit_topsites_button_text": "தொகு", "edit_topsites_button_text": "தொகு",
"edit_topsites_button_label": "உங்களின் சிறந்த தளங்களுக்கான தொகுதியை விருப்பமை", "edit_topsites_button_label": "உங்களின் சிறந்த தளங்களுக்கான தொகுதியை விருப்பமை",
@@ -3673,6 +4033,7 @@
"edit_topsites_showless_button": "குறைவாகக் காண்பி", "edit_topsites_showless_button": "குறைவாகக் காண்பி",
"edit_topsites_done_button": "முடிந்தது", "edit_topsites_done_button": "முடிந்தது",
"edit_topsites_pin_button": "இத்தளத்தை இடமுனையில் வை", "edit_topsites_pin_button": "இத்தளத்தை இடமுனையில் வை",
"edit_topsites_unpin_button": "முனையிலிருந்து நீக்கு",
"edit_topsites_edit_button": "இத்தளத்தை தொகு", "edit_topsites_edit_button": "இத்தளத்தை தொகு",
"edit_topsites_dismiss_button": "இந்த தளத்தை வெளியேற்று", "edit_topsites_dismiss_button": "இந்த தளத்தை வெளியேற்று",
"edit_topsites_add_button": "சேர்", "edit_topsites_add_button": "சேர்",
@@ -3686,6 +4047,8 @@
"topsites_form_url_validation": "சரியான URL தேவை", "topsites_form_url_validation": "சரியான URL தேவை",
"pocket_read_more": "பிரபலமான தலைப்புகள்:", "pocket_read_more": "பிரபலமான தலைப்புகள்:",
"pocket_read_even_more": "இன்னும் கதைகளைப் பார்க்கவும்", "pocket_read_even_more": "இன்னும் கதைகளைப் பார்க்கவும்",
"pocket_feedback_header": "இணையத்தின் சிறந்த செயலி, 250 இலட்ச மக்களால் தேர்ந்தெடுக்கப்பட்டது.",
"pocket_feedback_body": "Pocket, ஒரு மொசில்லா குடும்ப உறுப்பினராக, உயர்தர உள்ளடக்கங்களுடன் இணைய உதவுகிறது, இது இல்லையேல் அது சாத்தியமாகது.",
"pocket_send_feedback": "கருத்துகளைத் தெறிவிக்கவும்" "pocket_send_feedback": "கருத்துகளைத் தெறிவிக்கவும்"
}, },
"ta-LK": {}, "ta-LK": {},
@@ -3744,10 +4107,12 @@
"default_label_loading": "กำลังโหลด…", "default_label_loading": "กำลังโหลด…",
"header_top_sites": "ไซต์เด่น", "header_top_sites": "ไซต์เด่น",
"header_highlights": "รายการเด่น", "header_highlights": "รายการเด่น",
"header_stories": "เรื่องราวเด่น",
"header_stories_from": "จาก", "header_stories_from": "จาก",
"type_label_visited": "เยี่ยมชมแล้ว", "type_label_visited": "เยี่ยมชมแล้ว",
"type_label_bookmarked": "มีที่คั่นหน้าแล้ว", "type_label_bookmarked": "มีที่คั่นหน้าแล้ว",
"type_label_synced": "ซิงค์จากอุปกรณ์อื่น", "type_label_synced": "ซิงค์จากอุปกรณ์อื่น",
"type_label_recommended": "กำลังนิยม",
"type_label_open": "เปิด", "type_label_open": "เปิด",
"type_label_topic": "หัวข้อ", "type_label_topic": "หัวข้อ",
"menu_action_bookmark": "เพิ่มที่คั่นหน้า", "menu_action_bookmark": "เพิ่มที่คั่นหน้า",
@@ -3781,6 +4146,7 @@
"settings_pane_topsites_options_showmore": "แสดงสองแถว", "settings_pane_topsites_options_showmore": "แสดงสองแถว",
"settings_pane_highlights_header": "รายการเด่น", "settings_pane_highlights_header": "รายการเด่น",
"settings_pane_highlights_body": "มองย้อนกลับมาดูประวัติการท่องเว็บเมื่อเร็ว ๆ นี้และที่คั่นหน้าที่สร้างใหม่ของคุณ", "settings_pane_highlights_body": "มองย้อนกลับมาดูประวัติการท่องเว็บเมื่อเร็ว ๆ นี้และที่คั่นหน้าที่สร้างใหม่ของคุณ",
"settings_pane_pocketstories_header": "เรื่องราวเด่น",
"settings_pane_done_button": "เสร็จสิ้น", "settings_pane_done_button": "เสร็จสิ้น",
"edit_topsites_button_text": "แก้ไข", "edit_topsites_button_text": "แก้ไข",
"edit_topsites_button_label": "ปรับแต่งส่วนไซต์เด่นของคุณ", "edit_topsites_button_label": "ปรับแต่งส่วนไซต์เด่นของคุณ",
@@ -3792,11 +4158,17 @@
"edit_topsites_edit_button": "แก้ไขไซต์นี้", "edit_topsites_edit_button": "แก้ไขไซต์นี้",
"edit_topsites_dismiss_button": "ไม่สนใจไซต์นี้", "edit_topsites_dismiss_button": "ไม่สนใจไซต์นี้",
"edit_topsites_add_button": "เพิ่ม", "edit_topsites_add_button": "เพิ่ม",
"topsites_form_add_header": "ไซต์เด่นใหม่",
"topsites_form_edit_header": "แก้ไขไซต์เด่น",
"topsites_form_title_placeholder": "ป้อนชื่อเรื่อง",
"topsites_form_url_placeholder": "พิมพ์หรือวาง URL", "topsites_form_url_placeholder": "พิมพ์หรือวาง URL",
"topsites_form_add_button": "เพิ่ม", "topsites_form_add_button": "เพิ่ม",
"topsites_form_save_button": "บันทึก", "topsites_form_save_button": "บันทึก",
"topsites_form_cancel_button": "ยกเลิก", "topsites_form_cancel_button": "ยกเลิก",
"pocket_read_even_more": "ดูเรื่องราวเพิ่มเติม" "topsites_form_url_validation": "ต้องการ URL ที่ถูกต้อง",
"pocket_read_more": "หัวข้อยอดนิยม:",
"pocket_read_even_more": "ดูเรื่องราวเพิ่มเติม",
"pocket_send_feedback": "ส่งข้อคิดเห็น"
}, },
"tl": { "tl": {
"newtab_page_title": "Bagong Tab", "newtab_page_title": "Bagong Tab",
@@ -3995,6 +4367,8 @@
"default_label_loading": "لوڈ کر رہا ہے…", "default_label_loading": "لوڈ کر رہا ہے…",
"header_top_sites": "بہترین سائٹیں", "header_top_sites": "بہترین سائٹیں",
"header_highlights": "شہ سرخياں", "header_highlights": "شہ سرخياں",
"header_stories": "بہترین کہانیاں",
"header_stories_from": "من جانب",
"type_label_visited": "دورہ شدہ", "type_label_visited": "دورہ شدہ",
"type_label_bookmarked": "نشان شدہ", "type_label_bookmarked": "نشان شدہ",
"type_label_synced": "کسی دوسرے آلے سے ہمہ وقت ساز کیا گیا ہے", "type_label_synced": "کسی دوسرے آلے سے ہمہ وقت ساز کیا گیا ہے",
@@ -4008,6 +4382,7 @@
"menu_action_open_private_window": "نئی نجی دریچے میں کھولیں", "menu_action_open_private_window": "نئی نجی دریچے میں کھولیں",
"menu_action_dismiss": "برخاست کریں", "menu_action_dismiss": "برخاست کریں",
"menu_action_delete": "تاریخ سے حذف کریں", "menu_action_delete": "تاریخ سے حذف کریں",
"menu_action_save_to_pocket": "Pocket میں محفوظ کریں",
"search_for_something_with": "ساتھ {search_term} کے لئے تلاش کریں:", "search_for_something_with": "ساتھ {search_term} کے لئے تلاش کریں:",
"search_button": "تلاش", "search_button": "تلاش",
"search_header": "{search_engine_name} پر تلاش کریں", "search_header": "{search_engine_name} پر تلاش کریں",
@@ -4022,16 +4397,37 @@
"time_label_day": "{number}d", "time_label_day": "{number}d",
"settings_pane_button_label": "اپنے نئے ٹیب کہ صفحہ کی تخصیص کریں", "settings_pane_button_label": "اپنے نئے ٹیب کہ صفحہ کی تخصیص کریں",
"settings_pane_header": "نئے َٹیب کی ترجیحات", "settings_pane_header": "نئے َٹیب کی ترجیحات",
"settings_pane_body": "انتخاب کریں آپ کیا دیکھنا چاہتےہیں جب آپ نیا ٹیب کھولیں گے۔",
"settings_pane_search_header": "تلاش", "settings_pane_search_header": "تلاش",
"settings_pane_search_body": "اپنے نئے ٹیب سے وہب پر تلاش کریں۔", "settings_pane_search_body": "اپنے نئے ٹیب سے وہب پر تلاش کریں۔",
"settings_pane_topsites_header": "بہترین سائٹیں", "settings_pane_topsites_header": "بہترین سائٹیں",
"settings_pane_topsites_body": "اپنی سب سے زیادہ دورہ کردہ ویب سائٹ تک رسائی حاصل کریں۔",
"settings_pane_topsites_options_showmore": "دو قطاریں دکھائیں", "settings_pane_topsites_options_showmore": "دو قطاریں دکھائیں",
"settings_pane_highlights_header": "شہ سرخياں", "settings_pane_highlights_header": "شہ سرخياں",
"settings_pane_highlights_body": "اپنی حالیہ براؤزنگ کی سابقات اور نو تشکیل کردہ نشانیوں پر نظر ڈالیں۔",
"settings_pane_pocketstories_header": "بہترین کہانیاں",
"settings_pane_done_button": "ہوگیا", "settings_pane_done_button": "ہوگیا",
"edit_topsites_button_text": "تدوین", "edit_topsites_button_text": "تدوین",
"edit_topsites_button_label": "اپنی بہترین سائٹس والے حصے کی تخصیص کریں",
"edit_topsites_showmore_button": "مزید دکھائیں",
"edit_topsites_done_button": "ہوگیا", "edit_topsites_done_button": "ہوگیا",
"edit_topsites_pin_button": "اس سائَٹ کو پن کریں",
"edit_topsites_unpin_button": "اس سائٹ کو انپن کریں",
"edit_topsites_edit_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": "ٹائپ کریں یا ایک URL چسباں کریں",
"topsites_form_add_button": "اظافہ کریں",
"topsites_form_save_button": "محفوظ کریں",
"topsites_form_cancel_button": "منسوخ کریں",
"topsites_form_url_validation": "جائز URL درکار ہے",
"pocket_read_more": "مشہور مضامین:",
"pocket_read_even_more": "مزید کہانیاں دیکھیں",
"pocket_feedback_body": "Pocket ایک جصہ ہے Mozilla کے خاندان کا،آپ کو اعلی میعار کے مواد سے جڑنے میں مدد دے گا جو شاید آپ بصورت دیگر نہ ڈھونڈ سکتے۔",
"pocket_send_feedback": "جواب الجواب ارسال کریں"
}, },
"uz": {}, "uz": {},
"vi": {}, "vi": {},

View File

@@ -4,108 +4,92 @@
"use strict"; "use strict";
const {utils: Cu} = Components; const {utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {}); // NB: Eagerly load modules that will be loaded/constructed/initialized in the
// common case to avoid the overhead of wrapping and detecting lazy loading.
const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {}); 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 {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", {});
const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
const {TelemetryFeed} = Cu.import("resource://activity-stream/lib/TelemetryFeed.jsm", {});
const {TopSitesFeed} = Cu.import("resource://activity-stream/lib/TopSitesFeed.jsm", {});
const REASON_ADDON_UNINSTALL = 6; const REASON_ADDON_UNINSTALL = 6;
XPCOMUtils.defineLazyModuleGetter(this, "DefaultPrefs", const PREFS_CONFIG = new Map([
"resource://activity-stream/lib/ActivityStreamPrefs.jsm"); ["default.sites", {
title: "Comma-separated list of default top sites to fill in behind visited sites",
// Feeds value: "https://www.facebook.com/,https://www.youtube.com/,https://www.amazon.com/,https://www.yahoo.com/,https://www.ebay.com/,https://twitter.com/"
XPCOMUtils.defineLazyModuleGetter(this, "LocalizationFeed", }],
"resource://activity-stream/lib/LocalizationFeed.jsm"); ["showSearch", {
XPCOMUtils.defineLazyModuleGetter(this, "NewTabInit",
"resource://activity-stream/lib/NewTabInit.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesFeed",
"resource://activity-stream/lib/PlacesFeed.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrefsFeed",
"resource://activity-stream/lib/PrefsFeed.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFeed",
"resource://activity-stream/lib/TelemetryFeed.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
"resource://activity-stream/lib/TopSitesFeed.jsm");
const PREFS_CONFIG = [
// When you add a feed pref here:
// 1. The pref should be prefixed with "feeds."
// 2. The init property should be a function that instantiates your Feed
// 3. You should use XPCOMUtils.defineLazyModuleGetter to import the Feed,
// so it isn't loaded until the feed is enabled.
{
name: "feeds.localization",
title: "Initialize strings and detect locale for Activity Stream",
value: true,
init: () => new LocalizationFeed()
},
{
name: "feeds.newtabinit",
title: "Sends a copy of the state to each new tab that is opened",
value: true,
init: () => new NewTabInit()
},
{
name: "feeds.places",
title: "Listens for and relays various Places-related events",
value: true,
init: () => new PlacesFeed()
},
{
name: "feeds.prefs",
title: "Preferences",
value: true,
init: () => new PrefsFeed(PREFS_CONFIG.map(pref => pref.name))
},
{
name: "feeds.telemetry",
title: "Relays telemetry-related actions to TelemetrySender",
value: true,
init: () => new TelemetryFeed()
},
{
name: "feeds.topsites",
title: "Queries places and gets metadata for Top Sites section",
value: true,
init: () => new TopSitesFeed()
},
{
name: "showSearch",
title: "Show the Search bar on the New Tab page", title: "Show the Search bar on the New Tab page",
value: true value: true
}],
["showTopSites", {
title: "Show the Top Sites section on the New Tab page",
value: true
}],
["telemetry", {
title: "Enable system error and usage data collection",
value: true,
value_local_dev: false
}],
["telemetry.log", {
title: "Log telemetry events in the console",
value: false,
value_local_dev: true
}],
["telemetry.ping.endpoint", {
title: "Telemetry server endpoint",
value: "https://onyx_tiles.stage.mozaws.net/v4/links/activity-stream"
}]
]);
const FEEDS_CONFIG = new Map();
for (const {name, factory, title, value} of [
{
name: "localization",
factory: () => new LocalizationFeed(),
title: "Initialize strings and detect locale for Activity Stream",
value: true
}, },
{ {
name: "showTopSites", name: "newtabinit",
title: "Show the Top Sites section on the New Tab page", factory: () => new NewTabInit(),
title: "Sends a copy of the state to each new tab that is opened",
value: true
},
{
name: "places",
factory: () => new PlacesFeed(),
title: "Listens for and relays various Places-related events",
value: true
},
{
name: "prefs",
factory: () => new PrefsFeed(PREFS_CONFIG),
title: "Preferences",
value: true value: true
}, },
{ {
name: "telemetry", name: "telemetry",
title: "Enable system error and usage data collection", factory: () => new TelemetryFeed(),
value: false title: "Relays telemetry-related actions to TelemetrySender",
value: true
}, },
{ {
name: "telemetry.log", name: "topsites",
title: "Log telemetry events in the console", factory: () => new TopSitesFeed(),
value: false title: "Queries places and gets metadata for Top Sites section",
}, value: true
{
name: "telemetry.ping.endpoint",
title: "Telemetry server endpoint",
value: "https://tiles.services.mozilla.com/v3/links/activity-stream"
}
];
const feeds = {};
for (const pref of PREFS_CONFIG) {
if (pref.name.match(/^feeds\./)) {
feeds[pref.name] = pref.init;
} }
]) {
const pref = `feeds.${name}`;
FEEDS_CONFIG.set(pref, factory);
PREFS_CONFIG.set(pref, {title, value});
} }
this.ActivityStream = class ActivityStream { this.ActivityStream = class ActivityStream {
@@ -122,17 +106,17 @@ this.ActivityStream = class ActivityStream {
this.initialized = false; this.initialized = false;
this.options = options; this.options = options;
this.store = new Store(); this.store = new Store();
this.feeds = feeds; this.feeds = FEEDS_CONFIG;
this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG); this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
} }
init() { init() {
this.initialized = true;
this._defaultPrefs.init(); this._defaultPrefs.init();
this.store.init(this.feeds); this.store.init(this.feeds);
this.store.dispatch({ this.store.dispatch({
type: at.INIT, type: at.INIT,
data: {version: this.options.version} data: {version: this.options.version}
}); });
this.initialized = true;
} }
uninit() { uninit() {
this.store.dispatch({type: at.UNINIT}); this.store.dispatch({type: at.UNINIT});

View File

@@ -5,20 +5,10 @@
"use strict"; "use strict";
const {utils: Cu} = Components; const {utils: Cu} = Components;
Cu.import("resource:///modules/AboutNewTab.jsm");
Cu.import("resource://gre/modules/RemotePageManager.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); const {actionCreators: ac, actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {
actionUtils: au,
actionCreators: ac,
actionTypes: at
} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "AboutNewTab",
"resource:///modules/AboutNewTab.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
"resource://gre/modules/RemotePageManager.jsm");
const ABOUT_NEW_TAB_URL = "about:newtab"; const ABOUT_NEW_TAB_URL = "about:newtab";
@@ -108,11 +98,11 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
send(action) { send(action) {
const targetId = action.meta && action.meta.toTarget; const targetId = action.meta && action.meta.toTarget;
const target = this.getTargetById(targetId); const target = this.getTargetById(targetId);
if (!target) { try {
// The target is no longer around - maybe the user closed the page target.sendAsyncMessage(this.outgoingMessageName, action);
return; } catch (e) {
// The target page is closed/closing by the user or test, so just ignore.
} }
target.sendAsyncMessage(this.outgoingMessageName, action);
} }
/** /**

View File

@@ -18,10 +18,23 @@ this.Prefs = class Prefs extends Preferences {
constructor(branch = ACTIVITY_STREAM_PREF_BRANCH) { constructor(branch = ACTIVITY_STREAM_PREF_BRANCH) {
super({branch}); super({branch});
this._branchName = branch; this._branchName = branch;
this._branchObservers = new Map();
} }
get branchName() { get branchName() {
return this._branchName; return this._branchName;
} }
ignoreBranch(listener) {
const observer = this._branchObservers.get(listener);
this._prefBranch.removeObserver("", observer);
this._branchObservers.delete(listener);
}
observeBranch(listener) {
const observer = (subject, topic, pref) => {
listener.onPrefChanged(pref, this.get(pref));
};
this._prefBranch.addObserver("", observer);
this._branchObservers.set(listener, observer);
}
}; };
this.DefaultPrefs = class DefaultPrefs { this.DefaultPrefs = class DefaultPrefs {
@@ -29,8 +42,8 @@ this.DefaultPrefs = class DefaultPrefs {
/** /**
* DefaultPrefs - A helper for setting and resetting default prefs for the add-on * DefaultPrefs - A helper for setting and resetting default prefs for the add-on
* *
* @param {Array} config An array of configuration objects with the following properties: * @param {Map} config A Map with {string} key of the pref name and {object}
* {string} .name The name of the pref * value with the following pref properties:
* {string} .title (optional) A description of the pref * {string} .title (optional) A description of the pref
* {bool|string|number} .value The default value for the pref * {bool|string|number} .value The default value for the pref
* @param {string} branch (optional) The pref branch (defaults to ACTIVITY_STREAM_PREF_BRANCH) * @param {string} branch (optional) The pref branch (defaults to ACTIVITY_STREAM_PREF_BRANCH)
@@ -64,8 +77,19 @@ this.DefaultPrefs = class DefaultPrefs {
* init - Set default prefs for all prefs in the config * init - Set default prefs for all prefs in the config
*/ */
init() { init() {
for (const pref of this._config) { // If Firefox is a locally built version or a testing build on try, etc.
this._setDefaultPref(pref.name, pref.value); // the value of the app.update.channel pref should be "default"
const IS_UNOFFICIAL_BUILD = Services.prefs.getStringPref("app.update.channel") === "default";
for (const pref of this._config.keys()) {
const prefConfig = this._config.get(pref);
let value;
if (IS_UNOFFICIAL_BUILD && "value_local_dev" in prefConfig) {
value = prefConfig.value_local_dev;
} else {
value = prefConfig.value;
}
this._setDefaultPref(pref, value);
} }
} }
@@ -73,8 +97,8 @@ this.DefaultPrefs = class DefaultPrefs {
* reset - Resets all user-defined prefs for prefs in ._config to their defaults * reset - Resets all user-defined prefs for prefs in ._config to their defaults
*/ */
reset() { reset() {
for (const pref of this._config) { for (const name of this._config.keys()) {
this.branch.clearUserPref(pref.name); this.branch.clearUserPref(name);
} }
} }
}; };

View File

@@ -4,13 +4,10 @@
"use strict"; "use strict";
const {utils: Cu} = Components; const {utils: Cu} = Components;
const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {}); Cu.import("resource://gre/modules/Services.jsm");
Cu.importGlobalProperties(["fetch"]); Cu.importGlobalProperties(["fetch"]);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
"resource://gre/modules/Services.jsm");
// What is our default locale for the app? // What is our default locale for the app?
const DEFAULT_LOCALE = "en-US"; const DEFAULT_LOCALE = "en-US";

View File

@@ -4,7 +4,8 @@
"use strict"; "use strict";
const {utils: Cu} = Components; const {utils: Cu} = Components;
const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
/** /**
* NewTabInit - A placeholder for now. This will send a copy of the state to all * NewTabInit - A placeholder for now. This will send a copy of the state to all

View File

@@ -3,17 +3,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"; "use strict";
const {utils: Cu, interfaces: Ci} = Components; const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {}); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm"); "resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm"); "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
const LINK_BLOCKED_EVENT = "newtab-linkBlocked"; const LINK_BLOCKED_EVENT = "newtab-linkBlocked";
@@ -151,8 +150,14 @@ class PlacesFeed {
} }
addObservers() { addObservers() {
PlacesUtils.history.addObserver(this.historyObserver, true); // NB: Directly get services without importing the *BIG* PlacesUtils module
PlacesUtils.bookmarks.addObserver(this.bookmarksObserver, true); Cc["@mozilla.org/browser/nav-history-service;1"]
.getService(Ci.nsINavHistoryService)
.addObserver(this.historyObserver, true);
Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
.getService(Ci.nsINavBookmarksService)
.addObserver(this.bookmarksObserver, true);
Services.obs.addObserver(this, LINK_BLOCKED_EVENT); Services.obs.addObserver(this, LINK_BLOCKED_EVENT);
} }

View File

@@ -4,31 +4,26 @@
"use strict"; "use strict";
const {utils: Cu} = Components; const {utils: Cu} = Components;
const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Prefs", const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
"resource://activity-stream/lib/ActivityStreamPrefs.jsm"); const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
this.PrefsFeed = class PrefsFeed { this.PrefsFeed = class PrefsFeed {
constructor(prefNames) { constructor(prefMap) {
this._prefNames = prefNames; this._prefMap = prefMap;
this._prefs = new Prefs(); this._prefs = new Prefs();
this._observers = new Map();
} }
onPrefChanged(name, value) { onPrefChanged(name, value) {
this.store.dispatch(ac.BroadcastToContent({type: at.PREF_CHANGED, data: {name, value}})); if (this._prefMap.has(name)) {
this.store.dispatch(ac.BroadcastToContent({type: at.PREF_CHANGED, data: {name, value}}));
}
} }
init() { init() {
const values = {}; this._prefs.observeBranch(this);
// Set up listeners for each activity stream pref // Get the initial value of each activity stream pref
for (const name of this._prefNames) { const values = {};
const handler = value => { for (const name of this._prefMap.keys()) {
this.onPrefChanged(name, value);
};
this._observers.set(name, handler, this);
this._prefs.observe(name, handler);
values[name] = this._prefs.get(name); values[name] = this._prefs.get(name);
} }
@@ -36,10 +31,7 @@ this.PrefsFeed = class PrefsFeed {
this.store.dispatch(ac.BroadcastToContent({type: at.PREFS_INITIAL_VALUES, data: values})); this.store.dispatch(ac.BroadcastToContent({type: at.PREFS_INITIAL_VALUES, data: values}));
} }
removeListeners() { removeListeners() {
for (const name of this._prefNames) { this._prefs.ignoreBranch(this);
this._prefs.ignore(name, this._observers.get(name));
}
this._observers.clear();
} }
onAction(action) { onAction(action) {
switch (action.type) { switch (action.type) {

View File

@@ -5,13 +5,10 @@
const {utils: Cu} = Components; const {utils: Cu} = Components;
const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {}); const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "Prefs", const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
"resource://activity-stream/lib/ActivityStreamPrefs.jsm");
/** /**
* Store - This has a similar structure to a redux store, but includes some extra * Store - This has a similar structure to a redux store, but includes some extra
@@ -30,15 +27,11 @@ this.Store = class Store {
this._middleware = this._middleware.bind(this); this._middleware = this._middleware.bind(this);
// Bind each redux method so we can call it directly from the Store. E.g., // Bind each redux method so we can call it directly from the Store. E.g.,
// store.dispatch() will call store._store.dispatch(); // store.dispatch() will call store._store.dispatch();
["dispatch", "getState", "subscribe"].forEach(method => { for (const method of ["dispatch", "getState", "subscribe"]) {
this[method] = (...args) => { this[method] = (...args) => this._store[method](...args);
return this._store[method](...args); }
};
});
this.feeds = new Map(); this.feeds = new Map();
this._feedFactories = null;
this._prefs = new Prefs(); this._prefs = new Prefs();
this._prefHandlers = new Map();
this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch}); this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
this._store = redux.createStore( this._store = redux.createStore(
redux.combineReducers(reducers), redux.combineReducers(reducers),
@@ -51,10 +44,14 @@ this.Store = class Store {
* it calls each feed's .onAction method, if one * it calls each feed's .onAction method, if one
* is defined. * is defined.
*/ */
_middleware(store) { _middleware() {
return next => action => { return next => action => {
next(action); next(action);
this.feeds.forEach(s => s.onAction && s.onAction(action)); for (const store of this.feeds.values()) {
if (store.onAction) {
store.onAction(action);
}
}
}; };
} }
@@ -65,7 +62,7 @@ this.Store = class Store {
* passed to Store.init * passed to Store.init
*/ */
initFeed(feedName) { initFeed(feedName) {
const feed = this._feedFactories[feedName](); const feed = this._feedFactories.get(feedName)();
feed.store = this; feed.store = this;
this.feeds.set(feedName, feed); this.feeds.set(feedName, feed);
} }
@@ -88,41 +85,34 @@ this.Store = class Store {
} }
/** /**
* maybeStartFeedAndListenForPrefChanges - Listen for pref changes that turn a * onPrefChanged - Listener for handling feed changes.
* feed off/on, and as long as that pref was not explicitly set to
* false, initialize the feed immediately.
*
* @param {string} name The name of a feed, as defined in the object passed
* to Store.init
*/ */
maybeStartFeedAndListenForPrefChanges(prefName) { onPrefChanged(name, value) {
// Create a listener that turns the feed off/on based on changes if (this._feedFactories.has(name)) {
// to the pref, and cache it so we can unlisten on shut-down. if (value) {
const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(prefName) : this.uninitFeed(prefName)); this.initFeed(name);
this._prefHandlers.set(prefName, onPrefChanged); } else {
this._prefs.observe(prefName, onPrefChanged); this.uninitFeed(name);
}
// TODO: This should propbably be done in a generic pref manager for Activity Stream.
// If the pref is true, start the feed immediately.
if (this._prefs.get(prefName)) {
this.initFeed(prefName);
} }
} }
/** /**
* init - Initializes the ActivityStreamMessageChannel channel, and adds feeds. * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
* *
* @param {array} feedConstructors An array of configuration objects for feeds * @param {Map} feedFactories A Map of feeds with the name of the pref for
* each with .name (the name of the pref for the feed) and .init, * the feed as the key and a function that
* a function that returns an instance of the feed * constructs an instance of the feed.
*/ */
init(feedConstructors) { init(feedFactories) {
if (feedConstructors) { this._feedFactories = feedFactories;
this._feedFactories = feedConstructors; for (const pref of feedFactories.keys()) {
for (const pref of Object.keys(feedConstructors)) { if (this._prefs.get(pref)) {
this.maybeStartFeedAndListenForPrefChanges(pref); this.initFeed(pref);
} }
} }
this._prefs.observeBranch(this);
this._messageChannel.createChannel(); this._messageChannel.createChannel();
} }
@@ -133,11 +123,10 @@ this.Store = class Store {
* @return {type} description * @return {type} description
*/ */
uninit() { uninit() {
this._prefs.ignoreBranch(this);
this.feeds.forEach(feed => this.uninitFeed(feed)); this.feeds.forEach(feed => this.uninitFeed(feed));
this._prefHandlers.forEach((handler, pref) => this._prefs.ignore(pref, handler));
this._prefHandlers.clear();
this._feedFactories = null;
this.feeds.clear(); this.feeds.clear();
this._feedFactories = null;
this._messageChannel.destroyChannel(); this._messageChannel.destroyChannel();
} }
}; };

View File

@@ -6,40 +6,51 @@
"use strict"; "use strict";
const {interfaces: Ci, utils: Cu} = Components; const {interfaces: Ci, utils: Cu} = Components;
const {actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {perfService} = Cu.import("resource://activity-stream/common/PerfService.jsm", {});
Cu.import("resource://gre/modules/ClientID.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "ClientID",
"resource://gre/modules/ClientID.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "perfService",
"resource://activity-stream/common/PerfService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySender",
"resource://activity-stream/lib/TelemetrySender.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator", XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
"@mozilla.org/uuid-generator;1", "@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator"); "nsIUUIDGenerator");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySender",
"resource://activity-stream/lib/TelemetrySender.jsm");
this.TelemetryFeed = class TelemetryFeed { this.TelemetryFeed = class TelemetryFeed {
constructor(options) { constructor(options) {
this.sessions = new Map(); this.sessions = new Map();
this.telemetryClientId = null;
this.telemetrySender = null;
} }
async init() { init() {
Services.obs.addObserver(this.browserOpenNewtabStart, "browser-open-newtab-start"); Services.obs.addObserver(this.browserOpenNewtabStart, "browser-open-newtab-start");
// TelemetrySender adds pref observers, so we initialize it after INIT
this.telemetrySender = new TelemetrySender();
const id = await ClientID.getClientID();
this.telemetryClientId = id;
} }
browserOpenNewtabStart() { browserOpenNewtabStart() {
perfService.mark("browser-open-newtab-start"); perfService.mark("browser-open-newtab-start");
} }
/**
* Lazily get the Telemetry id promise
*/
get telemetryClientId() {
Object.defineProperty(this, "telemetryClientId", {value: ClientID.getClientID()});
return this.telemetryClientId;
}
/**
* Lazily initialize TelemetrySender to send pings
*/
get telemetrySender() {
Object.defineProperty(this, "telemetrySender", {value: new TelemetrySender()});
return this.telemetrySender;
}
/** /**
* addSession - Start tracking a new session * addSession - Start tracking a new session
* *
@@ -112,10 +123,10 @@ this.TelemetryFeed = class TelemetryFeed {
* @param {string} id The portID of the session, if a session is relevant (optional) * @param {string} id The portID of the session, if a session is relevant (optional)
* @return {obj} A telemetry ping * @return {obj} A telemetry ping
*/ */
createPing(portID) { async createPing(portID) {
const appInfo = this.store.getState().App; const appInfo = this.store.getState().App;
const ping = { const ping = {
client_id: this.telemetryClientId, client_id: await this.telemetryClientId,
addon_version: appInfo.version, addon_version: appInfo.version,
locale: appInfo.locale locale: appInfo.locale
}; };
@@ -131,34 +142,34 @@ this.TelemetryFeed = class TelemetryFeed {
return ping; return ping;
} }
createUserEvent(action) { async createUserEvent(action) {
return Object.assign( return Object.assign(
this.createPing(au.getPortIdOfSender(action)), await this.createPing(au.getPortIdOfSender(action)),
action.data, action.data,
{action: "activity_stream_user_event"} {action: "activity_stream_user_event"}
); );
} }
createUndesiredEvent(action) { async createUndesiredEvent(action) {
return Object.assign( return Object.assign(
this.createPing(au.getPortIdOfSender(action)), await this.createPing(au.getPortIdOfSender(action)),
{value: 0}, // Default value {value: 0}, // Default value
action.data, action.data,
{action: "activity_stream_undesired_event"} {action: "activity_stream_undesired_event"}
); );
} }
createPerformanceEvent(action) { async createPerformanceEvent(action) {
return Object.assign( return Object.assign(
this.createPing(au.getPortIdOfSender(action)), await this.createPing(au.getPortIdOfSender(action)),
action.data, action.data,
{action: "activity_stream_performance_event"} {action: "activity_stream_performance_event"}
); );
} }
createSessionEndEvent(session) { async createSessionEndEvent(session) {
return Object.assign( return Object.assign(
this.createPing(), await this.createPing(),
{ {
session_id: session.session_id, session_id: session.session_id,
page: session.page, page: session.page,
@@ -169,8 +180,8 @@ this.TelemetryFeed = class TelemetryFeed {
); );
} }
sendEvent(event) { async sendEvent(eventPromise) {
this.telemetrySender.sendPing(event); this.telemetrySender.sendPing(await eventPromise);
} }
onAction(action) { onAction(action) {
@@ -201,8 +212,10 @@ this.TelemetryFeed = class TelemetryFeed {
Services.obs.removeObserver(this.browserOpenNewtabStart, Services.obs.removeObserver(this.browserOpenNewtabStart,
"browser-open-newtab-start"); "browser-open-newtab-start");
this.telemetrySender.uninit(); // Only uninit if the getter has initialized it
this.telemetrySender = null; if (Object.prototype.hasOwnProperty.call(this, "telemetrySender")) {
this.telemetrySender.uninit();
}
// TODO: Send any unfinished sessions // TODO: Send any unfinished sessions
} }
}; };

View File

@@ -3,11 +3,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const {interfaces: Ci, utils: Cu} = Components; const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["fetch"]); Cu.importGlobalProperties(["fetch"]);
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Console.jsm"); // eslint-disable-line no-console XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
// This is intentionally a different pref-branch than the SDK-based add-on // This is intentionally a different pref-branch than the SDK-based add-on
// used, to avoid extra weirdness for people who happen to have the SDK-based // used, to avoid extra weirdness for people who happen to have the SDK-based
@@ -71,7 +72,7 @@ TelemetrySender.prototype = {
this._fhrEnabled = prefVal; this._fhrEnabled = prefVal;
}, },
async sendPing(data) { sendPing(data) {
if (this.logging) { if (this.logging) {
// performance related pings cause a lot of logging, so we mute them // performance related pings cause a lot of logging, so we mute them
if (data.action !== "activity_stream_performance") { if (data.action !== "activity_stream_performance") {
@@ -81,7 +82,7 @@ TelemetrySender.prototype = {
if (!this.enabled) { if (!this.enabled) {
return Promise.resolve(); return Promise.resolve();
} }
return fetch(this._pingEndpoint, {method: "POST", body: data}).then(response => { return fetch(this._pingEndpoint, {method: "POST", body: JSON.stringify(data)}).then(response => {
if (!response.ok) { if (!response.ok) {
Cu.reportError(`Ping failure with HTTP response code: ${response.status}`); Cu.reportError(`Ping failure with HTTP response code: ${response.status}`);
} }

View File

@@ -4,58 +4,95 @@
"use strict"; "use strict";
const {utils: Cu} = Components; const {utils: Cu} = Components;
const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {}); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/NewTabUtils.jsm"); const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
Cu.import("resource:///modules/PreviewProvider.jsm"); const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PreviewProvider",
"resource:///modules/PreviewProvider.jsm");
const TOP_SITES_SHOWMORE_LENGTH = 12; const TOP_SITES_SHOWMORE_LENGTH = 12;
const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
const DEFAULT_TOP_SITES = [ const DEFAULT_TOP_SITES = [];
{"url": "https://www.facebook.com/"},
{"url": "https://www.youtube.com/"},
{"url": "http://www.amazon.com/"},
{"url": "https://www.yahoo.com/"},
{"url": "http://www.ebay.com"},
{"url": "https://twitter.com/"}
].map(row => Object.assign(row, {isDefault: true}));
this.TopSitesFeed = class TopSitesFeed { this.TopSitesFeed = class TopSitesFeed {
constructor() { constructor() {
this.lastUpdated = 0; this.lastUpdated = 0;
} }
init() {
// Add default sites if any based on the pref
let sites = new Prefs().get("default.sites");
if (sites) {
for (const url of sites.split(",")) {
DEFAULT_TOP_SITES.push({
isDefault: true,
url
});
}
}
}
async getScreenshot(url) { async getScreenshot(url) {
let screenshot = await PreviewProvider.getThumbnail(url); let screenshot = await PreviewProvider.getThumbnail(url);
const action = {type: at.SCREENSHOT_UPDATED, data: {url, screenshot}}; const action = {type: at.SCREENSHOT_UPDATED, data: {url, screenshot}};
this.store.dispatch(ac.BroadcastToContent(action)); this.store.dispatch(ac.BroadcastToContent(action));
} }
sortLinks(frecent, pinned) {
let sortedLinks = [...frecent, ...DEFAULT_TOP_SITES];
sortedLinks = sortedLinks.filter(link => !NewTabUtils.pinnedLinks.isPinned(link));
// Insert the pinned links in their specified location
pinned.forEach((val, index) => {
if (!val) { return; }
let link = Object.assign({}, val, {isPinned: true, pinIndex: index, pinTitle: val.title});
if (index > sortedLinks.length) {
sortedLinks[index] = link;
} else {
sortedLinks.splice(index, 0, link);
}
});
return sortedLinks.slice(0, TOP_SITES_SHOWMORE_LENGTH);
}
async getLinksWithDefaults(action) { async getLinksWithDefaults(action) {
let links = await NewTabUtils.activityStreamLinks.getTopSites(); let pinned = NewTabUtils.pinnedLinks.links;
let frecent = await NewTabUtils.activityStreamLinks.getTopSites();
if (!links) { if (!frecent) {
links = []; frecent = [];
} else { } else {
links = links.filter(link => link && link.type !== "affiliate").slice(0, 12); frecent = frecent.filter(link => link && link.type !== "affiliate");
} }
if (links.length < TOP_SITES_SHOWMORE_LENGTH) { return this.sortLinks(frecent, pinned);
links = [...links, ...DEFAULT_TOP_SITES].slice(0, TOP_SITES_SHOWMORE_LENGTH);
}
return links;
} }
async refresh(action) { async refresh(action) {
const links = await this.getLinksWithDefaults(); const links = await this.getLinksWithDefaults();
// First, cache existing screenshots in case we need to reuse them
const currentScreenshots = {};
for (const link of this.store.getState().TopSites.rows) {
if (link.screenshot) {
currentScreenshots[link.url] = link.screenshot;
}
}
// Now, get a screenshot for every item
for (let link of links) {
if (currentScreenshots[link.url]) {
link.screenshot = currentScreenshots[link.url];
} else {
this.getScreenshot(link.url);
}
}
const newAction = {type: at.TOP_SITES_UPDATED, data: links}; const newAction = {type: at.TOP_SITES_UPDATED, data: links};
// Send an update to content so the preloaded tab can get the updated content // Send an update to content so the preloaded tab can get the updated content
this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget)); this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
this.lastUpdated = Date.now(); this.lastUpdated = Date.now();
// Now, get a screenshot for every item
for (let link of links) {
this.getScreenshot(link.url);
}
} }
openNewWindow(action, isPrivate = false) { openNewWindow(action, isPrivate = false) {
const win = action._target.browser.ownerGlobal; const win = action._target.browser.ownerGlobal;
@@ -64,15 +101,20 @@ this.TopSitesFeed = class TopSitesFeed {
onAction(action) { onAction(action) {
let realRows; let realRows;
switch (action.type) { switch (action.type) {
case at.INIT:
this.init();
break;
case at.NEW_TAB_LOAD: case at.NEW_TAB_LOAD:
// Only check against real rows returned from history, not default ones. // Only check against real rows returned from history, not default ones.
realRows = this.store.getState().TopSites.rows.filter(row => !row.isDefault); realRows = this.store.getState().TopSites.rows.filter(row => !row.isDefault);
// When a new tab is opened, if we don't have enough top sites yet, refresh the data. if (
if (realRows.length < TOP_SITES_SHOWMORE_LENGTH) { // When a new tab is opened, if we don't have enough top sites yet, refresh the data.
this.refresh(action); (realRows.length < TOP_SITES_SHOWMORE_LENGTH) ||
} else if (Date.now() - this.lastUpdated >= UPDATE_TIME) {
// When a new tab is opened, if the last time we refreshed the data // When a new tab is opened, if the last time we refreshed the data
// is greater than 15 minutes, refresh the data. // is greater than 15 minutes, refresh the data.
(Date.now() - this.lastUpdated >= UPDATE_TIME)
) {
this.refresh(action); this.refresh(action);
} }
break; break;

View File

@@ -31,6 +31,9 @@ const UserEventAction = Joi.object().keys({
"SEARCH", "SEARCH",
"BLOCK", "BLOCK",
"DELETE", "DELETE",
"DELETE_CONFIRM",
"DIALOG_CANCEL",
"DIALOG_OPEN",
"OPEN_NEW_WINDOW", "OPEN_NEW_WINDOW",
"OPEN_PRIVATE_WINDOW", "OPEN_PRIVATE_WINDOW",
"OPEN_NEWTAB_PREFS", "OPEN_NEWTAB_PREFS",

View File

@@ -1,5 +1,5 @@
const {reducers, INITIAL_STATE} = require("common/Reducers.jsm"); const {reducers, INITIAL_STATE} = require("common/Reducers.jsm");
const {TopSites, App, Prefs} = reducers; const {TopSites, App, Prefs, Dialog} = reducers;
const {actionTypes: at} = require("common/Actions.jsm"); const {actionTypes: at} = require("common/Actions.jsm");
describe("Reducers", () => { describe("Reducers", () => {
@@ -147,4 +147,30 @@ describe("Reducers", () => {
}); });
}); });
}); });
describe("Dialog", () => {
it("should return INITIAL_STATE by default", () => {
assert.equal(INITIAL_STATE.Dialog, Dialog(undefined, {type: "non_existent"}));
});
it("should toggle visible to true on DIALOG_OPEN", () => {
const action = {type: at.DIALOG_OPEN};
const nextState = Dialog(INITIAL_STATE.Dialog, action);
assert.isTrue(nextState.visible);
});
it("should pass url data on DIALOG_OPEN", () => {
const action = {type: at.DIALOG_OPEN, data: "some url"};
const nextState = Dialog(INITIAL_STATE.Dialog, action);
assert.equal(nextState.data, action.data);
});
it("should toggle visible to false on DIALOG_CANCEL", () => {
const action = {type: at.DIALOG_CANCEL, data: "some url"};
const nextState = Dialog(INITIAL_STATE.Dialog, action);
assert.isFalse(nextState.visible);
});
it("should return inital state on DELETE_HISTORY_URL", () => {
const action = {type: at.DELETE_HISTORY_URL};
const nextState = Dialog(INITIAL_STATE.Dialog, action);
assert.deepEqual(INITIAL_STATE.Dialog, nextState);
});
});
}); });

View File

@@ -83,27 +83,27 @@ describe("ActivityStream", () => {
}); });
describe("feeds", () => { describe("feeds", () => {
it("should create a Localization feed", () => { it("should create a Localization feed", () => {
const feed = as.feeds["feeds.localization"](); const feed = as.feeds.get("feeds.localization")();
assert.instanceOf(feed, Fake); assert.instanceOf(feed, Fake);
}); });
it("should create a NewTabInit feed", () => { it("should create a NewTabInit feed", () => {
const feed = as.feeds["feeds.newtabinit"](); const feed = as.feeds.get("feeds.newtabinit")();
assert.instanceOf(feed, Fake); assert.instanceOf(feed, Fake);
}); });
it("should create a Places feed", () => { it("should create a Places feed", () => {
const feed = as.feeds["feeds.places"](); const feed = as.feeds.get("feeds.places")();
assert.instanceOf(feed, Fake); assert.instanceOf(feed, Fake);
}); });
it("should create a TopSites feed", () => { it("should create a TopSites feed", () => {
const feed = as.feeds["feeds.topsites"](); const feed = as.feeds.get("feeds.topsites")();
assert.instanceOf(feed, Fake); assert.instanceOf(feed, Fake);
}); });
it("should create a Telemetry feed", () => { it("should create a Telemetry feed", () => {
const feed = as.feeds["feeds.telemetry"](); const feed = as.feeds.get("feeds.telemetry")();
assert.instanceOf(feed, Fake); assert.instanceOf(feed, Fake);
}); });
it("should create a Prefs feed", () => { it("should create a Prefs feed", () => {
const feed = as.feeds["feeds.prefs"](); const feed = as.feeds.get("feeds.prefs")();
assert.instanceOf(feed, Fake); assert.instanceOf(feed, Fake);
}); });
}); });

View File

@@ -1,41 +1,94 @@
const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream."; const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream.";
const {Prefs, DefaultPrefs} = require("lib/ActivityStreamPrefs.jsm"); const {Prefs, DefaultPrefs} = require("lib/ActivityStreamPrefs.jsm");
const TEST_PREF_CONFIG = [ const TEST_PREF_CONFIG = new Map([
{name: "foo", value: true}, ["foo", {value: true}],
{name: "bar", value: "BAR"}, ["bar", {value: "BAR"}],
{name: "baz", value: 1} ["baz", {value: 1}],
]; ["qux", {value: "foo", value_local_dev: "foofoo"}]
]);
describe("ActivityStreamPrefs", () => { describe("ActivityStreamPrefs", () => {
describe("Prefs", () => { describe("Prefs", () => {
let p;
beforeEach(() => {
p = new Prefs();
});
it("should have get, set, and observe methods", () => { it("should have get, set, and observe methods", () => {
const p = new Prefs();
assert.property(p, "get"); assert.property(p, "get");
assert.property(p, "set"); assert.property(p, "set");
assert.property(p, "observe"); assert.property(p, "observe");
}); });
describe(".branchName", () => { describe(".branchName", () => {
it("should return the activity stream branch by default", () => { it("should return the activity stream branch by default", () => {
const p = new Prefs();
assert.equal(p.branchName, ACTIVITY_STREAM_PREF_BRANCH); assert.equal(p.branchName, ACTIVITY_STREAM_PREF_BRANCH);
}); });
it("should return the custom branch name if it was passed to the constructor", () => { it("should return the custom branch name if it was passed to the constructor", () => {
const p = new Prefs("foo"); p = new Prefs("foo");
assert.equal(p.branchName, "foo"); assert.equal(p.branchName, "foo");
}); });
}); });
describe("#observeBranch", () => {
let listener;
beforeEach(() => {
p._prefBranch = {addObserver: sinon.stub()};
listener = {onPrefChanged: sinon.stub()};
p.observeBranch(listener);
});
it("should add an observer", () => {
assert.calledOnce(p._prefBranch.addObserver);
assert.calledWith(p._prefBranch.addObserver, "");
});
it("should store the listener", () => {
assert.equal(p._branchObservers.size, 1);
assert.ok(p._branchObservers.has(listener));
});
it("should call listener's onPrefChanged", () => {
p._branchObservers.get(listener)();
assert.calledOnce(listener.onPrefChanged);
});
});
describe("#ignoreBranch", () => {
let listener;
beforeEach(() => {
p._prefBranch = {
addObserver: sinon.stub(),
removeObserver: sinon.stub()
};
listener = {};
p.observeBranch(listener);
});
it("should remove the observer", () => {
p.ignoreBranch(listener);
assert.calledOnce(p._prefBranch.removeObserver);
assert.calledWith(p._prefBranch.removeObserver, p._prefBranch.addObserver.firstCall.args[0]);
});
it("should remove the listener", () => {
assert.equal(p._branchObservers.size, 1);
p.ignoreBranch(listener);
assert.equal(p._branchObservers.size, 0);
});
});
}); });
describe("DefaultPrefs", () => { describe("DefaultPrefs", () => {
describe("#init", () => { describe("#init", () => {
let defaultPrefs; let defaultPrefs;
let sandbox;
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create();
defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG); defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
sinon.spy(defaultPrefs.branch, "setBoolPref"); sinon.spy(defaultPrefs.branch, "setBoolPref");
sinon.spy(defaultPrefs.branch, "setStringPref"); sinon.spy(defaultPrefs.branch, "setStringPref");
sinon.spy(defaultPrefs.branch, "setIntPref"); sinon.spy(defaultPrefs.branch, "setIntPref");
}); });
afterEach(() => {
sandbox.restore();
});
it("should initialize a boolean pref", () => { it("should initialize a boolean pref", () => {
defaultPrefs.init(); defaultPrefs.init();
assert.calledWith(defaultPrefs.branch.setBoolPref, "foo", true); assert.calledWith(defaultPrefs.branch.setBoolPref, "foo", true);
@@ -48,14 +101,19 @@ describe("ActivityStreamPrefs", () => {
defaultPrefs.init(); defaultPrefs.init();
assert.calledWith(defaultPrefs.branch.setIntPref, "baz", 1); assert.calledWith(defaultPrefs.branch.setIntPref, "baz", 1);
}); });
it("should initialize a pref with value_local_dev if Firefox is a local build", () => {
sandbox.stub(global.Services.prefs, "getStringPref", () => "default"); // eslint-disable-line max-nested-callbacks
defaultPrefs.init();
assert.calledWith(defaultPrefs.branch.setStringPref, "qux", "foofoo");
});
}); });
describe("#reset", () => { describe("#reset", () => {
it("should clear user preferences for each pref in the config", () => { it("should clear user preferences for each pref in the config", () => {
const defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG); const defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
sinon.spy(defaultPrefs.branch, "clearUserPref"); sinon.spy(defaultPrefs.branch, "clearUserPref");
defaultPrefs.reset(); defaultPrefs.reset();
for (const pref of TEST_PREF_CONFIG) { for (const name of TEST_PREF_CONFIG.keys()) {
assert.calledWith(defaultPrefs.branch.clearUserPref, pref.name); assert.calledWith(defaultPrefs.branch.clearUserPref, name);
} }
}); });
}); });

View File

@@ -28,6 +28,16 @@ describe("PlacesFeed", () => {
history: {addObserver: sandbox.spy(), removeObserver: sandbox.spy()}, history: {addObserver: sandbox.spy(), removeObserver: sandbox.spy()},
bookmarks: {TYPE_BOOKMARK, addObserver: sandbox.spy(), removeObserver: sandbox.spy()} bookmarks: {TYPE_BOOKMARK, addObserver: sandbox.spy(), removeObserver: sandbox.spy()}
}); });
global.Components.classes["@mozilla.org/browser/nav-history-service;1"] = {
getService() {
return global.PlacesUtils.history;
}
};
global.Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"] = {
getService() {
return global.PlacesUtils.bookmarks;
}
};
sandbox.spy(global.Services.obs, "addObserver"); sandbox.spy(global.Services.obs, "addObserver");
sandbox.spy(global.Services.obs, "removeObserver"); sandbox.spy(global.Services.obs, "removeObserver");
sandbox.spy(global.Components.utils, "reportError"); sandbox.spy(global.Components.utils, "reportError");

View File

@@ -1,18 +1,20 @@
const {PrefsFeed} = require("lib/PrefsFeed.jsm"); const {PrefsFeed} = require("lib/PrefsFeed.jsm");
const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm"); const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
const FAKE_PREFS = [{name: "foo", value: 1}, {name: "bar", value: 2}]; const FAKE_PREFS = new Map([["foo", {value: 1}], ["bar", {value: 2}]]);
describe("PrefsFeed", () => { describe("PrefsFeed", () => {
let feed; let feed;
beforeEach(() => { beforeEach(() => {
feed = new PrefsFeed(FAKE_PREFS.map(p => p.name)); feed = new PrefsFeed(FAKE_PREFS);
feed.store = {dispatch: sinon.spy()}; feed.store = {dispatch: sinon.spy()};
feed._prefs = { feed._prefs = {
get: sinon.spy(item => FAKE_PREFS.filter(p => p.name === item)[0].value), get: sinon.spy(item => FAKE_PREFS.get(item).value),
set: sinon.spy(), set: sinon.spy(),
observe: sinon.spy(), observe: sinon.spy(),
ignore: sinon.spy() observeBranch: sinon.spy(),
ignore: sinon.spy(),
ignoreBranch: sinon.spy()
}; };
}); });
it("should set a pref when a SET_PREF action is received", () => { it("should set a pref when a SET_PREF action is received", () => {
@@ -25,27 +27,15 @@ describe("PrefsFeed", () => {
assert.equal(feed.store.dispatch.firstCall.args[0].type, at.PREFS_INITIAL_VALUES); assert.equal(feed.store.dispatch.firstCall.args[0].type, at.PREFS_INITIAL_VALUES);
assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {foo: 1, bar: 2}); assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {foo: 1, bar: 2});
}); });
it("should add one observer per pref on init", () => { it("should add one branch observer on init", () => {
feed.onAction({type: at.INIT}); feed.onAction({type: at.INIT});
FAKE_PREFS.forEach(pref => { assert.calledOnce(feed._prefs.observeBranch);
assert.calledWith(feed._prefs.observe, pref.name); assert.calledWith(feed._prefs.observeBranch, feed);
assert.isTrue(feed._observers.has(pref.name));
});
}); });
it("should call onPrefChanged when an observer is called", () => { it("should remove the branch observer on uninit", () => {
sinon.stub(feed, "onPrefChanged");
feed.onAction({type: at.INIT});
const handlerForFoo = feed._observers.get("foo");
handlerForFoo(true);
assert.calledWith(feed.onPrefChanged, "foo", true);
});
it("should remove all observers on uninit", () => {
feed.onAction({type: at.UNINIT}); feed.onAction({type: at.UNINIT});
FAKE_PREFS.forEach(pref => { assert.calledOnce(feed._prefs.ignoreBranch);
assert.calledWith(feed._prefs.ignore, pref.name); assert.calledWith(feed._prefs.ignoreBranch, feed);
});
}); });
it("should send a PREF_CHANGED action when onPrefChanged is called", () => { it("should send a PREF_CHANGED action when onPrefChanged is called", () => {
feed.onPrefChanged("foo", 2); feed.onPrefChanged("foo", 2);

View File

@@ -43,8 +43,8 @@ describe("Store", () => {
describe("#initFeed", () => { describe("#initFeed", () => {
it("should add an instance of the feed to .feeds", () => { it("should add an instance of the feed to .feeds", () => {
class Foo {} class Foo {}
store._prefs.set("foo", false); store._prefs.set("foo", true);
store.init({foo: () => new Foo()}); store.init(new Map([["foo", () => new Foo()]]));
store.initFeed("foo"); store.initFeed("foo");
assert.isTrue(store.feeds.has("foo"), "foo is set"); assert.isTrue(store.feeds.has("foo"), "foo is set");
@@ -52,7 +52,7 @@ describe("Store", () => {
}); });
it("should add a .store property to the feed", () => { it("should add a .store property to the feed", () => {
class Foo {} class Foo {}
store._feedFactories = {foo: () => new Foo()}; store._feedFactories = new Map([["foo", () => new Foo()]]);
store.initFeed("foo"); store.initFeed("foo");
assert.propertyVal(store.feeds.get("foo"), "store", store); assert.propertyVal(store.feeds.get("foo"), "store", store);
@@ -70,7 +70,7 @@ describe("Store", () => {
feed = {uninit: sinon.spy()}; feed = {uninit: sinon.spy()};
return feed; return feed;
} }
store._feedFactories = {foo: createFeed}; store._feedFactories = new Map([["foo", createFeed]]);
store.initFeed("foo"); store.initFeed("foo");
store.uninitFeed("foo"); store.uninitFeed("foo");
@@ -79,7 +79,7 @@ describe("Store", () => {
}); });
it("should remove the feed from .feeds", () => { it("should remove the feed from .feeds", () => {
class Foo {} class Foo {}
store._feedFactories = {foo: () => new Foo()}; store._feedFactories = new Map([["foo", () => new Foo()]]);
store.initFeed("foo"); store.initFeed("foo");
store.uninitFeed("foo"); store.uninitFeed("foo");
@@ -87,69 +87,70 @@ describe("Store", () => {
assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds"); assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
}); });
}); });
describe("maybeStartFeedAndListenForPrefChanges", () => { describe("onPrefChanged", () => {
beforeEach(() => { beforeEach(() => {
sinon.stub(store, "initFeed"); sinon.stub(store, "initFeed");
sinon.stub(store, "uninitFeed"); sinon.stub(store, "uninitFeed");
});
it("should initialize the feed if the Pref is set to true", () => {
store._prefs.set("foo", true);
store.maybeStartFeedAndListenForPrefChanges("foo");
assert.calledWith(store.initFeed, "foo");
});
it("should not initialize the feed if the Pref is set to false", () => {
store._prefs.set("foo", false); store._prefs.set("foo", false);
store.maybeStartFeedAndListenForPrefChanges("foo"); store.init(new Map([["foo", () => ({})]]));
});
it("should initialize the feed if called with true", () => {
store.onPrefChanged("foo", true);
assert.calledWith(store.initFeed, "foo");
assert.notCalled(store.uninitFeed);
});
it("should uninitialize the feed if called with false", () => {
store.onPrefChanged("foo", false);
assert.calledWith(store.uninitFeed, "foo");
assert.notCalled(store.initFeed); assert.notCalled(store.initFeed);
}); });
it("should observe the pref", () => { it("should do nothing if not an expected feed", () => {
sinon.stub(store._prefs, "observe"); store.onPrefChanged("bar", false);
store.maybeStartFeedAndListenForPrefChanges("foo");
assert.calledWith(store._prefs.observe, "foo", store._prefHandlers.get("foo")); assert.notCalled(store.initFeed);
}); assert.notCalled(store.uninitFeed);
describe("handler", () => {
let handler;
beforeEach(() => {
store.maybeStartFeedAndListenForPrefChanges("foo");
handler = store._prefHandlers.get("foo");
});
it("should initialize the feed if called with true", () => {
handler(true);
assert.calledWith(store.initFeed, "foo");
});
it("should uninitialize the feed if called with false", () => {
handler(false);
assert.calledWith(store.uninitFeed, "foo");
});
}); });
}); });
describe("#init", () => { describe("#init", () => {
it("should call .maybeStartFeedAndListenForPrefChanges with each key", () => { it("should call .initFeed with each key", () => {
sinon.stub(store, "maybeStartFeedAndListenForPrefChanges"); sinon.stub(store, "initFeed");
store.init({foo: () => {}, bar: () => {}}); store._prefs.set("foo", true);
assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "foo"); store._prefs.set("bar", true);
assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "bar"); store.init(new Map([["foo", () => {}], ["bar", () => {}]]));
assert.calledWith(store.initFeed, "foo");
assert.calledWith(store.initFeed, "bar");
});
it("should not initialize the feed if the Pref is set to false", () => {
sinon.stub(store, "initFeed");
store._prefs.set("foo", false);
store.init(new Map([["foo", () => {}]]));
assert.notCalled(store.initFeed);
});
it("should observe the pref branch", () => {
sinon.stub(store._prefs, "observeBranch");
store.init(new Map());
assert.calledOnce(store._prefs.observeBranch);
assert.calledWith(store._prefs.observeBranch, store);
}); });
it("should initialize the ActivityStreamMessageChannel channel", () => { it("should initialize the ActivityStreamMessageChannel channel", () => {
store.init(); store.init(new Map());
assert.calledOnce(store._messageChannel.createChannel); assert.calledOnce(store._messageChannel.createChannel);
}); });
}); });
describe("#uninit", () => { describe("#uninit", () => {
it("should clear .feeds, ._prefHandlers, and ._feedFactories", () => { it("should clear .feeds and ._feedFactories", () => {
store._prefs.set("a", true); store._prefs.set("a", true);
store._prefs.set("b", true); store.init(new Map([
store._prefs.set("c", true); ["a", () => ({})],
store.init({ ["b", () => ({})],
a: () => ({}), ["c", () => ({})]
b: () => ({}), ]));
c: () => ({})
});
store.uninit(); store.uninit();
assert.equal(store.feeds.size, 0); assert.equal(store.feeds.size, 0);
assert.equal(store._prefHandlers.size, 0);
assert.isNull(store._feedFactories); assert.isNull(store._feedFactories);
}); });
it("should destroy the ActivityStreamMessageChannel channel", () => { it("should destroy the ActivityStreamMessageChannel channel", () => {
@@ -171,7 +172,7 @@ describe("Store", () => {
const action = {type: "FOO"}; const action = {type: "FOO"};
store._prefs.set("sub", true); store._prefs.set("sub", true);
store.init({sub: () => sub}); store.init(new Map([["sub", () => sub]]));
dispatch(action); dispatch(action);

View File

@@ -47,20 +47,16 @@ describe("TelemetryFeed", () => {
globals.restore(); globals.restore();
}); });
describe("#init", () => { describe("#init", () => {
it("should add .telemetrySender, a TelemetrySender instance", async () => { it("should add .telemetrySender, a TelemetrySender instance", () => {
assert.isNull(instance.telemetrySender);
await instance.init();
assert.instanceOf(instance.telemetrySender, TelemetrySender); assert.instanceOf(instance.telemetrySender, TelemetrySender);
}); });
it("should add .telemetryClientId from the ClientID module", async () => { it("should add .telemetryClientId from the ClientID module", async () => {
assert.isNull(instance.telemetryClientId); assert.equal(await instance.telemetryClientId, FAKE_TELEMETRY_ID);
await instance.init();
assert.equal(instance.telemetryClientId, FAKE_TELEMETRY_ID);
}); });
it("should make this.browserOpenNewtabStart() observe browser-open-newtab-start", async () => { it("should make this.browserOpenNewtabStart() observe browser-open-newtab-start", () => {
sandbox.spy(Services.obs, "addObserver"); sandbox.spy(Services.obs, "addObserver");
await instance.init(); instance.init();
assert.calledOnce(Services.obs.addObserver); assert.calledOnce(Services.obs.addObserver);
assert.calledWithExactly(Services.obs.addObserver, assert.calledWithExactly(Services.obs.addObserver,
@@ -130,19 +126,19 @@ describe("TelemetryFeed", () => {
describe("ping creators", () => { describe("ping creators", () => {
beforeEach(async () => await instance.init()); beforeEach(async () => await instance.init());
describe("#createPing", () => { describe("#createPing", () => {
it("should create a valid base ping without a session if no portID is supplied", () => { it("should create a valid base ping without a session if no portID is supplied", async () => {
const ping = instance.createPing(); const ping = await instance.createPing();
assert.validate(ping, BasePing); assert.validate(ping, BasePing);
assert.notProperty(ping, "session_id"); assert.notProperty(ping, "session_id");
}); });
it("should create a valid base ping with session info if a portID is supplied", () => { it("should create a valid base ping with session info if a portID is supplied", async () => {
// Add a session // Add a session
const portID = "foo"; const portID = "foo";
instance.addSession(portID); instance.addSession(portID);
const sessionID = instance.sessions.get(portID).session_id; const sessionID = instance.sessions.get(portID).session_id;
// Create a ping referencing the session // Create a ping referencing the session
const ping = instance.createPing(portID); const ping = await instance.createPing(portID);
assert.validate(ping, BasePing); assert.validate(ping, BasePing);
// Make sure we added the right session-related stuff to the ping // Make sure we added the right session-related stuff to the ping
@@ -151,12 +147,12 @@ describe("TelemetryFeed", () => {
}); });
}); });
describe("#createUserEvent", () => { describe("#createUserEvent", () => {
it("should create a valid event", () => { it("should create a valid event", async () => {
const portID = "foo"; const portID = "foo";
const data = {source: "TOP_SITES", event: "CLICK"}; const data = {source: "TOP_SITES", event: "CLICK"};
const action = ac.SendToMain(ac.UserEvent(data), portID); const action = ac.SendToMain(ac.UserEvent(data), portID);
const session = addSession(portID); const session = addSession(portID);
const ping = instance.createUserEvent(action); const ping = await instance.createUserEvent(action);
// Is it valid? // Is it valid?
assert.validate(ping, UserEventPing); assert.validate(ping, UserEventPing);
@@ -165,21 +161,21 @@ describe("TelemetryFeed", () => {
}); });
}); });
describe("#createUndesiredEvent", () => { describe("#createUndesiredEvent", () => {
it("should create a valid event without a session", () => { it("should create a valid event without a session", async () => {
const action = ac.UndesiredEvent({source: "TOP_SITES", event: "MISSING_IMAGE", value: 10}); const action = ac.UndesiredEvent({source: "TOP_SITES", event: "MISSING_IMAGE", value: 10});
const ping = instance.createUndesiredEvent(action); const ping = await instance.createUndesiredEvent(action);
// Is it valid? // Is it valid?
assert.validate(ping, UndesiredPing); assert.validate(ping, UndesiredPing);
// Does it have the right value? // Does it have the right value?
assert.propertyVal(ping, "value", 10); assert.propertyVal(ping, "value", 10);
}); });
it("should create a valid event with a session", () => { it("should create a valid event with a session", async () => {
const portID = "foo"; const portID = "foo";
const data = {source: "TOP_SITES", event: "MISSING_IMAGE", value: 10}; const data = {source: "TOP_SITES", event: "MISSING_IMAGE", value: 10};
const action = ac.SendToMain(ac.UndesiredEvent(data), portID); const action = ac.SendToMain(ac.UndesiredEvent(data), portID);
const session = addSession(portID); const session = addSession(portID);
const ping = instance.createUndesiredEvent(action); const ping = await instance.createUndesiredEvent(action);
// Is it valid? // Is it valid?
assert.validate(ping, UndesiredPing); assert.validate(ping, UndesiredPing);
@@ -190,21 +186,21 @@ describe("TelemetryFeed", () => {
}); });
}); });
describe("#createPerformanceEvent", () => { describe("#createPerformanceEvent", () => {
it("should create a valid event without a session", () => { it("should create a valid event without a session", async () => {
const action = ac.PerfEvent({event: "SCREENSHOT_FINISHED", value: 100}); const action = ac.PerfEvent({event: "SCREENSHOT_FINISHED", value: 100});
const ping = instance.createPerformanceEvent(action); const ping = await instance.createPerformanceEvent(action);
// Is it valid? // Is it valid?
assert.validate(ping, PerfPing); assert.validate(ping, PerfPing);
// Does it have the right value? // Does it have the right value?
assert.propertyVal(ping, "value", 100); assert.propertyVal(ping, "value", 100);
}); });
it("should create a valid event with a session", () => { it("should create a valid event with a session", async () => {
const portID = "foo"; const portID = "foo";
const data = {event: "PAGE_LOADED", value: 100}; const data = {event: "PAGE_LOADED", value: 100};
const action = ac.SendToMain(ac.PerfEvent(data), portID); const action = ac.SendToMain(ac.PerfEvent(data), portID);
const session = addSession(portID); const session = addSession(portID);
const ping = instance.createPerformanceEvent(action); const ping = await instance.createPerformanceEvent(action);
// Is it valid? // Is it valid?
assert.validate(ping, PerfPing); assert.validate(ping, PerfPing);
@@ -215,8 +211,8 @@ describe("TelemetryFeed", () => {
}); });
}); });
describe("#createSessionEndEvent", () => { describe("#createSessionEndEvent", () => {
it("should create a valid event", () => { it("should create a valid event", async () => {
const ping = instance.createSessionEndEvent({ const ping = await instance.createSessionEndEvent({
session_id: FAKE_UUID, session_id: FAKE_UUID,
page: "about:newtab", page: "about:newtab",
session_duration: 12345, session_duration: 12345,
@@ -236,20 +232,17 @@ describe("TelemetryFeed", () => {
}); });
describe("#sendEvent", () => { describe("#sendEvent", () => {
it("should call telemetrySender", async () => { it("should call telemetrySender", async () => {
await instance.init();
sandbox.stub(instance.telemetrySender, "sendPing"); sandbox.stub(instance.telemetrySender, "sendPing");
const event = {}; const event = {};
instance.sendEvent(event); await instance.sendEvent(Promise.resolve(event));
assert.calledWith(instance.telemetrySender.sendPing, event); assert.calledWith(instance.telemetrySender.sendPing, event);
}); });
}); });
describe("#uninit", () => { describe("#uninit", () => {
it("should call .telemetrySender.uninit and remove it", async () => { it("should call .telemetrySender.uninit", () => {
await instance.init();
const stub = sandbox.stub(instance.telemetrySender, "uninit"); const stub = sandbox.stub(instance.telemetrySender, "uninit");
instance.uninit(); instance.uninit();
assert.calledOnce(stub); assert.calledOnce(stub);
assert.isNull(instance.telemetrySender);
}); });
it("should make this.browserOpenNewtabStart() stop observing browser-open-newtab-start", async () => { it("should make this.browserOpenNewtabStart() stop observing browser-open-newtab-start", async () => {
await instance.init(); await instance.init();

View File

@@ -165,7 +165,7 @@ describe("TelemetrySender", () => {
assert.calledOnce(fetchStub); assert.calledOnce(fetchStub);
assert.calledWithExactly(fetchStub, fakeEndpointUrl, assert.calledWithExactly(fetchStub, fakeEndpointUrl,
{method: "POST", body: fakePingJSON}); {method: "POST", body: JSON.stringify(fakePingJSON)});
}); });
it("should log HTTP failures using Cu.reportError", async () => { it("should log HTTP failures using Cu.reportError", async () => {

View File

@@ -1,23 +1,36 @@
"use strict"; "use strict";
const {TopSitesFeed, UPDATE_TIME, TOP_SITES_SHOWMORE_LENGTH, DEFAULT_TOP_SITES} = require("lib/TopSitesFeed.jsm"); const injector = require("inject!lib/TopSitesFeed.jsm");
const {GlobalOverrider} = require("test/unit/utils"); const {UPDATE_TIME, TOP_SITES_SHOWMORE_LENGTH} = require("lib/TopSitesFeed.jsm");
const {FakePrefs, GlobalOverrider} = require("test/unit/utils");
const action = {meta: {fromTarget: {}}}; const action = {meta: {fromTarget: {}}};
const {actionTypes: at} = require("common/Actions.jsm"); const {actionTypes: at} = require("common/Actions.jsm");
const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `site${i}.com`})); const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `site${i}.com`}));
const FAKE_SCREENSHOT = "data123"; const FAKE_SCREENSHOT = "data123";
describe("Top Sites Feed", () => { describe("Top Sites Feed", () => {
let TopSitesFeed;
let DEFAULT_TOP_SITES;
let feed; let feed;
let globals; let globals;
let sandbox; let sandbox;
let links; let links;
let clock; let clock;
let fakeNewTabUtils;
beforeEach(() => { beforeEach(() => {
globals = new GlobalOverrider(); globals = new GlobalOverrider();
sandbox = globals.sandbox; sandbox = globals.sandbox;
globals.set("NewTabUtils", {activityStreamLinks: {getTopSites: sandbox.spy(() => Promise.resolve(links))}}); fakeNewTabUtils = {
activityStreamLinks: {getTopSites: sandbox.spy(() => Promise.resolve(links))},
pinnedLinks: {
links: [],
isPinned: () => false
}
};
globals.set("NewTabUtils", fakeNewTabUtils);
globals.set("PreviewProvider", {getThumbnail: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))}); globals.set("PreviewProvider", {getThumbnail: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))});
FakePrefs.prototype.prefs["default.sites"] = "https://foo.com/";
({TopSitesFeed, DEFAULT_TOP_SITES} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
feed = new TopSitesFeed(); feed = new TopSitesFeed();
feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }}; feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
links = FAKE_LINKS; links = FAKE_LINKS;
@@ -28,11 +41,74 @@ describe("Top Sites Feed", () => {
clock.restore(); clock.restore();
}); });
it("should have default sites with .isDefault = true", () => { describe("#init", () => {
DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "isDefault", true)); it("should add defaults on INIT", () => {
feed.onAction({type: at.INIT});
assert.ok(DEFAULT_TOP_SITES.length);
});
it("should have default sites with .isDefault = true", () => {
feed.init();
DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "isDefault", true));
});
it("should add no defaults on empty pref", () => {
FakePrefs.prototype.prefs["default.sites"] = "";
feed.init();
assert.equal(DEFAULT_TOP_SITES.length, 0);
});
}); });
describe("#sortLinks", () => {
beforeEach(() => {
feed.init();
});
it("should place pinned links where they belong", () => {
const pinned = [
{"url": "http://github.com/mozilla/activity-stream", "title": "moz/a-s"},
{"url": "http://example.com", "title": "example"}
];
const result = feed.sortLinks(links, pinned);
for (let index of [0, 1]) {
assert.equal(result[index].url, pinned[index].url);
assert.ok(result[index].isPinned);
assert.equal(result[index].pinTitle, pinned[index].title);
assert.equal(result[index].pinIndex, index);
}
assert.deepEqual(result.slice(2), links.slice(0, -2));
});
it("should handle empty slots in the pinned list", () => {
const pinned = [
null,
{"url": "http://github.com/mozilla/activity-stream", "title": "moz/a-s"},
null,
null,
{"url": "http://example.com", "title": "example"}
];
const result = feed.sortLinks(links, pinned);
for (let index of [1, 4]) {
assert.equal(result[index].url, pinned[index].url);
assert.ok(result[index].isPinned);
assert.equal(result[index].pinTitle, pinned[index].title);
assert.equal(result[index].pinIndex, index);
}
result.splice(4, 1);
result.splice(1, 1);
assert.deepEqual(result, links.slice(0, -2));
});
it("should handle a pinned site past the end of the list of frecent+default", () => {
const pinned = [];
pinned[11] = {"url": "http://github.com/mozilla/activity-stream", "title": "moz/a-s"};
const result = feed.sortLinks([], pinned);
assert.equal(result[11].url, pinned[11].url);
assert.isTrue(result[11].isPinned);
assert.equal(result[11].pinTitle, pinned[11].title);
assert.equal(result[11].pinIndex, 11);
});
});
describe("#getLinksWithDefaults", () => { describe("#getLinksWithDefaults", () => {
beforeEach(() => {
feed.init();
});
it("should get the links from NewTabUtils", async () => { it("should get the links from NewTabUtils", async () => {
const result = await feed.getLinksWithDefaults(); const result = await feed.getLinksWithDefaults();
assert.deepEqual(result, links); assert.deepEqual(result, links);
@@ -64,11 +140,21 @@ describe("Top Sites Feed", () => {
assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED); assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, links); assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, links);
}); });
it("should call .getScreenshot for each link", async () => { it("should reuse screenshots for existing links, and call feed.getScreenshot for others", async () => {
sandbox.stub(feed, "getScreenshot"); sandbox.stub(feed, "getScreenshot");
const rows = [{url: FAKE_LINKS[0].url, screenshot: "foo.jpg"}];
feed.store.getState = () => ({TopSites: {rows}});
await feed.refresh(action); await feed.refresh(action);
links.forEach(link => assert.calledWith(feed.getScreenshot, link.url)); const results = feed.store.dispatch.firstCall.args[0].data;
results.forEach(link => {
if (link.url === FAKE_LINKS[0].url) {
assert.equal(link.screenshot, "foo.jpg");
} else {
assert.calledWith(feed.getScreenshot, link.url);
}
});
}); });
}); });
describe("getScreenshot", () => { describe("getScreenshot", () => {

View File

@@ -13,6 +13,7 @@ let overrider = new GlobalOverrider();
overrider.set({ overrider.set({
Components: { Components: {
classes: {},
interfaces: {}, interfaces: {},
utils: { utils: {
import() {}, import() {},
@@ -38,6 +39,7 @@ overrider.set({
removeObserver() {} removeObserver() {}
}, },
prefs: { prefs: {
getStringPref() {},
getDefaultBranch() { getDefaultBranch() {
return { return {
setBoolPref() {}, setBoolPref() {},

View File

@@ -104,6 +104,9 @@ FakePrefs.prototype = {
delete this.observers[prefName]; delete this.observers[prefName];
} }
}, },
_prefBranch: {},
observeBranch(listener) {},
ignoreBranch(listener) {},
prefs: {}, prefs: {},
get(prefName) { return this.prefs[prefName]; }, get(prefName) { return this.prefs[prefName]; },