--- a/browser/extensions/activity-stream/README.md
+++ b/browser/extensions/activity-stream/README.md
@@ -2,9 +2,9 @@
This system add-on replaces the new tab page in Firefox with a new design and
functionality as part of the Activity Stream project. It can be enabled (or disabled)
via the browser.newtabpage.activity-stream.enabled pref.
The files in this directory, including vendor dependencies, are imported from the
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.
--- a/browser/extensions/activity-stream/bootstrap.js
+++ b/browser/extensions/activity-stream/bootstrap.js
@@ -1,25 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["fetch"]);
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
-
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"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 BROWSER_READY_NOTIFICATION = "browser-delayed-startup-finished";
+const BROWSER_READY_NOTIFICATION = "sessionstore-windows-restored";
const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
const REASON_STARTUP_ON_PREF_CHANGE = "PREF_ON";
const RESOURCE_BASE = "resource://activity-stream";
const ACTIVITY_STREAM_OPTIONS = {newTabURL: "about:newtab"};
let activityStream;
let modulesToUnload = new Set();
@@ -57,17 +58,21 @@ XPCOMUtils.defineLazyModuleGetter(this,
*/
function init(reason) {
// Don't re-initialize
if (activityStream && activityStream.initialized) {
return;
}
const options = Object.assign({}, startupData || {}, ACTIVITY_STREAM_OPTIONS);
activityStream = new ActivityStream(options);
- activityStream.init(reason);
+ try {
+ activityStream.init(reason);
+ } catch (e) {
+ Cu.reportError(e);
+ }
}
/**
* uninit - Uninitializes the activityStream instance, if it exsits.This could be
* called by the shutdown() function exposed by bootstrap.js, or it could
* be called when ACTIVITY_STREAM_ENABLED_PREF is changed from true to false.
*
* @param {type} reason Reason for uninitialization. Could be uninstall, upgrade, or PREF_OFF
@@ -108,17 +113,18 @@ function onBrowserReady() {
/**
* observe - nsIObserver callback to handle various browser notifications.
*/
function observe(subject, topic, data) {
switch (topic) {
case 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;
}
}
// The functions below are required by bootstrap.js
this.install = function install(data, reason) {};
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -12,21 +12,30 @@ this.BACKGROUND_PROCESS = 2;
* globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process?
* Use this in action creators if you need different logic
* for ui/background processes.
*/
const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
// Export for tests
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",
"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",
@@ -40,23 +49,19 @@ const actionTypes = [
"PREF_CHANGED",
"SCREENSHOT_UPDATED",
"SET_PREF",
"TELEMETRY_PERFORMANCE_EVENT",
"TELEMETRY_UNDESIRED_EVENT",
"TELEMETRY_USER_EVENT",
"TOP_SITES_UPDATED",
"UNINIT"
-// The line below creates an object like this:
-// {
-// INIT: "INIT",
-// UNINIT: "UNINIT"
-// }
-// It prevents accidentally adding a different key/value name.
-].reduce((obj, type) => { obj[type] = type; return obj; }, {});
+]) {
+ actionTypes[type] = type;
+}
// Helper function for creating routed actions between content and main
// Not intended to be used by consumers
function _RouteMessage(action, options) {
const meta = action.meta ? Object.assign({}, action.meta) : {};
if (!options || !options.from || !options.to) {
throw new Error("Routed Messages must have options as the second parameter, and must at least include a .from and .to property.");
}
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -20,16 +20,20 @@ const INITIAL_STATE = {
// Have we received real data from history yet?
initialized: false,
// The history (and possibly default) links
rows: []
},
Prefs: {
initialized: false,
values: {}
+ },
+ Dialog: {
+ visible: false,
+ data: {}
}
};
function App(prevState = INITIAL_STATE.App, action) {
switch (action.type) {
case at.INIT:
return Object.assign({}, action.data || {}, {initialized: true});
case at.LOCALE_UPDATED: {
@@ -90,26 +94,39 @@ function TopSites(prevState = INITIAL_ST
case at.PLACES_LINK_BLOCKED:
newRows = prevState.rows.filter(val => val.url !== action.data.url);
return Object.assign({}, prevState, {rows: newRows});
default:
return prevState;
}
}
+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) {
let newValues;
switch (action.type) {
case at.PREFS_INITIAL_VALUES:
return Object.assign({}, prevState, {initialized: true, values: action.data});
case at.PREF_CHANGED:
newValues = Object.assign({}, prevState.values);
newValues[action.data.name] = action.data.value;
return Object.assign({}, prevState, {values: newValues});
default:
return prevState;
}
}
this.INITIAL_STATE = INITIAL_STATE;
-this.reducers = {TopSites, App, Prefs};
+this.reducers = {TopSites, App, Prefs, Dialog};
this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -92,26 +92,25 @@ var BACKGROUND_PROCESS = 2;
* Use this in action creators if you need different logic
* for ui/background processes.
*/
const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
// 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"
-// The line below creates an object like this:
+// Create an object that avoids accidental differing key/value pairs:
// {
// INIT: "INIT",
// UNINIT: "UNINIT"
// }
-// It prevents accidentally adding a different key/value name.
-].reduce((obj, type) => {
- obj[type] = type;return obj;
-}, {});
+const actionTypes = {};
+for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "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"]) {
+ actionTypes[type] = type;
+}
// Helper function for creating routed actions between content and main
// Not intended to be used by consumers
function _RouteMessage(action, options) {
const meta = action.meta ? Object.assign({}, action.meta) : {};
if (!options || !options.from || !options.to) {
throw new Error("Routed Messages must have options as the second parameter, and must at least include a .from and .to property.");
}
@@ -307,19 +306,20 @@ var _require = __webpack_require__(2);
const connect = _require.connect;
var _require2 = __webpack_require__(3);
const addLocaleData = _require2.addLocaleData,
IntlProvider = _require2.IntlProvider;
-const TopSites = __webpack_require__(13);
-const Search = __webpack_require__(12);
-const PreferencesPane = __webpack_require__(11);
+const TopSites = __webpack_require__(14);
+const Search = __webpack_require__(13);
+const ConfirmDialog = __webpack_require__(9);
+const PreferencesPane = __webpack_require__(12);
// Locales that should be displayed RTL
const RTL_LIST = ["ar", "he", "fa", "ur"];
// Add the locale data for pluralization and relative-time formatting for now,
// this just uses english locale data. We can make this more sophisticated if
// more features are needed.
function addLocaleDataForReactIntl(_ref) {
@@ -368,17 +368,18 @@ class Base extends React.Component {
{ key: locale, locale: locale, messages: strings },
React.createElement(
"div",
{ className: "outer-wrapper" },
React.createElement(
"main",
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)
)
);
}
}
module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
@@ -389,17 +390,17 @@ module.exports = connect(state => ({ App
"use strict";
var _require = __webpack_require__(1);
const at = _require.actionTypes;
-var _require2 = __webpack_require__(15);
+var _require2 = __webpack_require__(16);
const perfSvc = _require2.perfService;
const VISIBLE = "visible";
const VISIBILITY_CHANGE_EVENT = "visibilitychange";
module.exports = class DetectUserSessionStart {
@@ -568,16 +569,20 @@ const INITIAL_STATE = {
// Have we received real data from history yet?
initialized: false,
// The history (and possibly default) links
rows: []
},
Prefs: {
initialized: false,
values: {}
+ },
+ Dialog: {
+ visible: false,
+ data: {}
}
};
function App() {
let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.App;
let action = arguments[1];
switch (action.type) {
@@ -652,16 +657,32 @@ function TopSites() {
case at.PLACES_LINK_BLOCKED:
newRows = prevState.rows.filter(val => val.url !== action.data.url);
return Object.assign({}, prevState, { rows: newRows });
default:
return prevState;
}
}
+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() {
let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Prefs;
let action = arguments[1];
let newValues;
switch (action.type) {
case at.PREFS_INITIAL_VALUES:
return Object.assign({}, prevState, { initialized: true, values: action.data });
@@ -669,17 +690,17 @@ function Prefs() {
newValues = Object.assign({}, prevState.values);
newValues[action.data.name] = action.data.value;
return Object.assign({}, prevState, { values: newValues });
default:
return prevState;
}
}
-var reducers = { TopSites, App, Prefs };
+var reducers = { TopSites, App, Prefs, Dialog };
module.exports = {
reducers,
INITIAL_STATE
};
/***/ }),
/* 8 */
/***/ (function(module, exports) {
@@ -690,16 +711,135 @@ module.exports = ReactDOM;
/* 9 */
/***/ (function(module, exports, __webpack_require__) {
"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);
+
class ContextMenu extends React.Component {
constructor(props) {
super(props);
this.hideContext = this.hideContext.bind(this);
}
hideContext() {
this.props.onUpdate(false);
}
@@ -765,152 +905,175 @@ class ContextMenu extends React.Componen
)
);
}
}
module.exports = ContextMenu;
/***/ }),
-/* 10 */
+/* 11 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
const React = __webpack_require__(0);
var _require = __webpack_require__(3);
const injectIntl = _require.injectIntl;
-const ContextMenu = __webpack_require__(9);
+const ContextMenu = __webpack_require__(10);
var _require2 = __webpack_require__(1);
-const actionTypes = _require2.actionTypes,
+const at = _require2.actionTypes,
ac = _require2.actionCreators;
+const RemoveBookmark = site => ({
+ id: "menu_action_remove_bookmark",
+ icon: "bookmark-remove",
+ action: ac.SendToMain({
+ type: at.DELETE_BOOKMARK_BY_ID,
+ data: site.bookmarkGuid
+ }),
+ userEvent: "BOOKMARK_DELETE"
+});
+
+const AddBookmark = site => ({
+ id: "menu_action_bookmark",
+ icon: "bookmark",
+ action: ac.SendToMain({
+ type: at.BOOKMARK_URL,
+ data: site.url
+ }),
+ userEvent: "BOOKMARK_ADD"
+});
+
+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"
+});
+
+const OpenInPrivateWindow = site => ({
+ id: "menu_action_open_private_window",
+ icon: "new-window-private",
+ action: ac.SendToMain({
+ type: at.OPEN_PRIVATE_WINDOW,
+ data: { url: site.url }
+ }),
+ userEvent: "OPEN_PRIVATE_WINDOW"
+});
+
+const BlockUrl = site => ({
+ id: "menu_action_dismiss",
+ icon: "dismiss",
+ 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"
+ }
+ },
+ userEvent: "DIALOG_OPEN"
+});
+
class LinkMenu extends React.Component {
- getBookmarkStatus(site) {
- return site.bookmarkGuid ? {
- id: "menu_action_remove_bookmark",
- icon: "bookmark-remove",
- action: "DELETE_BOOKMARK_BY_ID",
- data: site.bookmarkGuid,
- 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;
+ const props = this.props;
+ const site = props.site;
- // default top sites have a limited set of context menu options
+ const isBookmark = site.bookmarkGuid;
+ const isDefault = site.isDefault;
- let options = this.getDefaultContextMenu(site);
+ const options = [
- // all other top sites have all the following context menu options
- if (!site.isDefault) {
- options = [this.getBookmarkStatus(site), { type: "separator" }, ...options, { type: "separator" }, {
- id: "menu_action_dismiss",
- icon: "dismiss",
- action: "BLOCK_URL",
- data: site.url,
- userEvent: "BLOCK"
- }, {
- id: "menu_action_delete",
- icon: "delete",
- action: "DELETE_HISTORY_URL",
- data: site.url,
- userEvent: "DELETE"
- }];
- }
- options.forEach(option => {
+ // 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,
- data = option.data,
id = option.id,
type = option.type,
userEvent = option.userEvent;
- // Convert message ids to localized labels and add onClick function
if (!type && id) {
- option.label = this.props.intl.formatMessage(option);
+ option.label = props.intl.formatMessage(option);
option.onClick = () => {
- dispatch(ac.SendToMain({ type: actionTypes[action], data }));
- dispatch(ac.UserEvent({
- event: userEvent,
- source,
- action_position: index
- }));
+ props.dispatch(action);
+ if (userEvent) {
+ props.dispatch(ac.UserEvent({
+ event: userEvent,
+ 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
- // is the last, so we can close the context menu accordingly
+ // This is for accessibility to support making each item tabbable.
+ // 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[options.length - 1].last = true;
return options;
}
render() {
return React.createElement(ContextMenu, {
visible: this.props.visible,
onUpdate: this.props.onUpdate,
options: this.getOptions() });
}
}
module.exports = injectIntl(LinkMenu);
module.exports._unconnected = LinkMenu;
/***/ }),
-/* 11 */
+/* 12 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
const React = __webpack_require__(0);
var _require = __webpack_require__(2);
const connect = _require.connect;
var _require2 = __webpack_require__(3);
const injectIntl = _require2.injectIntl,
FormattedMessage = _require2.FormattedMessage;
-const classNames = __webpack_require__(16);
-
var _require3 = __webpack_require__(1);
const ac = _require3.actionCreators;
const PreferencesInput = props => React.createElement(
"section",
null,
@@ -962,26 +1125,26 @@ class PreferencesPane extends React.Comp
const isVisible = this.state.visible;
return React.createElement(
"div",
{ className: "prefs-pane-wrapper", ref: "wrapper" },
React.createElement(
"div",
{ className: "prefs-pane-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" }),
onClick: this.togglePane })
),
React.createElement(
"div",
{ className: "prefs-pane" },
React.createElement(
"div",
- { className: classNames("sidebar", { hidden: !isVisible }) },
+ { className: `sidebar ${isVisible ? "" : "hidden"}` },
React.createElement(
"div",
{ className: "prefs-modal-inner-wrapper" },
React.createElement(
"h1",
null,
React.createElement(FormattedMessage, { id: "settings_pane_header" })
),
@@ -1010,17 +1173,17 @@ class PreferencesPane extends React.Comp
}
}
module.exports = connect(state => ({ Prefs: state.Prefs }))(injectIntl(PreferencesPane));
module.exports.PreferencesPane = PreferencesPane;
module.exports.PreferencesInput = PreferencesInput;
/***/ }),
-/* 12 */
+/* 13 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/* globals ContentSearchUIController */
const React = __webpack_require__(0);
@@ -1109,34 +1272,34 @@ class Search extends React.Component {
);
}
}
module.exports = connect()(injectIntl(Search));
module.exports._unconnected = Search;
/***/ }),
-/* 13 */
+/* 14 */
/***/ (function(module, exports, __webpack_require__) {
"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;
-const shortURL = __webpack_require__(14);
-const LinkMenu = __webpack_require__(10);
+const shortURL = __webpack_require__(15);
+const LinkMenu = __webpack_require__(11);
var _require3 = __webpack_require__(1);
const ac = _require3.actionCreators;
const TOP_SITES_SOURCE = "TOP_SITES";
class TopSite extends React.Component {
@@ -1156,17 +1319,17 @@ class TopSite extends React.Component {
}
render() {
var _props = this.props;
const link = _props.link,
index = _props.index,
dispatch = _props.dispatch;
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 topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`;
const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
return React.createElement(
"li",
{ className: topSiteOuterClassName, key: link.url },
React.createElement(
"a",
@@ -1178,18 +1341,23 @@ class TopSite extends React.Component {
"span",
{ className: "letter-fallback" },
title[0]
),
React.createElement("div", { className: screenshotClassName, style: style })
),
React.createElement(
"div",
- { className: "title" },
- title
+ { className: `title ${link.isPinned ? "pinned" : ""}` },
+ link.isPinned && React.createElement("div", { className: "icon icon-pin-small" }),
+ React.createElement(
+ "span",
+ null,
+ title
+ )
)
),
React.createElement(
"button",
{ className: "context-menu-button",
onClick: e => {
e.preventDefault();
this.toggleContextMenu(e, index);
@@ -1230,17 +1398,17 @@ const TopSites = props => React.createEl
)
);
module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSites);
module.exports._unconnected = TopSites;
module.exports.TopSite = TopSite;
/***/ }),
-/* 14 */
+/* 15 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/**
* shortURL - Creates a short version of a link's url, used for display purposes
* e.g. {url: http://www.foosite.com, eTLD: "com"} => "foosite"
@@ -1265,17 +1433,17 @@ module.exports = function shortURL(link)
// Remove the eTLD (e.g., com, net) and the preceding period from the hostname
const eTLDLength = (eTLD || "").length || hostname.match(/\.com$/) && 3;
const eTLDExtra = eTLDLength > 0 ? -(eTLDLength + 1) : Infinity;
return hostname.slice(0, eTLDExtra).toLowerCase();
};
/***/ }),
-/* 15 */
+/* 16 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/* globals Services */
let usablePerfObj;
@@ -1370,71 +1538,16 @@ var _PerfService = function _PerfService
var perfService = new _PerfService();
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 */
/***/ (function(module, exports) {
module.exports = Redux;
/***/ }),
/* 18 */
/***/ (function(module, exports, __webpack_require__) {
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -39,16 +39,21 @@ input {
.icon.icon-dismiss {
background-image: url("assets/glyph-dismiss-16.svg"); }
.icon.icon-new-window {
background-image: url("assets/glyph-newWindow-16.svg"); }
.icon.icon-new-window-private {
background-image: url("assets/glyph-newWindow-private-16.svg"); }
.icon.icon-settings {
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,
body,
#root {
height: 100%; }
body {
background: #F6F6F8;
@@ -217,31 +222,40 @@ main {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: #FFF;
border-radius: 6px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
- background-size: 250%;
+ background-size: cover;
background-position: top left;
transition: opacity 1s;
opacity: 0; }
.top-sites-list .top-site-outer .screenshot.active {
opacity: 1; }
.top-sites-list .top-site-outer .title {
height: 30px;
line-height: 30px;
text-align: center;
- white-space: nowrap;
font-size: 11px;
- overflow: hidden;
- text-overflow: ellipsis;
- width: 96px; }
+ width: 96px;
+ position: relative; }
+ .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 {
cursor: default;
display: flex;
position: relative;
margin: 0 0 48px;
width: 100%;
height: 36px; }
@@ -417,23 +431,26 @@ main {
content: '';
height: 21px;
offset-inline-start: 0;
position: absolute;
top: 0;
width: 21px; }
.prefs-pane [type='checkbox']:not(: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: '';
height: 21px;
offset-inline-start: 0;
position: absolute;
top: 0;
- width: 21px; }
+ width: 21px;
+ -moz-context-properties: fill, stroke;
+ fill: #1691D2;
+ stroke: none; }
.prefs-pane [type='checkbox']:not(:checked) + label::after {
opacity: 0; }
.prefs-pane [type='checkbox']:checked + label::after {
opacity: 1; }
.prefs-pane [type='checkbox'] + label:hover::before {
border: 1px solid #1691D2; }
.prefs-pane [type='checkbox']:checked:focus + label::before,
.prefs-pane [type='checkbox']:not(:checked):focus + label::before {
@@ -449,8 +466,47 @@ main {
right: 15px;
top: 15px;
z-index: 12001; }
.prefs-pane-button button:hover {
background-color: #EBEBEB; }
.prefs-pane-button button:dir(rtl) {
left: 5px;
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; }
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-pin-12.svg
@@ -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>
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -51,29 +51,33 @@
},
"af": {},
"an": {},
"ar": {
"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": "سيستخدم فيرفكس هذا المكان لعرض أكثر العلامات، و المقالات، و الفيديوهات والصفحات التي زرتها مؤخرا، ليمكنك العودة إليها بسهولة.",
"welcome_label": "تعرّف على أهم الأخبار",
@@ -86,25 +90,42 @@
"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 –عضو في أسرة موزيلا– على الوصول إلى محتوى عالِ الجودة ربما لم يُكن ليتاح لك بدونه.",
"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_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": {},
"ast": {
"newtab_page_title": "Llingüeta nueva",
"default_label_loading": "Cargando…",
"header_top_sites": "Sitios destacaos",
"type_label_open": "Abrir",
"type_label_topic": "Tema",
@@ -528,20 +549,22 @@
"edit_topsites_edit_button": "Golygu'r wefan",
"edit_topsites_dismiss_button": "Dileu'r wefan"
},
"da": {
"newtab_page_title": "Nyt faneblad",
"default_label_loading": "Indlæser…",
"header_top_sites": "Mest besøgte websider",
"header_highlights": "Højdepunkter",
+ "header_stories": "Tophistorier",
"header_stories_from": "fra",
"type_label_visited": "Besøgt",
"type_label_bookmarked": "Bogmærket",
"type_label_synced": "Synkroniseret fra en anden enhed",
+ "type_label_recommended": "Populært",
"type_label_open": "Åben",
"type_label_topic": "Emne",
"menu_action_bookmark": "Bogmærk",
"menu_action_remove_bookmark": "Fjern bogmærke",
"menu_action_copy_address": "Kopier adresse",
"menu_action_email_link": "Send link…",
"menu_action_open_new_window": "Åbn i et nyt vindue",
"menu_action_open_private_window": "Åbn i et nyt privat vindue",
@@ -565,36 +588,41 @@
"settings_pane_body": "Vælg, hvad der vises, når du åbner et nyt faneblad.",
"settings_pane_search_header": "Søgning",
"settings_pane_search_body": "Søg på nettet fra Nyt faneblad.",
"settings_pane_topsites_header": "Mest besøgte websider",
"settings_pane_topsites_body": "Adgang til de websider, du besøger oftest.",
"settings_pane_topsites_options_showmore": "Vis to rækker",
"settings_pane_highlights_header": "Højdepunkter",
"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",
"edit_topsites_button_text": "Rediger",
"edit_topsites_button_label": "Tilpas afsnittet Mest besøgte websider",
"edit_topsites_showmore_button": "Vis flere",
"edit_topsites_showless_button": "Vis færre",
"edit_topsites_done_button": "Færdig",
"edit_topsites_pin_button": "Fastgør denne webside",
"edit_topsites_unpin_button": "Frigør denne webside",
"edit_topsites_edit_button": "Rediger denne webside",
"edit_topsites_dismiss_button": "Afvis denne webside",
"edit_topsites_add_button": "Tilføj",
+ "topsites_form_add_header": "Ny webside",
"topsites_form_edit_header": "Rediger mest besøgte webside",
"topsites_form_title_placeholder": "Indtast en titel",
"topsites_form_url_placeholder": "Indtast eller indsæt en URL",
"topsites_form_add_button": "Tilføj",
"topsites_form_save_button": "Gem",
"topsites_form_cancel_button": "Annuller",
"topsites_form_url_validation": "Gyldig URL påkrævet",
"pocket_read_more": "Populære emner:",
"pocket_read_even_more": "Se flere historier",
+ "pocket_feedback_header": "Det bedste fra nettet, udvalgt af mere end 25 millioner mennesker.",
+ "pocket_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"
},
"de": {
"newtab_page_title": "Neuer Tab",
"default_label_loading": "Wird geladen…",
"header_top_sites": "Meistbesuchte Seiten",
"header_highlights": "Wichtige Seiten",
"header_stories": "Meistgelesene Meldungen",
@@ -854,17 +882,16 @@
"edit_topsites_pin_button": "Pin this site",
"edit_topsites_edit_button": "Edit this site",
"edit_topsites_dismiss_button": "Dismiss this site"
},
"en-US": {
"newtab_page_title": "New Tab",
"default_label_loading": "Loading…",
"header_top_sites": "Top Sites",
- "header_highlights": "Highlights",
"header_stories": "Top Stories",
"header_visit_again": "Visit Again",
"header_bookmarks": "Recent Bookmarks",
"header_bookmarks_placeholder": "You don't have any bookmarks yet.",
"header_stories_from": "from",
"type_label_visited": "Visited",
"type_label_bookmarked": "Bookmarked",
"type_label_synced": "Synced from another device",
@@ -874,16 +901,18 @@
"menu_action_bookmark": "Bookmark",
"menu_action_remove_bookmark": "Remove Bookmark",
"menu_action_copy_address": "Copy Address",
"menu_action_email_link": "Email Link…",
"menu_action_open_new_window": "Open in a New Window",
"menu_action_open_private_window": "Open in a New Private Window",
"menu_action_dismiss": "Dismiss",
"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",
"search_for_something_with": "Search for {search_term} with:",
"search_button": "Search",
"search_header": "{search_engine_name} Search",
"search_web_placeholder": "Search the Web",
"search_settings": "Change Search Settings",
"welcome_title": "Welcome to new tab",
"welcome_body": "Firefox will use this space to show your most relevant bookmarks, articles, videos, and pages you’ve recently visited, so you can get back to them easily.",
@@ -895,18 +924,16 @@
"settings_pane_button_label": "Customize your New Tab page",
"settings_pane_header": "New Tab Preferences",
"settings_pane_body": "Choose what you see when you open a new tab.",
"settings_pane_search_header": "Search",
"settings_pane_search_body": "Search the Web from your new tab.",
"settings_pane_topsites_header": "Top Sites",
"settings_pane_topsites_body": "Access the websites you visit most.",
"settings_pane_topsites_options_showmore": "Show two rows",
- "settings_pane_highlights_header": "Highlights",
- "settings_pane_highlights_body": "Look back at your recent browsing history and newly created bookmarks.",
"settings_pane_bookmarks_header": "Recent Bookmarks",
"settings_pane_bookmarks_body": "Your newly created bookmarks in one handy location.",
"settings_pane_visit_again_header": "Visit Again",
"settings_pane_visit_again_body": "Firefox will show you parts of your browsing history that you might want to remember or get back to.",
"settings_pane_pocketstories_header": "Top Stories",
"settings_pane_pocketstories_body": "Pocket, a part of the Mozilla family, will help connect you to high-quality content that you may not have found otherwise.",
"settings_pane_done_button": "Done",
"edit_topsites_button_text": "Edit",
@@ -966,29 +993,33 @@
"time_label_day": "{number}t",
"settings_pane_button_label": "Personecigi la paĝon por novaj langetoj"
},
"es-AR": {
"newtab_page_title": "Nueva pestaña",
"default_label_loading": "Cargando…",
"header_top_sites": "Más visitados",
"header_highlights": "Destacados",
+ "header_stories": "Historias principales",
+ "header_stories_from": "de",
"type_label_visited": "Visitados",
"type_label_bookmarked": "Marcados",
"type_label_synced": "Sincronizados de otro dispositivo",
+ "type_label_recommended": "Tendencias",
"type_label_open": "Abrir",
"type_label_topic": "Tópico",
"menu_action_bookmark": "Marcador",
"menu_action_remove_bookmark": "Eliminar marcador",
"menu_action_copy_address": "Copiar dirección",
"menu_action_email_link": "Enlace por correo electrónico…",
"menu_action_open_new_window": "Abrir en nueva ventana",
"menu_action_open_private_window": "Abrir en nueva ventana privada",
"menu_action_dismiss": "Descartar",
"menu_action_delete": "Borrar del historial",
+ "menu_action_save_to_pocket": "Guardar en Pocket",
"search_for_something_with": "Buscar {search_term} con:",
"search_button": "Buscar",
"search_header": "Buscar con {search_engine_name}",
"search_web_placeholder": "Buscar en la web",
"search_settings": "Cambiar opciones de búsqueda",
"welcome_title": "Bienvenido a una nueva pestaña",
"welcome_body": "Firefox usará este espacio para mostrar sus marcadores, artículos, videos y páginas más relevantes que se hayan visitado para poder volver más fácilmente.",
"welcome_label": "Identificar los destacados",
@@ -1001,25 +1032,42 @@
"settings_pane_body": "Elegir que se verá al abrir una nueva pestaña.",
"settings_pane_search_header": "Buscar",
"settings_pane_search_body": "Buscar en la Web desde nueva pestaña.",
"settings_pane_topsites_header": "Más visitados",
"settings_pane_topsites_body": "Acceder a los sitios web más visitados.",
"settings_pane_topsites_options_showmore": "Mostrar dos filas",
"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_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",
"edit_topsites_button_text": "Editar",
"edit_topsites_button_label": "Personalizar la sección de sitios más visitados",
"edit_topsites_showmore_button": "Mostrar más",
"edit_topsites_showless_button": "Mostrar menos",
"edit_topsites_done_button": "Listo",
"edit_topsites_pin_button": "Pegar este sitio",
+ "edit_topsites_unpin_button": "Despegar 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": {
"newtab_page_title": "Nueva pestaña",
"default_label_loading": "Cargando…",
"header_top_sites": "Sitios frecuentes",
"header_highlights": "Destacados",
"type_label_visited": "Visitado",
"type_label_bookmarked": "Marcado",
@@ -1137,29 +1185,33 @@
"pocket_feedback_body": "Pocket, que forma parte de la familia de Mozilla, te ayudará a encontrar contenido de alta calidad que puede que no encuentres de otra forma.",
"pocket_send_feedback": "Enviar comentario"
},
"es-MX": {
"newtab_page_title": "Nueva pestaña",
"default_label_loading": "Cargando…",
"header_top_sites": "Sitios favoritos",
"header_highlights": "Destacados",
+ "header_stories": "Historias populares",
+ "header_stories_from": "de",
"type_label_visited": "Visitados",
"type_label_bookmarked": "Marcados",
"type_label_synced": "Sincronizado desde otro dispositivo",
+ "type_label_recommended": "Tendencias",
"type_label_open": "Abrir",
"type_label_topic": "Tema",
"menu_action_bookmark": "Marcador",
"menu_action_remove_bookmark": "Eliminar marcador",
"menu_action_copy_address": "Copiar dirección",
"menu_action_email_link": "Enlace por correo electrónico…",
"menu_action_open_new_window": "Abrir en una Nueva Ventana",
"menu_action_open_private_window": "Abrir en una Nueva Ventana Privada",
"menu_action_dismiss": "Descartar",
"menu_action_delete": "Eliminar del historial",
+ "menu_action_save_to_pocket": "Guardar en Pocket",
"search_for_something_with": "Buscar {search_term} con:",
"search_button": "Buscar",
"search_header": "Buscar {search_engine_name}",
"search_web_placeholder": "Buscar en la Web",
"search_settings": "Cambiar configuraciones de búsqueda",
"welcome_title": "Bienvenido a una nueva pestaña",
"welcome_body": "Firefox usará este espacio para mostrar tus marcadores, artículos, videos y páginas más relevantes que se hayan visitado para poder volver más fácilmente.",
"welcome_label": "Identificando tus destacados",
@@ -1172,44 +1224,65 @@
"settings_pane_body": "Elige lo que ves al abrir una nueva pestaña.",
"settings_pane_search_header": "Buscar",
"settings_pane_search_body": "Busca en la web de tu nueva pestaña.",
"settings_pane_topsites_header": "Sitios populares",
"settings_pane_topsites_body": "Accede a los sitios web que más visitas.",
"settings_pane_topsites_options_showmore": "Mostrar dos filas",
"settings_pane_highlights_header": "Destacados",
"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",
"edit_topsites_button_text": "Editar",
"edit_topsites_button_label": "Personalizar la sección de tus sitios preferidos",
"edit_topsites_showmore_button": "Mostrar más",
"edit_topsites_showless_button": "Mostrar menos",
"edit_topsites_done_button": "Listo",
"edit_topsites_pin_button": "Fijar este sitio",
+ "edit_topsites_unpin_button": "Despegar 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": {
"newtab_page_title": "Uus kaart",
"default_label_loading": "Laadimine…",
"header_top_sites": "Top saidid",
"header_highlights": "Esiletõstetud",
+ "header_stories": "Top lood",
+ "header_stories_from": "allikast",
"type_label_visited": "Külastatud",
"type_label_bookmarked": "Järjehoidjatest",
"type_label_synced": "Sünkroniseeritud teisest seadmest",
+ "type_label_recommended": "Menukad",
"type_label_open": "Avatud",
"type_label_topic": "Teema",
"menu_action_bookmark": "Lisa järjehoidjatesse",
"menu_action_remove_bookmark": "Eemalda järjehoidja",
"menu_action_copy_address": "Kopeeri aadress",
"menu_action_email_link": "Saada link e-postiga…",
"menu_action_open_new_window": "Ava uues aknas",
"menu_action_open_private_window": "Ava uues privaatses aknas",
"menu_action_dismiss": "Peida",
"menu_action_delete": "Kustuta ajaloost",
+ "menu_action_save_to_pocket": "Salvesta Pocketisse",
"search_for_something_with": "Otsi fraasi {search_term}, kasutades otsingumootorit:",
"search_button": "Otsi",
"search_header": "{search_engine_name}",
"search_web_placeholder": "Otsi veebist",
"search_settings": "Muuda otsingu sätteid",
"welcome_title": "Tere tulemast uuele kaardile",
"welcome_body": "Firefox kasutab seda lehte, et kuvada sulle kõige olulisemaid järjehoidjaid, artikleid, videoid ja lehti, mida oled hiljuti külastanud, nii et pääseksid kergelt nende juurde tagasi.",
"welcome_label": "Esiletõstetava sisu tuvastamine",
@@ -1222,16 +1295,18 @@
"settings_pane_body": "Vali asjad, mida soovid uue kaardi avamisel näha.",
"settings_pane_search_header": "Otsi",
"settings_pane_search_body": "Veebis otsimine uuel kaardil.",
"settings_pane_topsites_header": "Top saidid",
"settings_pane_topsites_body": "Ligipääs enim külastatud veebilehtedele.",
"settings_pane_topsites_options_showmore": "Kuvatakse kahel real",
"settings_pane_highlights_header": "Esiletõstetud",
"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",
"edit_topsites_button_text": "Muuda",
"edit_topsites_button_label": "Kohanda top saitide osa",
"edit_topsites_showmore_button": "Kuva rohkem",
"edit_topsites_showless_button": "Näita vähem",
"edit_topsites_done_button": "Valmis",
"edit_topsites_pin_button": "Kinnita see sait",
"edit_topsites_unpin_button": "Eemalda see sait",
@@ -1240,17 +1315,21 @@
"edit_topsites_add_button": "Lisa",
"topsites_form_add_header": "Uue top saidi lisamine",
"topsites_form_edit_header": "Top saidi muutmine",
"topsites_form_title_placeholder": "Sisesta pealkiri",
"topsites_form_url_placeholder": "Sisesta või aseta URL",
"topsites_form_add_button": "Lisa",
"topsites_form_save_button": "Salvesta",
"topsites_form_cancel_button": "Tühista",
+ "topsites_form_url_validation": "URL peab olema korrektne",
"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"
},
"eu": {},
"fa": {
"newtab_page_title": "زبانه جدید",
"default_label_loading": "در حال بارگیری…",
"header_top_sites": "سایتهای برتر",
"header_highlights": "برجستهها",
@@ -1504,40 +1583,81 @@
"pocket_feedback_body": "Pocket, part fan de Mozilla-famylje, sil jo helpe te ferbinen mei hege kwaliteit ynhâld dy't jo oars miskien net fûn hienen.",
"pocket_send_feedback": "Kommentaar ferstjoere"
},
"ga-IE": {
"newtab_page_title": "Cluaisín Nua",
"default_label_loading": "Á Luchtú…",
"header_top_sites": "Barrshuímh",
"header_highlights": "Buaicphointí",
+ "header_stories": "Barrscéalta",
+ "header_stories_from": "ó",
"type_label_visited": "Feicthe",
"type_label_bookmarked": "Leabharmharcáilte",
"type_label_synced": "Sioncronaithe ó ghléas eile",
+ "type_label_recommended": "Treochtáil",
"type_label_open": "Oscailte",
"type_label_topic": "Ábhar",
"menu_action_bookmark": "Cruthaigh leabharmharc",
"menu_action_remove_bookmark": "Scrios Leabharmharc",
"menu_action_copy_address": "Cóipeáil an Seoladh",
"menu_action_email_link": "Seol an Nasc trí Ríomhphost…",
"menu_action_open_new_window": "Oscail i bhFuinneog Nua",
"menu_action_open_private_window": "Oscail i bhFuinneog Nua Phríobháideach",
"menu_action_dismiss": "Ruaig",
"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_button": "Cuardach",
"search_header": "Cuardach {search_engine_name}",
"search_web_placeholder": "Cuardaigh an Gréasán",
"search_settings": "Socruithe Cuardaigh",
"welcome_title": "Fáilte go dtí cluaisín nua",
"welcome_body": "Úsáidfidh Firefox an spás seo chun na leabharmharcanna, ailt, físeáin, agus leathanaigh is tábhachtaí a thaispeáint duit, ionas go mbeidh tú in ann filleadh orthu gan stró.",
"welcome_label": "Buaicphointí á lorg",
"time_label_less_than_minute": "< 1 n",
"time_label_minute": "{number}n",
"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": {
"newtab_page_title": "Taba ùr",
"default_label_loading": "’Ga luchdadh…",
"header_top_sites": "Brod nan làrach",
"header_highlights": "Highlights",
"type_label_visited": "Na thadhail thu air",
"type_label_bookmarked": "’Nan comharran-lìn",
@@ -1918,29 +2038,33 @@
"time_label_hour": "{number} ժ",
"time_label_day": "{number} օր"
},
"id": {
"newtab_page_title": "Tab Baru",
"default_label_loading": "Memuat…",
"header_top_sites": "Situs Teratas",
"header_highlights": "Sorotan",
+ "header_stories": "Cerita Utama",
+ "header_stories_from": "dari",
"type_label_visited": "Dikunjungi",
"type_label_bookmarked": "Dimarkahi",
"type_label_synced": "Disinkronkan dari perangkat lain",
+ "type_label_recommended": "Trending",
"type_label_open": "Buka",
"type_label_topic": "Topik",
"menu_action_bookmark": "Markah",
"menu_action_remove_bookmark": "Hapus Markah",
"menu_action_copy_address": "Salin Alamat",
"menu_action_email_link": "Emailkan Tautan…",
"menu_action_open_new_window": "Buka di Jendela Baru",
"menu_action_open_private_window": "Buka di Jendela Penjelajahan Pribadi Baru",
"menu_action_dismiss": "Tutup",
"menu_action_delete": "Hapus dari Riwayat",
+ "menu_action_save_to_pocket": "Simpan ke Pocket",
"search_for_something_with": "Cari {search_term} lewat:",
"search_button": "Cari",
"search_header": "Pencarian {search_engine_name}",
"search_web_placeholder": "Cari di Web",
"search_settings": "Ubah Pengaturan Pencarian",
"welcome_title": "Selamat datang di tab baru",
"welcome_body": "Firefox akan menggunakan ruang ini untuk menampilkan markah, artikel, video, dan laman yang baru-baru ini dikunjungi, yang paling relevan agar Anda bisa kembali mengunjunginya dengan mudah.",
"welcome_label": "Mengidentifikasi Sorotan Anda",
@@ -1953,25 +2077,42 @@
"settings_pane_body": "Pilih apa yang Anda lihat ketika Anda membuka tab baru.",
"settings_pane_search_header": "Pencarian",
"settings_pane_search_body": "Cari Web dari tab baru Anda.",
"settings_pane_topsites_header": "Situs Teratas",
"settings_pane_topsites_body": "Mengakses situs web yang paling sering Anda kunjungi.",
"settings_pane_topsites_options_showmore": "Tampilkan dua baris",
"settings_pane_highlights_header": "Sorotan",
"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",
"edit_topsites_button_text": "Sunting",
"edit_topsites_button_label": "Ubahsuai bagian Situs Teratas Anda",
"edit_topsites_showmore_button": "Tampilkan lainnya",
"edit_topsites_showless_button": "Tampilkan lebih sedikit",
"edit_topsites_done_button": "Selesai",
"edit_topsites_pin_button": "Sematkan situs ini",
+ "edit_topsites_unpin_button": "Lepaskan 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": {},
"it": {
"newtab_page_title": "Nuova scheda",
"default_label_loading": "Caricamento…",
"header_top_sites": "Siti principali",
"header_highlights": "In evidenza",
"header_stories": "Storie principali",
@@ -2106,17 +2247,87 @@
"topsites_form_cancel_button": "キャンセル",
"topsites_form_url_validation": "正しい URL を入力してください",
"pocket_read_more": "人気のトピック:",
"pocket_read_even_more": "他の記事を見る",
"pocket_feedback_header": "2,500 万人以上の人々によって収集されている、ウェブ上で最も優れたコンテンツ。",
"pocket_feedback_body": "Mozilla ファミリーの一員となった Pocket は、他では見つからなかったかもしれない高品質なコンテンツとあなたを結び付ける手助けをします。",
"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": {
"newtab_page_title": "Iccer amaynut",
"default_label_loading": "Asali…",
"header_top_sites": "Ismal ifazen",
"header_highlights": "Iferdisen tisura",
"header_stories": "Tiqsiɣin ifazen",
"header_stories_from": "seg",
"type_label_visited": "Yettwarza",
@@ -2171,16 +2382,17 @@
"topsites_form_title_placeholder": "Sekcem azwel",
"topsites_form_url_placeholder": "Aru neɣ sekcem tansa URL",
"topsites_form_add_button": "Rnu",
"topsites_form_save_button": "Sekles",
"topsites_form_cancel_button": "Sefsex",
"topsites_form_url_validation": "Tansa URL tameɣtut tettwasra",
"pocket_read_more": "Isental ittwasnen aṭas:",
"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"
},
"kk": {
"newtab_page_title": "Жаңа бет",
"default_label_loading": "Жүктелуде…",
"header_top_sites": "Топ сайттар",
"header_highlights": "Бастысы",
"header_stories": "Топ хикаялар",
@@ -2280,29 +2492,33 @@
"time_label_day": "{number} ថ្ងៃ"
},
"kn": {},
"ko": {
"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": "하이라이트 확인",
@@ -2315,16 +2531,17 @@
"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_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_edit_button": "이 사이트 수정",
@@ -2431,29 +2648,33 @@
"edit_topsites_edit_button": "ແກ້ໄຂເວັບໄຊທ໌ນີ້",
"edit_topsites_dismiss_button": "ຍົກເລີກເວັບໄຊທ໌ນີ້"
},
"lt": {
"newtab_page_title": "Nauja kortelė",
"default_label_loading": "Įkeliama…",
"header_top_sites": "Lankomiausios svetainės",
"header_highlights": "Akcentai",
+ "header_stories": "Populiariausi straipsniai",
+ "header_stories_from": "iš",
"type_label_visited": "Aplankyti",
"type_label_bookmarked": "Adresyne",
"type_label_synced": "Sinchronizuoti iš kito įrenginio",
+ "type_label_recommended": "Populiaru",
"type_label_open": "Atviri",
"type_label_topic": "Tema",
"menu_action_bookmark": "Įrašyti į adresyną",
"menu_action_remove_bookmark": "Pašalinti iš adresyno",
"menu_action_copy_address": "Kopijuoti adresą",
"menu_action_email_link": "Siųsti saitą el. paštu…",
"menu_action_open_new_window": "Atverti naujame lange",
"menu_action_open_private_window": "Atverti naujame privačiajame lange",
"menu_action_dismiss": "Paslėpti",
"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_button": "Ieškoti",
"search_header": "{search_engine_name} paieška",
"search_web_placeholder": "Ieškokite saityne",
"search_settings": "Keisti paieškos nuostatas",
"welcome_title": "Sveiki, čia nauja kortelė",
"welcome_body": "„Firefox“ naudos šią vietą jums aktualiausių adresyno įrašų, straipsnių, vaizdo įrašų bei neseniai lankytų tinklalapių rodymui, kad galėtumėte lengvai į juos sugrįžti.",
"welcome_label": "Nustatomi jūsų akcentai",
@@ -2466,35 +2687,158 @@
"settings_pane_body": "Pasirinkite, ką matysite atvėrę naują kortelę.",
"settings_pane_search_header": "Paieška",
"settings_pane_search_body": "Ieškokite saityne tiesiai iš naujos kortelės.",
"settings_pane_topsites_header": "Lankomiausios svetainės",
"settings_pane_topsites_body": "Pasiekite jūsų dažniausiai lankomas svetaines.",
"settings_pane_topsites_options_showmore": "Rodyti dvi eilutes",
"settings_pane_highlights_header": "Akcentai",
"settings_pane_highlights_body": "Pažvelkite į savo naujausią naršymo istoriją bei paskiausiai pridėtus adresyno įrašus.",
+ "settings_pane_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",
"edit_topsites_button_text": "Keisti",
"edit_topsites_button_label": "Tinkinkite savo lankomiausių svetainių skiltį",
"edit_topsites_showmore_button": "Rodyti daugiau",
"edit_topsites_showless_button": "Rodyti mažiau",
"edit_topsites_done_button": "Atlikta",
"edit_topsites_pin_button": "Įsegti šią svetainę",
+ "edit_topsites_unpin_button": "Išsegti š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": {},
"lv": {
"newtab_page_title": "Jauna cilne"
},
"mai": {},
"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": {},
- "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": {
"newtab_page_title": "Tab Baru",
"default_label_loading": "Memuatkan…",
"header_top_sites": "Laman Teratas",
"header_highlights": "Serlahan",
"header_stories": "Berita Hangat",
"header_stories_from": "dari",
"type_label_visited": "Dilawati",
@@ -3016,22 +3360,22 @@
"settings_pane_search_body": "Pesquise na Web a partir da sua nova aba.",
"settings_pane_topsites_header": "Sites preferidos",
"settings_pane_topsites_body": "Acesse os sites que você mais visita.",
"settings_pane_topsites_options_showmore": "Mostrar duas linhas",
"settings_pane_highlights_header": "Destaques",
"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_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_label": "Personalizar a sua seção de sites preferidos",
"edit_topsites_showmore_button": "Mostrar mais",
"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_unpin_button": "Desafixar este site",
"edit_topsites_edit_button": "Editar este site",
"edit_topsites_dismiss_button": "Descartar este site",
"edit_topsites_add_button": "Adicionar",
"topsites_form_add_header": "Novo site popular",
"topsites_form_edit_header": "Editar site popular",
"topsites_form_title_placeholder": "Digite um título",
@@ -3167,16 +3511,17 @@
"edit_topsites_edit_button": "Modifitgar questa pagina",
"edit_topsites_dismiss_button": "Allontanar questa pagina"
},
"ro": {
"newtab_page_title": "Filă nouă",
"default_label_loading": "Se încarcă…",
"header_top_sites": "Site-uri de top",
"header_highlights": "Evidențieri",
+ "header_stories_from": "de la",
"type_label_visited": "Vizitate",
"type_label_bookmarked": "Însemnat",
"type_label_synced": "Sincronizat de pe alt dispozitiv",
"type_label_open": "Deschise",
"type_label_topic": "Subiect",
"menu_action_bookmark": "Însemnează",
"menu_action_remove_bookmark": "Elimină semnul de carte",
"menu_action_copy_address": "Copiază adresa",
@@ -3193,34 +3538,45 @@
"welcome_title": "Bun venit în noua filă",
"welcome_body": "Firefox va folosi acest spațiu pentru a arăta cele mai relevante semne de carte, articole, videouri și pagini vizitate recent pentru a reveni la acestea ușor.",
"welcome_label": "Se identifică evidențierile tale",
"time_label_less_than_minute": "<1m",
"time_label_minute": "{number}m",
"time_label_hour": "{number}h",
"time_label_day": "{number}d",
"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_search_header": "Caută",
"settings_pane_search_body": "Caută pe web din noua filă.",
"settings_pane_topsites_header": "Site-uri de top",
"settings_pane_topsites_body": "Accesează site-urile pe care le vizitezi mai des.",
"settings_pane_topsites_options_showmore": "Arată două rânduri",
"settings_pane_highlights_header": "Evidențieri",
"settings_pane_highlights_body": "Privește înapoi la istoricul de navigare recent și noile semne de carte create.",
"settings_pane_done_button": "Gata",
"edit_topsites_button_text": "Editează",
"edit_topsites_button_label": "Particularizează secțiunea site-urilor de top",
"edit_topsites_showmore_button": "Arată mai mult",
"edit_topsites_showless_button": "Arată mai puțin",
"edit_topsites_done_button": "Gata",
"edit_topsites_pin_button": "Fixează 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": {
"newtab_page_title": "Новая вкладка",
"default_label_loading": "Загрузка…",
"header_top_sites": "Топ сайтов",
"header_highlights": "Избранные",
"header_stories": "Топ статей",
"header_stories_from": "от",
@@ -3625,30 +3981,33 @@
"pocket_send_feedback": "Skicka återkoppling"
},
"ta": {
"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": "வரலாற்றிலருந்து அழி",
- "search_for_something_with": "{search_term} என்பதற்காகத் தேடு:",
+ "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நி",
@@ -3661,36 +4020,40 @@
"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, ஒரு மொசில்லா குடும்ப உறுப்பினராக, உயர்தர உள்ளடக்கங்களுடன் இணைய உதவுகிறது, இது இல்லையேல் அது சாத்தியமாகது.",
"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, ஒரு மொசில்லா குடும்ப உறுப்பினராக, உயர்தர உள்ளடக்கங்களுடன் இணைய உதவுகிறது, இது இல்லையேல் அது சாத்தியமாகது.",
"pocket_send_feedback": "கருத்துகளைத் தெறிவிக்கவும்"
},
"ta-LK": {},
"te": {
"newtab_page_title": "కొత్త ట్యాబు",
"default_label_loading": "వస్తోంది…",
"header_top_sites": "మేటి సైట్లు",
"header_highlights": "ముఖ్యాంశాలు",
@@ -3739,20 +4102,22 @@
"edit_topsites_edit_button": "ఈ సైటును మార్చు",
"edit_topsites_dismiss_button": "ఈ సైటుని తీసివేయి"
},
"th": {
"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": "เปิดในหน้าต่างส่วนตัวใหม่",
@@ -3776,32 +4141,39 @@
"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_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": "ยกเลิก",
- "pocket_read_even_more": "ดูเรื่องราวเพิ่มเติม"
+ "topsites_form_url_validation": "ต้องการ URL ที่ถูกต้อง",
+ "pocket_read_more": "หัวข้อยอดนิยม:",
+ "pocket_read_even_more": "ดูเรื่องราวเพิ่มเติม",
+ "pocket_send_feedback": "ส่งข้อคิดเห็น"
},
"tl": {
"newtab_page_title": "Bagong Tab",
"default_label_loading": "Pagkarga…",
"header_top_sites": "Tuktok na mga Site",
"header_highlights": "Highlights",
"type_label_visited": "Binisita",
"type_label_bookmarked": "Bookmarked",
@@ -3990,53 +4362,77 @@
"pocket_feedback_body": "Pocket, частина сім'ї Mozilla, допоможе підключити вас до якісного вмісту, що ви можете інакше й не знайти.",
"pocket_send_feedback": "Надіслати відгук"
},
"ur": {
"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": "نئے ٹیب میں خوش آمدید",
"welcome_body": "اس جگہ کا استعمال کرنے ہوئے Firefox آپکی متعلقہ نشانیاں، عبارات، وڈیوز اور صفحات جن کا حال ہی میں ص آُپ نے دورہ کیا ہے دکھائے گا۔ تاکہ آپ ان تک واپس آسانی سے پہنچ سکیں۔",
"welcome_label": "آپکی جھلکیوں کی نشاندہی کر رہا ہے",
"time_label_less_than_minute": "<1m",
"time_label_minute": "{number}m",
"time_label_hour": "{number}h",
"time_label_day": "{number}d",
"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_done_button": "ہوگیا",
"edit_topsites_button_text": "تدوین",
+ "edit_topsites_button_label": "اپنی بہترین سائٹس والے حصے کی تخصیص کریں",
+ "edit_topsites_showmore_button": "مزید دکھائیں",
"edit_topsites_done_button": "ہوگیا",
+ "edit_topsites_pin_button": "اس سائَٹ کو پن کریں",
+ "edit_topsites_unpin_button": "اس سائٹ کو انپن کریں",
"edit_topsites_edit_button": "اس سائٹ کی تدوین کریں",
- "edit_topsites_dismiss_button": "اس سائٹ کو برخاست کریں"
+ "edit_topsites_dismiss_button": "اس سائٹ کو برخاست کریں",
+ "edit_topsites_add_button": "آظافہ کریں",
+ "topsites_form_add_header": "نئی بہترین سائٹ",
+ "topsites_form_edit_header": "بہترین سائٹٹ کیی تدوین کریں",
+ "topsites_form_title_placeholder": "ایک عنوان داخل کریں",
+ "topsites_form_url_placeholder": "ٹائپ کریں یا ایک 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": {},
"vi": {},
"wo": {},
"xh": {},
"zh-CN": {
"newtab_page_title": "新标签页",
"default_label_loading": "正在载入…",
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -1,143 +1,127 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {utils: Cu} = Components;
-Cu.import("resource://gre/modules/XPCOMUtils.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 {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 {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.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;
-XPCOMUtils.defineLazyModuleGetter(this, "DefaultPrefs",
- "resource://activity-stream/lib/ActivityStreamPrefs.jsm");
-
-// Feeds
-XPCOMUtils.defineLazyModuleGetter(this, "LocalizationFeed",
- "resource://activity-stream/lib/LocalizationFeed.jsm");
-
-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",
+const PREFS_CONFIG = new Map([
+ ["default.sites", {
+ title: "Comma-separated list of default top sites to fill in behind visited sites",
+ value: "https://www.facebook.com/,https://www.youtube.com/,https://www.amazon.com/,https://www.yahoo.com/,https://www.ebay.com/,https://twitter.com/"
+ }],
+ ["showSearch", {
+ title: "Show the Search bar on the New Tab page",
+ 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,
- 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()
- },
+ 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: "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",
+ name: "localization",
+ factory: () => new LocalizationFeed(),
+ title: "Initialize strings and detect locale for Activity Stream",
value: true
},
{
- name: "showTopSites",
- title: "Show the Top Sites section on the New Tab page",
+ name: "newtabinit",
+ 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
},
{
name: "telemetry",
- title: "Enable system error and usage data collection",
- value: false
- },
- {
- name: "telemetry.log",
- title: "Log telemetry events in the console",
- value: false
+ factory: () => new TelemetryFeed(),
+ title: "Relays telemetry-related actions to TelemetrySender",
+ value: true
},
{
- name: "telemetry.ping.endpoint",
- title: "Telemetry server endpoint",
- value: "https://tiles.services.mozilla.com/v3/links/activity-stream"
+ name: "topsites",
+ factory: () => new TopSitesFeed(),
+ title: "Queries places and gets metadata for Top Sites section",
+ value: true
}
-];
-
-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 {
/**
* constructor - Initializes an instance of ActivityStream
*
* @param {object} options Options for the ActivityStream instance
* @param {string} options.id Add-on ID. e.g. "activity-stream@mozilla.org".
* @param {string} options.version Version of the add-on. e.g. "0.1.0"
* @param {string} options.newTabURL URL of New Tab page on which A.S. is displayed. e.g. "about:newtab"
*/
constructor(options = {}) {
this.initialized = false;
this.options = options;
this.store = new Store();
- this.feeds = feeds;
+ this.feeds = FEEDS_CONFIG;
this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
}
init() {
- this.initialized = true;
this._defaultPrefs.init();
this.store.init(this.feeds);
this.store.dispatch({
type: at.INIT,
data: {version: this.options.version}
});
+ this.initialized = true;
}
uninit() {
this.store.dispatch({type: at.UNINIT});
this.store.uninit();
this.initialized = false;
}
uninstall(reason) {
--- a/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -1,29 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/AboutNewTab.jsm");
+Cu.import("resource://gre/modules/RemotePageManager.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 {actionCreators: ac, actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const ABOUT_NEW_TAB_URL = "about:newtab";
const DEFAULT_OPTIONS = {
dispatch(action) {
throw new Error(`\nMessageChannel: Received action ${action.type}, but no dispatcher was defined.\n`);
},
pageURL: ABOUT_NEW_TAB_URL,
@@ -103,21 +93,21 @@ this.ActivityStreamMessageChannel = clas
/**
* send - Sends an action to a specific port
*
* @param {obj} action A redux action; it should contain a portID in the meta.toTarget property
*/
send(action) {
const targetId = action.meta && action.meta.toTarget;
const target = this.getTargetById(targetId);
- if (!target) {
- // The target is no longer around - maybe the user closed the page
- return;
+ try {
+ target.sendAsyncMessage(this.outgoingMessageName, action);
+ } catch (e) {
+ // The target page is closed/closing by the user or test, so just ignore.
}
- target.sendAsyncMessage(this.outgoingMessageName, action);
}
/**
* getIdByTarget - Retrieve the id of a message target, if it exists in this.targets
*
* @param {obj} targetObj A message target
* @return {string|null} The unique id of the target, if it exists.
*/
--- a/browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
@@ -13,29 +13,42 @@ this.Prefs = class Prefs extends Prefere
/**
* Prefs - A wrapper around Preferences that always sets the branch to
* ACTIVITY_STREAM_PREF_BRANCH
*/
constructor(branch = ACTIVITY_STREAM_PREF_BRANCH) {
super({branch});
this._branchName = branch;
+ this._branchObservers = new Map();
}
get 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 {
/**
* 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:
- * {string} .name The name of the pref
+ * @param {Map} config A Map with {string} key of the pref name and {object}
+ * value with the following pref properties:
* {string} .title (optional) A description of 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)
*/
constructor(config, branch = ACTIVITY_STREAM_PREF_BRANCH) {
this._config = config;
this.branch = Services.prefs.getDefaultBranch(branch);
}
@@ -59,24 +72,35 @@ this.DefaultPrefs = class DefaultPrefs {
break;
}
}
/**
* init - Set default prefs for all prefs in the config
*/
init() {
- for (const pref of this._config) {
- this._setDefaultPref(pref.name, pref.value);
+ // If Firefox is a locally built version or a testing build on try, etc.
+ // 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);
}
}
/**
* reset - Resets all user-defined prefs for prefs in ._config to their defaults
*/
reset() {
- for (const pref of this._config) {
- this.branch.clearUserPref(pref.name);
+ for (const name of this._config.keys()) {
+ this.branch.clearUserPref(name);
}
}
};
this.EXPORTED_SYMBOLS = ["DefaultPrefs", "Prefs"];
--- a/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
+++ b/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
@@ -1,21 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {utils: Cu} = Components;
-const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
-
+Cu.import("resource://gre/modules/Services.jsm");
Cu.importGlobalProperties(["fetch"]);
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
- "resource://gre/modules/Services.jsm");
+const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
// What is our default locale for the app?
const DEFAULT_LOCALE = "en-US";
// Event from LocaleService when locales are assigned
const LOCALES_CHANGE_TOPIC = "intl:requested-locales-changed";
// Where is the packaged locales json with all strings?
const LOCALES_FILE = "resource://activity-stream/data/locales.json";
--- a/browser/extensions/activity-stream/lib/NewTabInit.jsm
+++ b/browser/extensions/activity-stream/lib/NewTabInit.jsm
@@ -1,15 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {utils: Cu} = Components;
-const {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
* newly opened tabs.
*/
this.NewTabInit = class NewTabInit {
onAction(action) {
let newAction;
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -1,24 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const {utils: Cu, interfaces: Ci} = Components;
-const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+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",
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
- "resource://gre/modules/Services.jsm");
const LINK_BLOCKED_EVENT = "newtab-linkBlocked";
/**
* Observer - a wrapper around history/bookmark observers to add the QueryInterface.
*/
class Observer {
constructor(dispatch, observerInterface) {
@@ -146,18 +145,24 @@ class BookmarksObserver extends Observer
class PlacesFeed {
constructor() {
this.historyObserver = new HistoryObserver(action => this.store.dispatch(ac.BroadcastToContent(action)));
this.bookmarksObserver = new BookmarksObserver(action => this.store.dispatch(ac.BroadcastToContent(action)));
}
addObservers() {
- PlacesUtils.history.addObserver(this.historyObserver, true);
- PlacesUtils.bookmarks.addObserver(this.bookmarksObserver, true);
+ // NB: Directly get services without importing the *BIG* PlacesUtils module
+ 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);
}
removeObservers() {
PlacesUtils.history.removeObserver(this.historyObserver);
PlacesUtils.bookmarks.removeObserver(this.bookmarksObserver);
Services.obs.removeObserver(this, LINK_BLOCKED_EVENT);
}
--- a/browser/extensions/activity-stream/lib/PrefsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PrefsFeed.jsm
@@ -1,50 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {utils: Cu} = Components;
-const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Prefs",
- "resource://activity-stream/lib/ActivityStreamPrefs.jsm");
+const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
this.PrefsFeed = class PrefsFeed {
- constructor(prefNames) {
- this._prefNames = prefNames;
+ constructor(prefMap) {
+ this._prefMap = prefMap;
this._prefs = new Prefs();
- this._observers = new Map();
}
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() {
- const values = {};
+ this._prefs.observeBranch(this);
- // Set up listeners for each activity stream pref
- for (const name of this._prefNames) {
- const handler = value => {
- this.onPrefChanged(name, value);
- };
- this._observers.set(name, handler, this);
- this._prefs.observe(name, handler);
+ // Get the initial value of each activity stream pref
+ const values = {};
+ for (const name of this._prefMap.keys()) {
values[name] = this._prefs.get(name);
}
// Set the initial state of all prefs in redux
this.store.dispatch(ac.BroadcastToContent({type: at.PREFS_INITIAL_VALUES, data: values}));
}
removeListeners() {
- for (const name of this._prefNames) {
- this._prefs.ignore(name, this._observers.get(name));
- }
- this._observers.clear();
+ this._prefs.ignoreBranch(this);
}
onAction(action) {
switch (action.type) {
case at.INIT:
this.init();
break;
case at.UNINIT:
this.removeListeners();
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -1,22 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {utils: Cu} = Components;
-const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
+const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
+const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
-const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Prefs",
- "resource://activity-stream/lib/ActivityStreamPrefs.jsm");
+const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
/**
* Store - This has a similar structure to a redux store, but includes some extra
* functionality to allow for routing of actions between the Main processes
* and child processes via a ActivityStreamMessageChannel.
* It also accepts an array of "Feeds" on inititalization, which
* can listen for any action that is dispatched through the store.
*/
@@ -25,52 +22,52 @@ this.Store = class Store {
/**
* constructor - The redux store and message manager are created here,
* but no listeners are added until "init" is called.
*/
constructor() {
this._middleware = this._middleware.bind(this);
// Bind each redux method so we can call it directly from the Store. E.g.,
// store.dispatch() will call store._store.dispatch();
- ["dispatch", "getState", "subscribe"].forEach(method => {
- this[method] = (...args) => {
- return this._store[method](...args);
- };
- });
+ for (const method of ["dispatch", "getState", "subscribe"]) {
+ this[method] = (...args) => this._store[method](...args);
+ }
this.feeds = new Map();
- this._feedFactories = null;
this._prefs = new Prefs();
- this._prefHandlers = new Map();
this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
this._store = redux.createStore(
redux.combineReducers(reducers),
redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
);
}
/**
* _middleware - This is redux middleware consumed by redux.createStore.
* it calls each feed's .onAction method, if one
* is defined.
*/
- _middleware(store) {
+ _middleware() {
return 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);
+ }
+ }
};
}
/**
* initFeed - Initializes a feed by calling its constructor function
*
* @param {string} feedName The name of a feed, as defined in the object
* passed to Store.init
*/
initFeed(feedName) {
- const feed = this._feedFactories[feedName]();
+ const feed = this._feedFactories.get(feedName)();
feed.store = this;
this.feeds.set(feedName, feed);
}
/**
* uninitFeed - Removes a feed and calls its uninit function if defined
*
* @param {string} feedName The name of a feed, as defined in the object
@@ -83,63 +80,55 @@ this.Store = class Store {
}
if (feed.uninit) {
feed.uninit();
}
this.feeds.delete(feedName);
}
/**
- * maybeStartFeedAndListenForPrefChanges - Listen for pref changes that turn a
- * 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
+ * onPrefChanged - Listener for handling feed changes.
*/
- maybeStartFeedAndListenForPrefChanges(prefName) {
- // Create a listener that turns the feed off/on based on changes
- // to the pref, and cache it so we can unlisten on shut-down.
- const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(prefName) : this.uninitFeed(prefName));
- this._prefHandlers.set(prefName, onPrefChanged);
- this._prefs.observe(prefName, onPrefChanged);
-
- // 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);
+ onPrefChanged(name, value) {
+ if (this._feedFactories.has(name)) {
+ if (value) {
+ this.initFeed(name);
+ } else {
+ this.uninitFeed(name);
+ }
}
}
/**
* init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
*
- * @param {array} feedConstructors An array of configuration objects for feeds
- * each with .name (the name of the pref for the feed) and .init,
- * a function that returns an instance of the feed
+ * @param {Map} feedFactories A Map of feeds with the name of the pref for
+ * the feed as the key and a function that
+ * constructs an instance of the feed.
*/
- init(feedConstructors) {
- if (feedConstructors) {
- this._feedFactories = feedConstructors;
- for (const pref of Object.keys(feedConstructors)) {
- this.maybeStartFeedAndListenForPrefChanges(pref);
+ init(feedFactories) {
+ this._feedFactories = feedFactories;
+ for (const pref of feedFactories.keys()) {
+ if (this._prefs.get(pref)) {
+ this.initFeed(pref);
}
}
+
+ this._prefs.observeBranch(this);
this._messageChannel.createChannel();
}
/**
* uninit - Uninitalizes each feed, clears them, and destroys the message
* manager channel.
*
* @return {type} description
*/
uninit() {
+ this._prefs.ignoreBranch(this);
this.feeds.forEach(feed => this.uninitFeed(feed));
- this._prefHandlers.forEach((handler, pref) => this._prefs.ignore(pref, handler));
- this._prefHandlers.clear();
+ this.feeds.clear();
this._feedFactories = null;
- this.feeds.clear();
this._messageChannel.destroyChannel();
}
};
this.EXPORTED_SYMBOLS = ["Store"];
--- a/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
@@ -1,51 +1,62 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* globals Services */
"use strict";
const {interfaces: Ci, utils: Cu} = Components;
+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", {});
-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");
+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",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
-XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySender",
- "resource://activity-stream/lib/TelemetrySender.jsm");
this.TelemetryFeed = class TelemetryFeed {
constructor(options) {
this.sessions = new Map();
- this.telemetryClientId = null;
- this.telemetrySender = null;
}
- async init() {
+ init() {
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() {
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
*
* @param {string} id the portID of the open session
* @param {number} absVisChangeTime absolute timestamp of
* document.visibilityState becoming visible
*/
addSession(id, absVisChangeTime) {
// XXX note that there is a race condition here; we're assuming that no
@@ -107,75 +118,75 @@ this.TelemetryFeed = class TelemetryFeed
}
/**
* createPing - Create a ping with common properties
*
* @param {string} id The portID of the session, if a session is relevant (optional)
* @return {obj} A telemetry ping
*/
- createPing(portID) {
+ async createPing(portID) {
const appInfo = this.store.getState().App;
const ping = {
- client_id: this.telemetryClientId,
+ client_id: await this.telemetryClientId,
addon_version: appInfo.version,
locale: appInfo.locale
};
// If the ping is part of a user session, add session-related info
if (portID) {
const session = this.sessions.get(portID);
Object.assign(ping, {
session_id: session.session_id,
page: session.page
});
}
return ping;
}
- createUserEvent(action) {
+ async createUserEvent(action) {
return Object.assign(
- this.createPing(au.getPortIdOfSender(action)),
+ await this.createPing(au.getPortIdOfSender(action)),
action.data,
{action: "activity_stream_user_event"}
);
}
- createUndesiredEvent(action) {
+ async createUndesiredEvent(action) {
return Object.assign(
- this.createPing(au.getPortIdOfSender(action)),
+ await this.createPing(au.getPortIdOfSender(action)),
{value: 0}, // Default value
action.data,
{action: "activity_stream_undesired_event"}
);
}
- createPerformanceEvent(action) {
+ async createPerformanceEvent(action) {
return Object.assign(
- this.createPing(au.getPortIdOfSender(action)),
+ await this.createPing(au.getPortIdOfSender(action)),
action.data,
{action: "activity_stream_performance_event"}
);
}
- createSessionEndEvent(session) {
+ async createSessionEndEvent(session) {
return Object.assign(
- this.createPing(),
+ await this.createPing(),
{
session_id: session.session_id,
page: session.page,
session_duration: session.session_duration,
action: "activity_stream_session",
perf: session.perf
}
);
}
- sendEvent(event) {
- this.telemetrySender.sendPing(event);
+ async sendEvent(eventPromise) {
+ this.telemetrySender.sendPing(await eventPromise);
}
onAction(action) {
switch (action.type) {
case at.INIT:
this.init();
break;
case at.NEW_TAB_VISIBLE:
@@ -196,15 +207,17 @@ this.TelemetryFeed = class TelemetryFeed
break;
}
}
uninit() {
Services.obs.removeObserver(this.browserOpenNewtabStart,
"browser-open-newtab-start");
- this.telemetrySender.uninit();
- this.telemetrySender = null;
+ // Only uninit if the getter has initialized it
+ if (Object.prototype.hasOwnProperty.call(this, "telemetrySender")) {
+ this.telemetrySender.uninit();
+ }
// TODO: Send any unfinished sessions
}
};
this.EXPORTED_SYMBOLS = ["TelemetryFeed"];
--- a/browser/extensions/activity-stream/lib/TelemetrySender.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetrySender.jsm
@@ -1,18 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const {interfaces: Ci, utils: Cu} = Components;
-
Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
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
// used, to avoid extra weirdness for people who happen to have the SDK-based
// installed. Though maybe we should just forcibly disable the old add-on?
const PREF_BRANCH = "browser.newtabpage.activity-stream.";
const ENDPOINT_PREF = `${PREF_BRANCH}telemetry.ping.endpoint`;
const TELEMETRY_PREF = `${PREF_BRANCH}telemetry`;
@@ -66,27 +67,27 @@ TelemetrySender.prototype = {
_onTelemetryPrefChange(prefVal) {
this._enabled = prefVal;
},
_onFhrPrefChange(prefVal) {
this._fhrEnabled = prefVal;
},
- async sendPing(data) {
+ sendPing(data) {
if (this.logging) {
// performance related pings cause a lot of logging, so we mute them
if (data.action !== "activity_stream_performance") {
console.log(`TELEMETRY PING: ${JSON.stringify(data)}\n`); // eslint-disable-line no-console
}
}
if (!this.enabled) {
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) {
Cu.reportError(`Ping failure with HTTP response code: ${response.status}`);
}
}).catch(e => {
Cu.reportError(`Ping failure with error: ${e}`);
});
},
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -1,83 +1,125 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {utils: Cu} = Components;
-const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
-Cu.import("resource://gre/modules/NewTabUtils.jsm");
-Cu.import("resource:///modules/PreviewProvider.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 UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
-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}));
+const DEFAULT_TOP_SITES = [];
this.TopSitesFeed = class TopSitesFeed {
constructor() {
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) {
let screenshot = await PreviewProvider.getThumbnail(url);
const action = {type: at.SCREENSHOT_UPDATED, data: {url, screenshot}};
this.store.dispatch(ac.BroadcastToContent(action));
}
- async getLinksWithDefaults(action) {
- let links = await NewTabUtils.activityStreamLinks.getTopSites();
+ sortLinks(frecent, pinned) {
+ let sortedLinks = [...frecent, ...DEFAULT_TOP_SITES];
+ sortedLinks = sortedLinks.filter(link => !NewTabUtils.pinnedLinks.isPinned(link));
- if (!links) {
- links = [];
+ // 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) {
+ let pinned = NewTabUtils.pinnedLinks.links;
+ let frecent = await NewTabUtils.activityStreamLinks.getTopSites();
+
+ if (!frecent) {
+ frecent = [];
} 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) {
- links = [...links, ...DEFAULT_TOP_SITES].slice(0, TOP_SITES_SHOWMORE_LENGTH);
- }
-
- return links;
+ return this.sortLinks(frecent, pinned);
}
async refresh(action) {
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};
// Send an update to content so the preloaded tab can get the updated content
this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
this.lastUpdated = Date.now();
-
- // Now, get a screenshot for every item
- for (let link of links) {
- this.getScreenshot(link.url);
- }
}
openNewWindow(action, isPrivate = false) {
const win = action._target.browser.ownerGlobal;
win.openLinkIn(action.data.url, "window", {private: isPrivate});
}
onAction(action) {
let realRows;
switch (action.type) {
+ case at.INIT:
+ this.init();
+ break;
case at.NEW_TAB_LOAD:
// Only check against real rows returned from history, not default ones.
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 (realRows.length < TOP_SITES_SHOWMORE_LENGTH) {
- this.refresh(action);
- } else if (Date.now() - this.lastUpdated >= UPDATE_TIME) {
+ if (
+ // When a new tab is opened, if we don't have enough top sites yet, refresh the data.
+ (realRows.length < TOP_SITES_SHOWMORE_LENGTH) ||
+
// When a new tab is opened, if the last time we refreshed the data
// is greater than 15 minutes, refresh the data.
+ (Date.now() - this.lastUpdated >= UPDATE_TIME)
+ ) {
this.refresh(action);
}
break;
case at.OPEN_NEW_WINDOW:
this.openNewWindow(action);
break;
case at.OPEN_PRIVATE_WINDOW: {
this.openNewWindow(action, true);
--- a/browser/extensions/activity-stream/test/schemas/pings.js
+++ b/browser/extensions/activity-stream/test/schemas/pings.js
@@ -26,16 +26,19 @@ const UserEventPing = Joi.object().keys(
const UserEventAction = Joi.object().keys({
type: Joi.string().required(),
data: Joi.object().keys({
event: Joi.valid([
"CLICK",
"SEARCH",
"BLOCK",
"DELETE",
+ "DELETE_CONFIRM",
+ "DIALOG_CANCEL",
+ "DIALOG_OPEN",
"OPEN_NEW_WINDOW",
"OPEN_PRIVATE_WINDOW",
"OPEN_NEWTAB_PREFS",
"CLOSE_NEWTAB_PREFS",
"BOOKMARK_DELETE",
"BOOKMARK_ADD"
]).required(),
source: Joi.valid(["TOP_SITES"]),
--- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -1,10 +1,10 @@
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");
describe("Reducers", () => {
describe("App", () => {
it("should return the initial state", () => {
const nextState = App(undefined, {type: "FOO"});
assert.equal(nextState, INITIAL_STATE.App);
});
@@ -142,9 +142,35 @@ describe("Reducers", () => {
});
it("should return a new .pref object instead of mutating", () => {
const oldState = prevState({foo: 1});
const state = Prefs(oldState, {type: at.PREF_CHANGED, data: {name: "foo", value: 2}});
assert.notEqual(oldState.values, state.values);
});
});
});
+ 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);
+ });
+ });
});
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -78,33 +78,33 @@ describe("ActivityStream", () => {
});
it("should not reset default prefs if the reason is something else", () => {
as.uninstall("foo");
assert.notCalled(as._defaultPrefs.reset);
});
});
describe("feeds", () => {
it("should create a Localization feed", () => {
- const feed = as.feeds["feeds.localization"]();
+ const feed = as.feeds.get("feeds.localization")();
assert.instanceOf(feed, Fake);
});
it("should create a NewTabInit feed", () => {
- const feed = as.feeds["feeds.newtabinit"]();
+ const feed = as.feeds.get("feeds.newtabinit")();
assert.instanceOf(feed, Fake);
});
it("should create a Places feed", () => {
- const feed = as.feeds["feeds.places"]();
+ const feed = as.feeds.get("feeds.places")();
assert.instanceOf(feed, Fake);
});
it("should create a TopSites feed", () => {
- const feed = as.feeds["feeds.topsites"]();
+ const feed = as.feeds.get("feeds.topsites")();
assert.instanceOf(feed, Fake);
});
it("should create a Telemetry feed", () => {
- const feed = as.feeds["feeds.telemetry"]();
+ const feed = as.feeds.get("feeds.telemetry")();
assert.instanceOf(feed, Fake);
});
it("should create a Prefs feed", () => {
- const feed = as.feeds["feeds.prefs"]();
+ const feed = as.feeds.get("feeds.prefs")();
assert.instanceOf(feed, Fake);
});
});
});
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStreamPrefs.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamPrefs.test.js
@@ -1,63 +1,121 @@
const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream.";
const {Prefs, DefaultPrefs} = require("lib/ActivityStreamPrefs.jsm");
-const TEST_PREF_CONFIG = [
- {name: "foo", value: true},
- {name: "bar", value: "BAR"},
- {name: "baz", value: 1}
-];
+const TEST_PREF_CONFIG = new Map([
+ ["foo", {value: true}],
+ ["bar", {value: "BAR"}],
+ ["baz", {value: 1}],
+ ["qux", {value: "foo", value_local_dev: "foofoo"}]
+]);
describe("ActivityStreamPrefs", () => {
describe("Prefs", () => {
+ let p;
+ beforeEach(() => {
+ p = new Prefs();
+ });
it("should have get, set, and observe methods", () => {
- const p = new Prefs();
assert.property(p, "get");
assert.property(p, "set");
assert.property(p, "observe");
});
describe(".branchName", () => {
it("should return the activity stream branch by default", () => {
- const p = new Prefs();
assert.equal(p.branchName, ACTIVITY_STREAM_PREF_BRANCH);
});
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");
});
});
+ 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("#init", () => {
let defaultPrefs;
+ let sandbox;
beforeEach(() => {
+ sandbox = sinon.sandbox.create();
defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
sinon.spy(defaultPrefs.branch, "setBoolPref");
sinon.spy(defaultPrefs.branch, "setStringPref");
sinon.spy(defaultPrefs.branch, "setIntPref");
});
+ afterEach(() => {
+ sandbox.restore();
+ });
it("should initialize a boolean pref", () => {
defaultPrefs.init();
assert.calledWith(defaultPrefs.branch.setBoolPref, "foo", true);
});
it("should initialize a string pref", () => {
defaultPrefs.init();
assert.calledWith(defaultPrefs.branch.setStringPref, "bar", "BAR");
});
it("should initialize a integer pref", () => {
defaultPrefs.init();
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", () => {
it("should clear user preferences for each pref in the config", () => {
const defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
sinon.spy(defaultPrefs.branch, "clearUserPref");
defaultPrefs.reset();
- for (const pref of TEST_PREF_CONFIG) {
- assert.calledWith(defaultPrefs.branch.clearUserPref, pref.name);
+ for (const name of TEST_PREF_CONFIG.keys()) {
+ assert.calledWith(defaultPrefs.branch.clearUserPref, name);
}
});
});
});
});
--- a/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
@@ -23,16 +23,26 @@ describe("PlacesFeed", () => {
deleteHistoryEntry: sandbox.spy(),
blockURL: sandbox.spy()
}
});
globals.set("PlacesUtils", {
history: {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, "removeObserver");
sandbox.spy(global.Components.utils, "reportError");
feed = new PlacesFeed();
feed.store = {dispatch: sinon.spy()};
});
afterEach(() => globals.restore());
--- a/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PrefsFeed.test.js
@@ -1,54 +1,44 @@
const {PrefsFeed} = require("lib/PrefsFeed.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", () => {
let feed;
beforeEach(() => {
- feed = new PrefsFeed(FAKE_PREFS.map(p => p.name));
+ feed = new PrefsFeed(FAKE_PREFS);
feed.store = {dispatch: sinon.spy()};
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(),
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", () => {
feed.onAction(ac.SetPref("foo", 2));
assert.calledWith(feed._prefs.set, "foo", 2);
});
it("should dispatch PREFS_INITIAL_VALUES on init", () => {
feed.onAction({type: at.INIT});
assert.calledOnce(feed.store.dispatch);
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});
});
- it("should add one observer per pref on init", () => {
- feed.onAction({type: at.INIT});
- FAKE_PREFS.forEach(pref => {
- assert.calledWith(feed._prefs.observe, pref.name);
- assert.isTrue(feed._observers.has(pref.name));
- });
- });
- it("should call onPrefChanged when an observer is called", () => {
- sinon.stub(feed, "onPrefChanged");
+ it("should add one branch observer on init", () => {
feed.onAction({type: at.INIT});
- const handlerForFoo = feed._observers.get("foo");
-
- handlerForFoo(true);
-
- assert.calledWith(feed.onPrefChanged, "foo", true);
+ assert.calledOnce(feed._prefs.observeBranch);
+ assert.calledWith(feed._prefs.observeBranch, feed);
});
- it("should remove all observers on uninit", () => {
+ it("should remove the branch observer on uninit", () => {
feed.onAction({type: at.UNINIT});
- FAKE_PREFS.forEach(pref => {
- assert.calledWith(feed._prefs.ignore, pref.name);
- });
+ assert.calledOnce(feed._prefs.ignoreBranch);
+ assert.calledWith(feed._prefs.ignoreBranch, feed);
});
it("should send a PREF_CHANGED action when onPrefChanged is called", () => {
feed.onPrefChanged("foo", 2);
assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({type: at.PREF_CHANGED, data: {name: "foo", value: 2}}));
});
});
--- a/browser/extensions/activity-stream/test/unit/lib/Store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/Store.test.js
@@ -38,26 +38,26 @@ describe("Store", () => {
});
it("should connect the ActivityStreamMessageChannel's middleware", () => {
store.dispatch({type: "FOO"});
assert.calledOnce(store._messageChannel.middleware);
});
describe("#initFeed", () => {
it("should add an instance of the feed to .feeds", () => {
class Foo {}
- store._prefs.set("foo", false);
- store.init({foo: () => new Foo()});
+ store._prefs.set("foo", true);
+ store.init(new Map([["foo", () => new Foo()]]));
store.initFeed("foo");
assert.isTrue(store.feeds.has("foo"), "foo is set");
assert.instanceOf(store.feeds.get("foo"), Foo);
});
it("should add a .store property to the feed", () => {
class Foo {}
- store._feedFactories = {foo: () => new Foo()};
+ store._feedFactories = new Map([["foo", () => new Foo()]]);
store.initFeed("foo");
assert.propertyVal(store.feeds.get("foo"), "store", store);
});
});
describe("#uninitFeed", () => {
it("should not throw if no feed with that name exists", () => {
assert.doesNotThrow(() => {
@@ -65,96 +65,97 @@ describe("Store", () => {
});
});
it("should call the feed's uninit function if it is defined", () => {
let feed;
function createFeed() {
feed = {uninit: sinon.spy()};
return feed;
}
- store._feedFactories = {foo: createFeed};
+ store._feedFactories = new Map([["foo", createFeed]]);
store.initFeed("foo");
store.uninitFeed("foo");
assert.calledOnce(feed.uninit);
});
it("should remove the feed from .feeds", () => {
class Foo {}
- store._feedFactories = {foo: () => new Foo()};
+ store._feedFactories = new Map([["foo", () => new Foo()]]);
store.initFeed("foo");
store.uninitFeed("foo");
assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
});
});
- describe("maybeStartFeedAndListenForPrefChanges", () => {
+ describe("onPrefChanged", () => {
beforeEach(() => {
sinon.stub(store, "initFeed");
sinon.stub(store, "uninitFeed");
+ store._prefs.set("foo", false);
+ store.init(new Map([["foo", () => ({})]]));
});
- it("should initialize the feed if the Pref is set to true", () => {
- store._prefs.set("foo", true);
- store.maybeStartFeedAndListenForPrefChanges("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 not initialize the feed if the Pref is set to false", () => {
- store._prefs.set("foo", false);
- store.maybeStartFeedAndListenForPrefChanges("foo");
+ it("should uninitialize the feed if called with false", () => {
+ store.onPrefChanged("foo", false);
+
+ assert.calledWith(store.uninitFeed, "foo");
assert.notCalled(store.initFeed);
});
- it("should observe the pref", () => {
- sinon.stub(store._prefs, "observe");
- store.maybeStartFeedAndListenForPrefChanges("foo");
- assert.calledWith(store._prefs.observe, "foo", store._prefHandlers.get("foo"));
- });
- 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");
- });
+ it("should do nothing if not an expected feed", () => {
+ store.onPrefChanged("bar", false);
+
+ assert.notCalled(store.initFeed);
+ assert.notCalled(store.uninitFeed);
});
});
describe("#init", () => {
- it("should call .maybeStartFeedAndListenForPrefChanges with each key", () => {
- sinon.stub(store, "maybeStartFeedAndListenForPrefChanges");
- store.init({foo: () => {}, bar: () => {}});
- assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "foo");
- assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "bar");
+ it("should call .initFeed with each key", () => {
+ sinon.stub(store, "initFeed");
+ store._prefs.set("foo", true);
+ store._prefs.set("bar", true);
+ 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", () => {
- store.init();
+ store.init(new Map());
assert.calledOnce(store._messageChannel.createChannel);
});
});
describe("#uninit", () => {
- it("should clear .feeds, ._prefHandlers, and ._feedFactories", () => {
+ it("should clear .feeds and ._feedFactories", () => {
store._prefs.set("a", true);
- store._prefs.set("b", true);
- store._prefs.set("c", true);
- store.init({
- a: () => ({}),
- b: () => ({}),
- c: () => ({})
- });
+ store.init(new Map([
+ ["a", () => ({})],
+ ["b", () => ({})],
+ ["c", () => ({})]
+ ]));
store.uninit();
assert.equal(store.feeds.size, 0);
- assert.equal(store._prefHandlers.size, 0);
assert.isNull(store._feedFactories);
});
it("should destroy the ActivityStreamMessageChannel channel", () => {
store.uninit();
assert.calledOnce(store._messageChannel.destroyChannel);
});
});
describe("#getState", () => {
@@ -166,17 +167,17 @@ describe("Store", () => {
});
describe("#dispatch", () => {
it("should call .onAction of each feed", () => {
const {dispatch} = store;
const sub = {onAction: sinon.spy()};
const action = {type: "FOO"};
store._prefs.set("sub", true);
- store.init({sub: () => sub});
+ store.init(new Map([["sub", () => sub]]));
dispatch(action);
assert.calledWith(sub.onAction, action);
});
it("should call the reducers", () => {
const {dispatch} = store;
store._store = createStore(addNumberReducer);
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
@@ -42,30 +42,26 @@ describe("TelemetryFeed", () => {
globals.set("gUUIDGenerator", {generateUUID: () => FAKE_UUID});
instance = new TelemetryFeed();
instance.store = store;
});
afterEach(() => {
globals.restore();
});
describe("#init", () => {
- it("should add .telemetrySender, a TelemetrySender instance", async () => {
- assert.isNull(instance.telemetrySender);
- await instance.init();
+ it("should add .telemetrySender, a TelemetrySender instance", () => {
assert.instanceOf(instance.telemetrySender, TelemetrySender);
});
it("should add .telemetryClientId from the ClientID module", async () => {
- assert.isNull(instance.telemetryClientId);
- await instance.init();
- assert.equal(instance.telemetryClientId, FAKE_TELEMETRY_ID);
+ assert.equal(await 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");
- await instance.init();
+ instance.init();
assert.calledOnce(Services.obs.addObserver);
assert.calledWithExactly(Services.obs.addObserver,
instance.browserOpenNewtabStart, "browser-open-newtab-start");
});
});
describe("#addSession", () => {
it("should add a session", () => {
@@ -125,103 +121,103 @@ describe("TelemetryFeed", () => {
// Did we call sendEvent with the result of createSessionEndEvent?
assert.calledWith(instance.createSessionEndEvent, session);
assert.calledWith(instance.sendEvent, instance.createSessionEndEvent.firstCall.returnValue);
});
});
describe("ping creators", () => {
beforeEach(async () => await instance.init());
describe("#createPing", () => {
- it("should create a valid base ping without a session if no portID is supplied", () => {
- const ping = instance.createPing();
+ it("should create a valid base ping without a session if no portID is supplied", async () => {
+ const ping = await instance.createPing();
assert.validate(ping, BasePing);
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
const portID = "foo";
instance.addSession(portID);
const sessionID = instance.sessions.get(portID).session_id;
// Create a ping referencing the session
- const ping = instance.createPing(portID);
+ const ping = await instance.createPing(portID);
assert.validate(ping, BasePing);
// Make sure we added the right session-related stuff to the ping
assert.propertyVal(ping, "session_id", sessionID);
assert.propertyVal(ping, "page", "about:newtab");
});
});
describe("#createUserEvent", () => {
- it("should create a valid event", () => {
+ it("should create a valid event", async () => {
const portID = "foo";
const data = {source: "TOP_SITES", event: "CLICK"};
const action = ac.SendToMain(ac.UserEvent(data), portID);
const session = addSession(portID);
- const ping = instance.createUserEvent(action);
+ const ping = await instance.createUserEvent(action);
// Is it valid?
assert.validate(ping, UserEventPing);
// Does it have the right session_id?
assert.propertyVal(ping, "session_id", session.session_id);
});
});
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 ping = instance.createUndesiredEvent(action);
+ const ping = await instance.createUndesiredEvent(action);
// Is it valid?
assert.validate(ping, UndesiredPing);
// Does it have the right value?
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 data = {source: "TOP_SITES", event: "MISSING_IMAGE", value: 10};
const action = ac.SendToMain(ac.UndesiredEvent(data), portID);
const session = addSession(portID);
- const ping = instance.createUndesiredEvent(action);
+ const ping = await instance.createUndesiredEvent(action);
// Is it valid?
assert.validate(ping, UndesiredPing);
// Does it have the right session_id?
assert.propertyVal(ping, "session_id", session.session_id);
// Does it have the right value?
assert.propertyVal(ping, "value", 10);
});
});
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 ping = instance.createPerformanceEvent(action);
+ const ping = await instance.createPerformanceEvent(action);
// Is it valid?
assert.validate(ping, PerfPing);
// Does it have the right value?
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 data = {event: "PAGE_LOADED", value: 100};
const action = ac.SendToMain(ac.PerfEvent(data), portID);
const session = addSession(portID);
- const ping = instance.createPerformanceEvent(action);
+ const ping = await instance.createPerformanceEvent(action);
// Is it valid?
assert.validate(ping, PerfPing);
// Does it have the right session_id?
assert.propertyVal(ping, "session_id", session.session_id);
// Does it have the right value?
assert.propertyVal(ping, "value", 100);
});
});
describe("#createSessionEndEvent", () => {
- it("should create a valid event", () => {
- const ping = instance.createSessionEndEvent({
+ it("should create a valid event", async () => {
+ const ping = await instance.createSessionEndEvent({
session_id: FAKE_UUID,
page: "about:newtab",
session_duration: 12345,
perf: {
load_trigger_ts: 10,
load_trigger_type: "menu_plus_or_keyboard",
visibility_event_rcvd_ts: 20
}
@@ -231,30 +227,27 @@ describe("TelemetryFeed", () => {
assert.propertyVal(ping, "session_id", FAKE_UUID);
assert.propertyVal(ping, "page", "about:newtab");
assert.propertyVal(ping, "session_duration", 12345);
});
});
});
describe("#sendEvent", () => {
it("should call telemetrySender", async () => {
- await instance.init();
sandbox.stub(instance.telemetrySender, "sendPing");
const event = {};
- instance.sendEvent(event);
+ await instance.sendEvent(Promise.resolve(event));
assert.calledWith(instance.telemetrySender.sendPing, event);
});
});
describe("#uninit", () => {
- it("should call .telemetrySender.uninit and remove it", async () => {
- await instance.init();
+ it("should call .telemetrySender.uninit", () => {
const stub = sandbox.stub(instance.telemetrySender, "uninit");
instance.uninit();
assert.calledOnce(stub);
- assert.isNull(instance.telemetrySender);
});
it("should make this.browserOpenNewtabStart() stop observing browser-open-newtab-start", async () => {
await instance.init();
sandbox.spy(Services.obs, "removeObserver");
sandbox.stub(instance.telemetrySender, "uninit");
await instance.uninit();
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
@@ -160,17 +160,17 @@ describe("TelemetrySender", () => {
it("should POST given ping data to telemetry.ping.endpoint pref w/fetch",
async () => {
fetchStub.resolves(fakeFetchSuccessResponse);
await tSender.sendPing(fakePingJSON);
assert.calledOnce(fetchStub);
assert.calledWithExactly(fetchStub, fakeEndpointUrl,
- {method: "POST", body: fakePingJSON});
+ {method: "POST", body: JSON.stringify(fakePingJSON)});
});
it("should log HTTP failures using Cu.reportError", async () => {
fetchStub.resolves(fakeFetchHttpErrorResponse);
await tSender.sendPing(fakePingJSON);
assert.called(Components.utils.reportError);
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -1,43 +1,119 @@
"use strict";
-const {TopSitesFeed, UPDATE_TIME, TOP_SITES_SHOWMORE_LENGTH, DEFAULT_TOP_SITES} = require("lib/TopSitesFeed.jsm");
-const {GlobalOverrider} = require("test/unit/utils");
+const injector = require("inject!lib/TopSitesFeed.jsm");
+const {UPDATE_TIME, TOP_SITES_SHOWMORE_LENGTH} = require("lib/TopSitesFeed.jsm");
+const {FakePrefs, GlobalOverrider} = require("test/unit/utils");
const action = {meta: {fromTarget: {}}};
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_SCREENSHOT = "data123";
describe("Top Sites Feed", () => {
+ let TopSitesFeed;
+ let DEFAULT_TOP_SITES;
let feed;
let globals;
let sandbox;
let links;
let clock;
+ let fakeNewTabUtils;
beforeEach(() => {
globals = new GlobalOverrider();
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))});
+ FakePrefs.prototype.prefs["default.sites"] = "https://foo.com/";
+ ({TopSitesFeed, DEFAULT_TOP_SITES} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
feed = new TopSitesFeed();
feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
links = FAKE_LINKS;
clock = sinon.useFakeTimers();
});
afterEach(() => {
globals.restore();
clock.restore();
});
- it("should have default sites with .isDefault = true", () => {
- DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "isDefault", true));
+ describe("#init", () => {
+ 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", () => {
+ beforeEach(() => {
+ feed.init();
+ });
+
it("should get the links from NewTabUtils", async () => {
const result = await feed.getLinksWithDefaults();
assert.deepEqual(result, links);
assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
});
it("should add defaults if there are are not enough links", async () => {
links = [{url: "foo.com"}];
const result = await feed.getLinksWithDefaults();
@@ -59,21 +135,31 @@ describe("Top Sites Feed", () => {
describe("#refresh", () => {
it("should dispatch an action with the links returned", async () => {
sandbox.stub(feed, "getScreenshot");
await feed.refresh(action);
assert.calledOnce(feed.store.dispatch);
assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
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");
+ const rows = [{url: FAKE_LINKS[0].url, screenshot: "foo.jpg"}];
+ feed.store.getState = () => ({TopSites: {rows}});
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", () => {
it("should call PreviewProvider.getThumbnail with the right url", async () => {
const url = "foo.com";
await feed.getScreenshot(url);
assert.calledWith(global.PreviewProvider.getThumbnail, url);
});
--- a/browser/extensions/activity-stream/test/unit/unit-entry.js
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -8,16 +8,17 @@ const files = req.keys();
sinon.assert.expose(assert, {prefix: ""});
chai.use(chaiAssertions);
let overrider = new GlobalOverrider();
overrider.set({
Components: {
+ classes: {},
interfaces: {},
utils: {
import() {},
importGlobalProperties() {},
reportError() {},
now: () => window.performance.now()
}
},
@@ -33,16 +34,17 @@ overrider.set({
removeMessageListener() {}
},
appShell: {hiddenDOMWindow: {performance: new FakePerformance()}},
obs: {
addObserver() {},
removeObserver() {}
},
prefs: {
+ getStringPref() {},
getDefaultBranch() {
return {
setBoolPref() {},
setIntPref() {},
setStringPref() {},
clearUserPref() {}
};
}
--- a/browser/extensions/activity-stream/test/unit/utils.js
+++ b/browser/extensions/activity-stream/test/unit/utils.js
@@ -99,16 +99,19 @@ FakePrefs.prototype = {
observe(prefName, callback) {
this.observers[prefName] = callback;
},
ignore(prefName, callback) {
if (prefName in this.observers) {
delete this.observers[prefName];
}
},
+ _prefBranch: {},
+ observeBranch(listener) {},
+ ignoreBranch(listener) {},
prefs: {},
get(prefName) { return this.prefs[prefName]; },
set(prefName, value) {
this.prefs[prefName] = value;
if (prefName in this.observers) {
this.observers[prefName](value);