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

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

View File

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

View File

@@ -4,17 +4,18 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.importGlobalProperties(["fetch"]);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["fetch"]);
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";
@@ -62,7 +63,11 @@ function init(reason) {
}
const options = Object.assign({}, startupData || {}, ACTIVITY_STREAM_OPTIONS);
activityStream = new ActivityStream(options);
activityStream.init(reason);
try {
activityStream.init(reason);
} catch (e) {
Cu.reportError(e);
}
}
/**
@@ -113,7 +118,8 @@ function observe(subject, topic, data) {
switch (topic) {
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;
}
}

View File

@@ -17,11 +17,20 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS :
// 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",
@@ -45,13 +54,9 @@ const actionTypes = [
"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

View File

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

View File

@@ -97,16 +97,15 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS :
// 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
@@ -312,9 +311,10 @@ 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"];
@@ -373,7 +373,8 @@ class Base extends React.Component {
"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)
)
@@ -394,7 +395,7 @@ var _require = __webpack_require__(1);
const at = _require.actionTypes;
var _require2 = __webpack_require__(15);
var _require2 = __webpack_require__(16);
const perfSvc = _require2.perfService;
@@ -573,6 +574,10 @@ const INITIAL_STATE = {
Prefs: {
initialized: false,
values: {}
},
Dialog: {
visible: false,
data: {}
}
};
@@ -657,6 +662,22 @@ function TopSites() {
}
}
function Dialog() {
let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Dialog;
let action = arguments[1];
switch (action.type) {
case at.DIALOG_OPEN:
return Object.assign({}, prevState, { visible: true, data: action.data });
case at.DIALOG_CANCEL:
return Object.assign({}, prevState, { visible: false });
case at.DELETE_HISTORY_URL:
return Object.assign({}, INITIAL_STATE.Dialog);
default:
return prevState;
}
}
function Prefs() {
let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Prefs;
let action = arguments[1];
@@ -674,7 +695,7 @@ function Prefs() {
}
}
var reducers = { TopSites, App, Prefs };
var reducers = { TopSites, App, Prefs, Dialog };
module.exports = {
reducers,
INITIAL_STATE
@@ -693,6 +714,125 @@ module.exports = ReactDOM;
"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 {
@@ -770,7 +910,7 @@ class ContextMenu extends React.Component {
module.exports = ContextMenu;
/***/ }),
/* 10 */
/* 11 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@@ -782,95 +922,120 @@ 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;
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 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"
});
// default top sites have a limited set of context menu options
const AddBookmark = site => ({
id: "menu_action_bookmark",
icon: "bookmark",
action: ac.SendToMain({
type: at.BOOKMARK_URL,
data: site.url
}),
userEvent: "BOOKMARK_ADD"
});
let options = this.getDefaultContextMenu(site);
const OpenInNewWindow = site => ({
id: "menu_action_open_new_window",
icon: "new-window",
action: ac.SendToMain({
type: at.OPEN_NEW_WINDOW,
data: { url: site.url }
}),
userEvent: "OPEN_NEW_WINDOW"
});
// all other top sites have all the following context menu options
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"
}];
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"
}
options.forEach(option => {
},
userEvent: "DIALOG_OPEN"
});
class LinkMenu extends React.Component {
getOptions() {
const props = this.props;
const site = props.site;
const isBookmark = site.bookmarkGuid;
const isDefault = site.isDefault;
const options = [
// Bookmarks
!isDefault && (isBookmark ? RemoveBookmark(site) : AddBookmark(site)), !isDefault && { type: "separator" },
// Menu items for all sites
OpenInNewWindow(site), OpenInPrivateWindow(site),
// Blocking and deleting
!isDefault && { type: "separator" }, !isDefault && BlockUrl(site), !isDefault && DeleteUrl(site)].filter(o => o).map(option => {
const action = option.action,
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;
@@ -887,7 +1052,7 @@ module.exports = injectIntl(LinkMenu);
module.exports._unconnected = LinkMenu;
/***/ }),
/* 11 */
/* 12 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@@ -904,8 +1069,6 @@ 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;
@@ -967,7 +1130,7 @@ class PreferencesPane extends React.Component {
"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 })
),
@@ -976,7 +1139,7 @@ class PreferencesPane extends React.Component {
{ className: "prefs-pane" },
React.createElement(
"div",
{ className: classNames("sidebar", { hidden: !isVisible }) },
{ className: `sidebar ${isVisible ? "" : "hidden"}` },
React.createElement(
"div",
{ className: "prefs-modal-inner-wrapper" },
@@ -1015,7 +1178,7 @@ module.exports.PreferencesPane = PreferencesPane;
module.exports.PreferencesInput = PreferencesInput;
/***/ }),
/* 12 */
/* 13 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@@ -1114,7 +1277,7 @@ module.exports = connect()(injectIntl(Search));
module.exports._unconnected = Search;
/***/ }),
/* 13 */
/* 14 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@@ -1130,8 +1293,8 @@ 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);
@@ -1161,7 +1324,7 @@ class TopSite extends React.Component {
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" };
@@ -1183,8 +1346,13 @@ class TopSite extends React.Component {
),
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(
@@ -1235,7 +1403,7 @@ module.exports._unconnected = TopSites;
module.exports.TopSite = TopSite;
/***/ }),
/* 14 */
/* 15 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@@ -1270,7 +1438,7 @@ module.exports = function shortURL(link) {
};
/***/ }),
/* 15 */
/* 16 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@@ -1374,61 +1542,6 @@ module.exports = {
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) {

View File

@@ -44,6 +44,11 @@ input {
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,
@@ -222,7 +227,7 @@ main {
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; }
@@ -232,11 +237,20 @@ main {
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;
@@ -422,13 +436,16 @@ main {
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 {
@@ -454,3 +471,42 @@ main {
.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; }

View File

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

After

Width:  |  Height:  |  Size: 479 B

View File

@@ -56,9 +56,12 @@
"default_label_loading": "يُحمّل…",
"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": "علّم",
@@ -69,6 +72,7 @@
"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}",
@@ -91,6 +95,8 @@
"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": "خصص قسم المواقع الأكثر زيارة",
@@ -98,8 +104,23 @@
"edit_topsites_showless_button": "اعرض أقل",
"edit_topsites_done_button": "تمّ",
"edit_topsites_pin_button": "ثبّت هذا الموقع",
"edit_topsites_unpin_button": "افصل هذا الموقع",
"edit_topsites_edit_button": "حرّر هذا الموقع",
"edit_topsites_dismiss_button": "احذف هذا الموقع"
"edit_topsites_dismiss_button": "احذف هذا الموقع",
"edit_topsites_add_button": "أضِفْ",
"topsites_form_add_header": "موقع شائع جديد",
"topsites_form_edit_header": "حرّر الموقع الشائع",
"topsites_form_title_placeholder": "أدخل عنوانًا",
"topsites_form_url_placeholder": "اكتب أو ألصق مسارًا",
"topsites_form_add_button": "أضِفْ",
"topsites_form_save_button": "احفظ",
"topsites_form_cancel_button": "ألغِ",
"topsites_form_url_validation": "مطلوب مسار صالح",
"pocket_read_more": "المواضيع الشائعة:",
"pocket_read_even_more": "اعرض المزيد من الأخبار",
"pocket_feedback_header": "أفضل ما في الوِب، انتقاها أكثر من ٢٥ مليون شخص.",
"pocket_feedback_body": "يساعدك Pocket –عضو في أسرة موزيلا– على الوصول إلى محتوى عالِ الجودة ربما لم يُكن ليتاح لك بدونه.",
"pocket_send_feedback": "أرسل انطباعك"
},
"as": {},
"ast": {
@@ -533,10 +554,12 @@
"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",
@@ -570,6 +593,8 @@
"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",
@@ -581,6 +606,7 @@
"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",
@@ -590,6 +616,8 @@
"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": {
@@ -859,7 +887,6 @@
"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",
@@ -879,6 +906,8 @@
"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",
@@ -900,8 +929,6 @@
"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",
@@ -971,9 +998,12 @@
"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",
@@ -984,6 +1014,7 @@
"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}",
@@ -1006,6 +1037,8 @@
"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",
@@ -1013,8 +1046,23 @@
"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",
@@ -1142,9 +1190,12 @@
"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",
@@ -1155,6 +1206,7 @@
"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}",
@@ -1177,6 +1229,8 @@
"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",
@@ -1184,17 +1238,35 @@
"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",
@@ -1205,6 +1277,7 @@
"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}",
@@ -1227,6 +1300,8 @@
"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",
@@ -1245,7 +1320,11 @@
"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": {},
@@ -1509,9 +1588,12 @@
"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",
@@ -1522,7 +1604,9 @@
"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",
@@ -1532,7 +1616,43 @@
"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",
@@ -1923,9 +2043,12 @@
"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",
@@ -1936,6 +2059,7 @@
"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}",
@@ -1958,6 +2082,8 @@
"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",
@@ -1965,8 +2091,23 @@
"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": {
@@ -2111,7 +2252,77 @@
"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…",
@@ -2176,6 +2387,7 @@
"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": {
@@ -2285,9 +2497,12 @@
"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": "즐겨찾기",
@@ -2298,6 +2513,7 @@
"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} 검색",
@@ -2320,6 +2536,7 @@
"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": "상위 사이트 영역 꾸미기",
@@ -2436,9 +2653,12 @@
"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ą",
@@ -2449,6 +2669,7 @@
"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",
@@ -2471,6 +2692,8 @@
"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į",
@@ -2478,8 +2701,23 @@
"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": {
@@ -2487,9 +2725,115 @@
},
"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…",
@@ -3021,12 +3365,12 @@
"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",
@@ -3172,6 +3516,7 @@
"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",
@@ -3198,7 +3543,7 @@
"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ă.",
@@ -3215,7 +3560,18 @@
"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": "Новая вкладка",
@@ -3630,9 +3986,11 @@
"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": "புத்தகக்குறி",
@@ -3643,7 +4001,8 @@
"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": "இணையத்தில் தேடு",
@@ -3666,6 +4025,7 @@
"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": "உங்களின் சிறந்த தளங்களுக்கான தொகுதியை விருப்பமை",
@@ -3673,6 +4033,7 @@
"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": "சேர்",
@@ -3686,6 +4047,8 @@
"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": {},
@@ -3744,10 +4107,12 @@
"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": "เพิ่มที่คั่นหน้า",
@@ -3781,6 +4146,7 @@
"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": "ปรับแต่งส่วนไซต์เด่นของคุณ",
@@ -3792,11 +4158,17 @@
"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",
@@ -3995,6 +4367,8 @@
"default_label_loading": "لوڈ کر رہا ہے…",
"header_top_sites": "بہترین سائٹیں",
"header_highlights": "شہ سرخياں",
"header_stories": "بہترین کہانیاں",
"header_stories_from": "من جانب",
"type_label_visited": "دورہ شدہ",
"type_label_bookmarked": "نشان شدہ",
"type_label_synced": "کسی دوسرے آلے سے ہمہ وقت ساز کیا گیا ہے",
@@ -4008,6 +4382,7 @@
"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} پر تلاش کریں",
@@ -4022,16 +4397,37 @@
"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": {},

View File

@@ -4,108 +4,92 @@
"use strict";
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
// NB: Eagerly load modules that will be loaded/constructed/initialized in the
// common case to avoid the overhead of wrapping and detecting lazy loading.
const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {DefaultPrefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
const {LocalizationFeed} = Cu.import("resource://activity-stream/lib/LocalizationFeed.jsm", {});
const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {});
const {PlacesFeed} = Cu.import("resource://activity-stream/lib/PlacesFeed.jsm", {});
const {PrefsFeed} = Cu.import("resource://activity-stream/lib/PrefsFeed.jsm", {});
const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
const {TelemetryFeed} = Cu.import("resource://activity-stream/lib/TelemetryFeed.jsm", {});
const {TopSitesFeed} = Cu.import("resource://activity-stream/lib/TopSitesFeed.jsm", {});
const REASON_ADDON_UNINSTALL = 6;
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",
value: true,
init: () => new LocalizationFeed()
},
{
name: "feeds.newtabinit",
title: "Sends a copy of the state to each new tab that is opened",
value: true,
init: () => new NewTabInit()
},
{
name: "feeds.places",
title: "Listens for and relays various Places-related events",
value: true,
init: () => new PlacesFeed()
},
{
name: "feeds.prefs",
title: "Preferences",
value: true,
init: () => new PrefsFeed(PREFS_CONFIG.map(pref => pref.name))
},
{
name: "feeds.telemetry",
title: "Relays telemetry-related actions to TelemetrySender",
value: true,
init: () => new TelemetryFeed()
},
{
name: "feeds.topsites",
title: "Queries places and gets metadata for Top Sites section",
value: true,
init: () => new TopSitesFeed()
},
{
name: "showSearch",
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,
value_local_dev: false
}],
["telemetry.log", {
title: "Log telemetry events in the console",
value: false,
value_local_dev: true
}],
["telemetry.ping.endpoint", {
title: "Telemetry server endpoint",
value: "https://onyx_tiles.stage.mozaws.net/v4/links/activity-stream"
}]
]);
const FEEDS_CONFIG = new Map();
for (const {name, factory, title, value} of [
{
name: "localization",
factory: () => new LocalizationFeed(),
title: "Initialize strings and detect locale for Activity Stream",
value: true
},
{
name: "showTopSites",
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
factory: () => new TelemetryFeed(),
title: "Relays telemetry-related actions to TelemetrySender",
value: true
},
{
name: "telemetry.log",
title: "Log telemetry events in the console",
value: false
},
{
name: "telemetry.ping.endpoint",
title: "Telemetry server endpoint",
value: "https://tiles.services.mozilla.com/v3/links/activity-stream"
}
];
const feeds = {};
for (const pref of PREFS_CONFIG) {
if (pref.name.match(/^feeds\./)) {
feeds[pref.name] = pref.init;
name: "topsites",
factory: () => new TopSitesFeed(),
title: "Queries places and gets metadata for Top Sites section",
value: true
}
]) {
const pref = `feeds.${name}`;
FEEDS_CONFIG.set(pref, factory);
PREFS_CONFIG.set(pref, {title, value});
}
this.ActivityStream = class ActivityStream {
@@ -122,17 +106,17 @@ this.ActivityStream = class ActivityStream {
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});

View File

@@ -5,20 +5,10 @@
"use strict";
const {utils: Cu} = Components;
Cu.import("resource:///modules/AboutNewTab.jsm");
Cu.import("resource://gre/modules/RemotePageManager.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {
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";
@@ -108,11 +98,11 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
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);
}
/**

View File

@@ -18,10 +18,23 @@ this.Prefs = class Prefs extends Preferences {
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 {
@@ -29,8 +42,8 @@ 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)
@@ -64,8 +77,19 @@ this.DefaultPrefs = class DefaultPrefs {
* 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);
}
}
@@ -73,8 +97,8 @@ this.DefaultPrefs = class DefaultPrefs {
* 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);
}
}
};

View File

@@ -4,13 +4,10 @@
"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";

View File

@@ -4,7 +4,8 @@
"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

View File

@@ -3,17 +3,16 @@
* 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");
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";
@@ -151,8 +150,14 @@ class PlacesFeed {
}
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);
}

View File

@@ -4,31 +4,26 @@
"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);
}
@@ -36,10 +31,7 @@ this.PrefsFeed = class PrefsFeed {
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) {

View File

@@ -5,13 +5,10 @@
const {utils: Cu} = Components;
const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Prefs",
"resource://activity-stream/lib/ActivityStreamPrefs.jsm");
const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
const {reducers} = Cu.import("resource://activity-stream/common/Reducers.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
@@ -30,15 +27,11 @@ this.Store = class Store {
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),
@@ -51,10 +44,14 @@ this.Store = class Store {
* 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);
}
}
};
}
@@ -65,7 +62,7 @@ this.Store = class Store {
* 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);
}
@@ -88,41 +85,34 @@ this.Store = class Store {
}
/**
* 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();
}
@@ -133,11 +123,10 @@ this.Store = class Store {
* @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._feedFactories = null;
this.feeds.clear();
this._feedFactories = null;
this._messageChannel.destroyChannel();
}
};

View File

@@ -6,40 +6,51 @@
"use strict";
const {interfaces: Ci, utils: Cu} = Components;
const {actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {perfService} = Cu.import("resource://activity-stream/common/PerfService.jsm", {});
Cu.import("resource://gre/modules/ClientID.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "ClientID",
"resource://gre/modules/ClientID.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "perfService",
"resource://activity-stream/common/PerfService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySender",
"resource://activity-stream/lib/TelemetrySender.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
"@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
*
@@ -112,10 +123,10 @@ this.TelemetryFeed = class TelemetryFeed {
* @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
};
@@ -131,34 +142,34 @@ this.TelemetryFeed = class TelemetryFeed {
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,
@@ -169,8 +180,8 @@ this.TelemetryFeed = class TelemetryFeed {
);
}
sendEvent(event) {
this.telemetrySender.sendPing(event);
async sendEvent(eventPromise) {
this.telemetrySender.sendPing(await eventPromise);
}
onAction(action) {
@@ -201,8 +212,10 @@ this.TelemetryFeed = class TelemetryFeed {
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
}
};

View File

@@ -3,11 +3,12 @@
* 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
@@ -71,7 +72,7 @@ TelemetrySender.prototype = {
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") {
@@ -81,7 +82,7 @@ TelemetrySender.prototype = {
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}`);
}

View File

@@ -4,58 +4,95 @@
"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");
Cu.import("resource://gre/modules/NewTabUtils.jsm");
Cu.import("resource:///modules/PreviewProvider.jsm");
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PreviewProvider",
"resource:///modules/PreviewProvider.jsm");
const TOP_SITES_SHOWMORE_LENGTH = 12;
const 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));
}
sortLinks(frecent, pinned) {
let sortedLinks = [...frecent, ...DEFAULT_TOP_SITES];
sortedLinks = sortedLinks.filter(link => !NewTabUtils.pinnedLinks.isPinned(link));
// Insert the pinned links in their specified location
pinned.forEach((val, index) => {
if (!val) { return; }
let link = Object.assign({}, val, {isPinned: true, pinIndex: index, pinTitle: val.title});
if (index > sortedLinks.length) {
sortedLinks[index] = link;
} else {
sortedLinks.splice(index, 0, link);
}
});
return sortedLinks.slice(0, TOP_SITES_SHOWMORE_LENGTH);
}
async getLinksWithDefaults(action) {
let links = await NewTabUtils.activityStreamLinks.getTopSites();
let pinned = NewTabUtils.pinnedLinks.links;
let frecent = await NewTabUtils.activityStreamLinks.getTopSites();
if (!links) {
links = [];
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;
@@ -64,15 +101,20 @@ this.TopSitesFeed = class TopSitesFeed {
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;

View File

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

View File

@@ -1,5 +1,5 @@
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", () => {
@@ -147,4 +147,30 @@ describe("Reducers", () => {
});
});
});
describe("Dialog", () => {
it("should return INITIAL_STATE by default", () => {
assert.equal(INITIAL_STATE.Dialog, Dialog(undefined, {type: "non_existent"}));
});
it("should toggle visible to true on DIALOG_OPEN", () => {
const action = {type: at.DIALOG_OPEN};
const nextState = Dialog(INITIAL_STATE.Dialog, action);
assert.isTrue(nextState.visible);
});
it("should pass url data on DIALOG_OPEN", () => {
const action = {type: at.DIALOG_OPEN, data: "some url"};
const nextState = Dialog(INITIAL_STATE.Dialog, action);
assert.equal(nextState.data, action.data);
});
it("should toggle visible to false on DIALOG_CANCEL", () => {
const action = {type: at.DIALOG_CANCEL, data: "some url"};
const nextState = Dialog(INITIAL_STATE.Dialog, action);
assert.isFalse(nextState.visible);
});
it("should return inital state on DELETE_HISTORY_URL", () => {
const action = {type: at.DELETE_HISTORY_URL};
const nextState = Dialog(INITIAL_STATE.Dialog, action);
assert.deepEqual(INITIAL_STATE.Dialog, nextState);
});
});
});

View File

@@ -83,27 +83,27 @@ describe("ActivityStream", () => {
});
describe("feeds", () => {
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);
});
});

View File

@@ -1,41 +1,94 @@
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);
@@ -48,14 +101,19 @@ describe("ActivityStreamPrefs", () => {
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);
}
});
});

View File

@@ -28,6 +28,16 @@ describe("PlacesFeed", () => {
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");

View File

@@ -1,18 +1,20 @@
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", () => {
@@ -25,27 +27,15 @@ describe("PrefsFeed", () => {
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", () => {
it("should add one branch observer 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));
});
assert.calledOnce(feed._prefs.observeBranch);
assert.calledWith(feed._prefs.observeBranch, feed);
});
it("should call onPrefChanged when an observer is called", () => {
sinon.stub(feed, "onPrefChanged");
feed.onAction({type: at.INIT});
const handlerForFoo = feed._observers.get("foo");
handlerForFoo(true);
assert.calledWith(feed.onPrefChanged, "foo", true);
});
it("should remove all observers on uninit", () => {
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);

View File

@@ -43,8 +43,8 @@ describe("Store", () => {
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");
@@ -52,7 +52,7 @@ describe("Store", () => {
});
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);
@@ -70,7 +70,7 @@ describe("Store", () => {
feed = {uninit: sinon.spy()};
return feed;
}
store._feedFactories = {foo: createFeed};
store._feedFactories = new Map([["foo", createFeed]]);
store.initFeed("foo");
store.uninitFeed("foo");
@@ -79,7 +79,7 @@ describe("Store", () => {
});
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");
@@ -87,69 +87,70 @@ describe("Store", () => {
assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
});
});
describe("maybeStartFeedAndListenForPrefChanges", () => {
describe("onPrefChanged", () => {
beforeEach(() => {
sinon.stub(store, "initFeed");
sinon.stub(store, "uninitFeed");
});
it("should initialize the feed if the Pref is set to true", () => {
store._prefs.set("foo", true);
store.maybeStartFeedAndListenForPrefChanges("foo");
assert.calledWith(store.initFeed, "foo");
});
it("should not initialize the feed if the Pref is set to false", () => {
store._prefs.set("foo", false);
store.maybeStartFeedAndListenForPrefChanges("foo");
store.init(new Map([["foo", () => ({})]]));
});
it("should initialize the feed if called with true", () => {
store.onPrefChanged("foo", true);
assert.calledWith(store.initFeed, "foo");
assert.notCalled(store.uninitFeed);
});
it("should uninitialize the feed if called with false", () => {
store.onPrefChanged("foo", false);
assert.calledWith(store.uninitFeed, "foo");
assert.notCalled(store.initFeed);
});
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", () => {
@@ -171,7 +172,7 @@ describe("Store", () => {
const action = {type: "FOO"};
store._prefs.set("sub", true);
store.init({sub: () => sub});
store.init(new Map([["sub", () => sub]]));
dispatch(action);

View File

@@ -47,20 +47,16 @@ describe("TelemetryFeed", () => {
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,
@@ -130,19 +126,19 @@ describe("TelemetryFeed", () => {
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
@@ -151,12 +147,12 @@ describe("TelemetryFeed", () => {
});
});
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);
@@ -165,21 +161,21 @@ describe("TelemetryFeed", () => {
});
});
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);
@@ -190,21 +186,21 @@ describe("TelemetryFeed", () => {
});
});
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);
@@ -215,8 +211,8 @@ describe("TelemetryFeed", () => {
});
});
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,
@@ -236,20 +232,17 @@ describe("TelemetryFeed", () => {
});
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();

View File

@@ -165,7 +165,7 @@ describe("TelemetrySender", () => {
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 () => {

View File

@@ -1,23 +1,36 @@
"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;
@@ -28,11 +41,74 @@ describe("Top Sites Feed", () => {
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);
@@ -64,11 +140,21 @@ describe("Top Sites Feed", () => {
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", () => {

View File

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

View File

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