Bug 1386314 - Add default icons, enable snippets and bug fixes to Activity Stream. r=dmose
MozReview-Commit-ID: 9Pty4jAV6te --HG-- extra : rebase_source : 62ea46ef7f892199dbaac8a333ce1bc8bd41ec22
|
@ -101,7 +101,7 @@ function onBrowserReady() {
|
|||
waitingForBrowserReady = false;
|
||||
|
||||
// Listen for changes to the pref that enables Activity Stream
|
||||
Services.prefs.addObserver(ACTIVITY_STREAM_ENABLED_PREF, observe);
|
||||
Services.prefs.addObserver(ACTIVITY_STREAM_ENABLED_PREF, observe); // eslint-disable-line no-use-before-define
|
||||
|
||||
// Only initialize if the pref is true
|
||||
if (Services.prefs.getBoolPref(ACTIVITY_STREAM_ENABLED_PREF, false)) {
|
||||
|
@ -120,7 +120,7 @@ function observe(subject, topic, data) {
|
|||
Services.tm.dispatchToMainThread(() => onBrowserReady());
|
||||
break;
|
||||
case PREF_CHANGED_TOPIC:
|
||||
if (data == ACTIVITY_STREAM_ENABLED_PREF) {
|
||||
if (data === ACTIVITY_STREAM_ENABLED_PREF) {
|
||||
onPrefChanged();
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
this.Dedupe = class Dedupe {
|
||||
constructor(createKey, compare) {
|
||||
this.createKey = createKey || this.defaultCreateKey;
|
||||
this.compare = compare || this.defaultCompare;
|
||||
}
|
||||
|
||||
defaultCreateKey(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
defaultCompare() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedupe an array containing groups of elements.
|
||||
* Duplicate removal favors earlier groups.
|
||||
*
|
||||
* @param {Array} groups Contains an arbitrary number of arrays of elements.
|
||||
* @returns {Array}
|
||||
*/
|
||||
group(groups) {
|
||||
const globalKeys = new Set();
|
||||
const result = [];
|
||||
for (const values of groups) {
|
||||
const valueMap = new Map();
|
||||
for (const value of values) {
|
||||
const key = this.createKey(value);
|
||||
if (!globalKeys.has(key) && (!valueMap.has(key) || this.compare(valueMap.get(key), value))) {
|
||||
valueMap.set(key, value);
|
||||
}
|
||||
}
|
||||
result.push(valueMap);
|
||||
valueMap.forEach((value, key) => globalKeys.add(key));
|
||||
}
|
||||
return result.map(m => Array.from(m.values()));
|
||||
}
|
||||
};
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Dedupe"];
|
|
@ -37,7 +37,7 @@ const INITIAL_STATE = {
|
|||
function App(prevState = INITIAL_STATE.App, action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
return Object.assign({}, action.data || {}, {initialized: true});
|
||||
return Object.assign({}, prevState, action.data || {}, {initialized: true});
|
||||
case at.LOCALE_UPDATED: {
|
||||
if (!action.data) {
|
||||
return prevState;
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
Components.utils.importGlobalProperties(["URL"]);
|
||||
|
||||
/**
|
||||
* shortURL - Creates a short version of a link's url, used for display purposes
|
||||
* e.g. {url: http://www.foosite.com, eTLD: "com"} => "foosite"
|
||||
*
|
||||
* @param {obj} link A link object
|
||||
* {str} link.url (required)- The url of the link
|
||||
* {str} link.eTLD (required) - The tld of the link
|
||||
* e.g. for https://foo.org, the tld would be "org"
|
||||
* Note that this property is added in various queries for ActivityStream
|
||||
* via Services.eTLD.getPublicSuffix
|
||||
* {str} link.hostname (optional) - The hostname of the url
|
||||
* e.g. for http://www.hello.com/foo/bar, the hostname would be "www.hello.com"
|
||||
* {str} link.title (optional) - The title of the link
|
||||
* @return {str} A short url
|
||||
*/
|
||||
this.shortURL = function shortURL(link) {
|
||||
if (!link.url && !link.hostname) {
|
||||
return "";
|
||||
}
|
||||
const {eTLD} = link;
|
||||
const hostname = (link.hostname || new URL(link.url).hostname).replace(/^www\./i, "");
|
||||
|
||||
// Remove the eTLD (e.g., com, net) and the preceding period from the hostname
|
||||
const eTLDLength = (eTLD || "").length || (hostname.match(/\.com$/) && 3);
|
||||
const eTLDExtra = eTLDLength > 0 ? -(eTLDLength + 1) : Infinity;
|
||||
// If URL and hostname are not present fallback to page title.
|
||||
return hostname.slice(0, eTLDExtra).toLowerCase() || hostname || link.title || link.url;
|
||||
};
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["shortURL"];
|
|
@ -63,7 +63,7 @@
|
|||
/******/ __webpack_require__.p = "";
|
||||
/******/
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(__webpack_require__.s = 26);
|
||||
/******/ return __webpack_require__(__webpack_require__.s = 25);
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ([
|
||||
|
@ -300,56 +300,19 @@ module.exports = ReactRedux;
|
|||
"use strict";
|
||||
|
||||
|
||||
/**
|
||||
* shortURL - Creates a short version of a link's url, used for display purposes
|
||||
* e.g. {url: http://www.foosite.com, eTLD: "com"} => "foosite"
|
||||
*
|
||||
* @param {obj} link A link object
|
||||
* {str} link.url (required)- The url of the link
|
||||
* {str} link.eTLD (required) - The tld of the link
|
||||
* e.g. for https://foo.org, the tld would be "org"
|
||||
* Note that this property is added in various queries for ActivityStream
|
||||
* via Services.eTLD.getPublicSuffix
|
||||
* {str} link.hostname (optional) - The hostname of the url
|
||||
* e.g. for http://www.hello.com/foo/bar, the hostname would be "www.hello.com"
|
||||
* {str} link.title (optional) - The title of the link
|
||||
* @return {str} A short url
|
||||
*/
|
||||
module.exports = function shortURL(link) {
|
||||
if (!link.url && !link.hostname) {
|
||||
return "";
|
||||
}
|
||||
const eTLD = link.eTLD;
|
||||
|
||||
const hostname = (link.hostname || new URL(link.url).hostname).replace(/^www\./i, "");
|
||||
|
||||
// Remove the eTLD (e.g., com, net) and the preceding period from the hostname
|
||||
const eTLDLength = (eTLD || "").length || hostname.match(/\.com$/) && 3;
|
||||
const eTLDExtra = eTLDLength > 0 ? -(eTLDLength + 1) : Infinity;
|
||||
// If URL and hostname are not present fallback to page title.
|
||||
return hostname.slice(0, eTLDExtra).toLowerCase() || hostname || link.title || link.url;
|
||||
};
|
||||
|
||||
/***/ }),
|
||||
/* 5 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
const React = __webpack_require__(0);
|
||||
|
||||
var _require = __webpack_require__(2);
|
||||
|
||||
const injectIntl = _require.injectIntl;
|
||||
|
||||
const ContextMenu = __webpack_require__(16);
|
||||
const ContextMenu = __webpack_require__(15);
|
||||
|
||||
var _require2 = __webpack_require__(1);
|
||||
|
||||
const ac = _require2.actionCreators;
|
||||
|
||||
const linkMenuOptions = __webpack_require__(23);
|
||||
const linkMenuOptions = __webpack_require__(22);
|
||||
const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow"];
|
||||
|
||||
class LinkMenu extends React.Component {
|
||||
|
@ -404,7 +367,7 @@ module.exports = injectIntl(LinkMenu);
|
|||
module.exports._unconnected = LinkMenu;
|
||||
|
||||
/***/ }),
|
||||
/* 6 */
|
||||
/* 5 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -527,7 +490,7 @@ module.exports = {
|
|||
};
|
||||
|
||||
/***/ }),
|
||||
/* 7 */
|
||||
/* 6 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -544,12 +507,12 @@ var _require2 = __webpack_require__(2);
|
|||
const addLocaleData = _require2.addLocaleData,
|
||||
IntlProvider = _require2.IntlProvider;
|
||||
|
||||
const TopSites = __webpack_require__(21);
|
||||
const Search = __webpack_require__(19);
|
||||
const ConfirmDialog = __webpack_require__(15);
|
||||
const ManualMigration = __webpack_require__(17);
|
||||
const PreferencesPane = __webpack_require__(18);
|
||||
const Sections = __webpack_require__(20);
|
||||
const TopSites = __webpack_require__(20);
|
||||
const Search = __webpack_require__(18);
|
||||
const ConfirmDialog = __webpack_require__(14);
|
||||
const ManualMigration = __webpack_require__(16);
|
||||
const PreferencesPane = __webpack_require__(17);
|
||||
const Sections = __webpack_require__(19);
|
||||
|
||||
// Locales that should be displayed RTL
|
||||
const RTL_LIST = ["ar", "he", "fa", "ur"];
|
||||
|
@ -573,7 +536,8 @@ class Base extends React.Component {
|
|||
componentWillUpdate(_ref2) {
|
||||
let App = _ref2.App;
|
||||
|
||||
if (App.locale !== this.props.App.locale) {
|
||||
// Early loads might not have locale yet, so wait until we do
|
||||
if (App.locale && App.locale !== this.props.App.locale) {
|
||||
addLocaleDataForReactIntl(App);
|
||||
this.updateTitle(App);
|
||||
}
|
||||
|
@ -621,7 +585,7 @@ class Base extends React.Component {
|
|||
module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
|
||||
|
||||
/***/ }),
|
||||
/* 8 */
|
||||
/* 7 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -631,7 +595,7 @@ var _require = __webpack_require__(1);
|
|||
|
||||
const at = _require.actionTypes;
|
||||
|
||||
var _require2 = __webpack_require__(6);
|
||||
var _require2 = __webpack_require__(5);
|
||||
|
||||
const perfSvc = _require2.perfService;
|
||||
|
||||
|
@ -701,7 +665,7 @@ module.exports = class DetectUserSessionStart {
|
|||
};
|
||||
|
||||
/***/ }),
|
||||
/* 9 */
|
||||
/* 8 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -709,7 +673,7 @@ module.exports = class DetectUserSessionStart {
|
|||
|
||||
/* eslint-env mozilla/frame-script */
|
||||
|
||||
var _require = __webpack_require__(25);
|
||||
var _require = __webpack_require__(24);
|
||||
|
||||
const createStore = _require.createStore,
|
||||
combineReducers = _require.combineReducers,
|
||||
|
@ -786,7 +750,7 @@ module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME;
|
|||
module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
|
||||
|
||||
/***/ }),
|
||||
/* 10 */
|
||||
/* 9 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -1097,10 +1061,10 @@ module.exports = {
|
|||
SnippetsProvider,
|
||||
SNIPPETS_UPDATE_INTERVAL_MS
|
||||
};
|
||||
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(24)))
|
||||
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(23)))
|
||||
|
||||
/***/ }),
|
||||
/* 11 */
|
||||
/* 10 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -1149,7 +1113,7 @@ function App() {
|
|||
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
return Object.assign({}, action.data || {}, { initialized: true });
|
||||
return Object.assign({}, prevState, action.data || {}, { initialized: true });
|
||||
case at.LOCALE_UPDATED:
|
||||
{
|
||||
if (!action.data) {
|
||||
|
@ -1401,27 +1365,26 @@ module.exports = {
|
|||
};
|
||||
|
||||
/***/ }),
|
||||
/* 12 */
|
||||
/* 11 */
|
||||
/***/ (function(module, exports) {
|
||||
|
||||
module.exports = ReactDOM;
|
||||
|
||||
/***/ }),
|
||||
/* 13 */
|
||||
/* 12 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
const React = __webpack_require__(0);
|
||||
const LinkMenu = __webpack_require__(5);
|
||||
const shortURL = __webpack_require__(4);
|
||||
const LinkMenu = __webpack_require__(4);
|
||||
|
||||
var _require = __webpack_require__(2);
|
||||
|
||||
const FormattedMessage = _require.FormattedMessage;
|
||||
|
||||
const cardContextTypes = __webpack_require__(14);
|
||||
const cardContextTypes = __webpack_require__(13);
|
||||
|
||||
var _require2 = __webpack_require__(1);
|
||||
|
||||
|
@ -1483,7 +1446,6 @@ class Card extends React.Component {
|
|||
eventSource = _props.eventSource;
|
||||
|
||||
const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
|
||||
const hostname = shortURL(link);
|
||||
var _cardContextTypes$lin = cardContextTypes[link.type];
|
||||
const icon = _cardContextTypes$lin.icon,
|
||||
intlID = _cardContextTypes$lin.intlID;
|
||||
|
@ -1506,7 +1468,7 @@ class Card extends React.Component {
|
|||
"div",
|
||||
{ className: "card-host-name" },
|
||||
" ",
|
||||
hostname,
|
||||
link.hostname,
|
||||
" "
|
||||
),
|
||||
React.createElement(
|
||||
|
@ -1564,7 +1526,7 @@ class Card extends React.Component {
|
|||
module.exports = Card;
|
||||
|
||||
/***/ }),
|
||||
/* 14 */
|
||||
/* 13 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -1590,7 +1552,7 @@ module.exports = {
|
|||
};
|
||||
|
||||
/***/ }),
|
||||
/* 15 */
|
||||
/* 14 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -1709,7 +1671,7 @@ module.exports._unconnected = ConfirmDialog;
|
|||
module.exports.Dialog = ConfirmDialog;
|
||||
|
||||
/***/ }),
|
||||
/* 16 */
|
||||
/* 15 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -1803,7 +1765,7 @@ module.exports.ContextMenu = ContextMenu;
|
|||
module.exports.ContextMenuItem = ContextMenuItem;
|
||||
|
||||
/***/ }),
|
||||
/* 17 */
|
||||
/* 16 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -1881,7 +1843,7 @@ module.exports = connect()(ManualMigration);
|
|||
module.exports._unconnected = ManualMigration;
|
||||
|
||||
/***/ }),
|
||||
/* 18 */
|
||||
/* 17 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -1928,10 +1890,13 @@ class PreferencesPane extends React.Component {
|
|||
this.togglePane = this.togglePane.bind(this);
|
||||
|
||||
// TODO This is temporary until sections register their PreferenceInput component automatically
|
||||
try {
|
||||
this.topStoriesOptions = JSON.parse(props.Prefs.values["feeds.section.topstories.options"]);
|
||||
} catch (e) {
|
||||
console.error("Problem parsing feeds.section.topstories.options", e); // eslint-disable-line no-console
|
||||
const optionJSON = props.Prefs.values["feeds.section.topstories.options"];
|
||||
if (optionJSON) {
|
||||
try {
|
||||
this.topStoriesOptions = JSON.parse(optionJSON);
|
||||
} catch (e) {
|
||||
console.error("Problem parsing feeds.section.topstories.options", e); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
|
@ -2018,7 +1983,7 @@ module.exports.PreferencesPane = PreferencesPane;
|
|||
module.exports.PreferencesInput = PreferencesInput;
|
||||
|
||||
/***/ }),
|
||||
/* 19 */
|
||||
/* 18 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -2117,7 +2082,7 @@ module.exports = connect()(injectIntl(Search));
|
|||
module.exports._unconnected = Search;
|
||||
|
||||
/***/ }),
|
||||
/* 20 */
|
||||
/* 19 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -2133,12 +2098,32 @@ const connect = _require.connect;
|
|||
|
||||
var _require2 = __webpack_require__(2);
|
||||
|
||||
const FormattedMessage = _require2.FormattedMessage;
|
||||
const injectIntl = _require2.injectIntl,
|
||||
FormattedMessage = _require2.FormattedMessage;
|
||||
|
||||
const Card = __webpack_require__(13);
|
||||
const Topics = __webpack_require__(22);
|
||||
const Card = __webpack_require__(12);
|
||||
const Topics = __webpack_require__(21);
|
||||
|
||||
class Section extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onInfoEnter = this.onInfoEnter.bind(this);
|
||||
this.onInfoLeave = this.onInfoLeave.bind(this);
|
||||
this.state = { infoActive: false };
|
||||
}
|
||||
|
||||
onInfoEnter() {
|
||||
this.setState({ infoActive: true });
|
||||
}
|
||||
|
||||
onInfoLeave(event) {
|
||||
// If we have a related target, check to see if it is within the current
|
||||
// target (section-info-option) to keep infoActive true. False otherwise.
|
||||
this.setState({
|
||||
infoActive: event && event.relatedTarget && event.relatedTarget.compareDocumentPosition(event.currentTarget) & Node.DOCUMENT_POSITION_CONTAINS
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
var _props = this.props;
|
||||
const id = _props.id,
|
||||
|
@ -2150,10 +2135,22 @@ class Section extends React.Component {
|
|||
emptyState = _props.emptyState,
|
||||
dispatch = _props.dispatch,
|
||||
maxCards = _props.maxCards,
|
||||
contextMenuOptions = _props.contextMenuOptions;
|
||||
contextMenuOptions = _props.contextMenuOptions,
|
||||
intl = _props.intl;
|
||||
|
||||
const initialized = rows && rows.length > 0;
|
||||
const shouldShowTopics = id === "TopStories" && this.props.topics && this.props.read_more_endpoint;
|
||||
|
||||
const infoOptionIconA11yAttrs = {
|
||||
"aria-haspopup": "true",
|
||||
"aria-controls": "info-option",
|
||||
"aria-expanded": this.state.infoActive ? "true" : "false",
|
||||
"role": "note",
|
||||
"tabIndex": 0
|
||||
};
|
||||
|
||||
const sectionInfoTitle = intl.formatMessage({ id: "section_info_option" });
|
||||
|
||||
// <Section> <-- React component
|
||||
// <section> <-- HTML5 element
|
||||
return React.createElement(
|
||||
|
@ -2170,19 +2167,19 @@ class Section extends React.Component {
|
|||
),
|
||||
infoOption && React.createElement(
|
||||
"span",
|
||||
{ className: "section-info-option" },
|
||||
React.createElement(
|
||||
"span",
|
||||
{ className: "sr-only" },
|
||||
React.createElement(FormattedMessage, { id: "section_info_option" })
|
||||
),
|
||||
React.createElement("img", { className: "info-option-icon" }),
|
||||
{ className: "section-info-option",
|
||||
onBlur: this.onInfoLeave,
|
||||
onFocus: this.onInfoEnter,
|
||||
onMouseOut: this.onInfoLeave,
|
||||
onMouseOver: this.onInfoEnter },
|
||||
React.createElement("img", _extends({ className: "info-option-icon", title: sectionInfoTitle
|
||||
}, infoOptionIconA11yAttrs)),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "info-option" },
|
||||
infoOption.header && React.createElement(
|
||||
"div",
|
||||
{ className: "info-option-header" },
|
||||
{ className: "info-option-header", role: "heading" },
|
||||
React.createElement(FormattedMessage, infoOption.header)
|
||||
),
|
||||
infoOption.body && React.createElement(
|
||||
|
@ -2222,23 +2219,26 @@ class Section extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const SectionIntl = injectIntl(Section);
|
||||
|
||||
class Sections extends React.Component {
|
||||
render() {
|
||||
const sections = this.props.Sections;
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ className: "sections-list" },
|
||||
sections.map(section => React.createElement(Section, _extends({ key: section.id }, section, { dispatch: this.props.dispatch })))
|
||||
sections.map(section => React.createElement(SectionIntl, _extends({ key: section.id }, section, { dispatch: this.props.dispatch })))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(state => ({ Sections: state.Sections }))(Sections);
|
||||
module.exports._unconnected = Sections;
|
||||
module.exports.Section = Section;
|
||||
module.exports.SectionIntl = SectionIntl;
|
||||
module.exports._unconnectedSection = Section;
|
||||
|
||||
/***/ }),
|
||||
/* 21 */
|
||||
/* 20 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -2254,15 +2254,14 @@ var _require2 = __webpack_require__(2);
|
|||
|
||||
const FormattedMessage = _require2.FormattedMessage;
|
||||
|
||||
const shortURL = __webpack_require__(4);
|
||||
const LinkMenu = __webpack_require__(5);
|
||||
const LinkMenu = __webpack_require__(4);
|
||||
|
||||
var _require3 = __webpack_require__(1);
|
||||
|
||||
const ac = _require3.actionCreators,
|
||||
at = _require3.actionTypes;
|
||||
|
||||
var _require4 = __webpack_require__(6);
|
||||
var _require4 = __webpack_require__(5);
|
||||
|
||||
const perfSvc = _require4.perfService;
|
||||
|
||||
|
@ -2304,10 +2303,22 @@ class TopSite extends React.Component {
|
|||
dispatch = _props.dispatch;
|
||||
|
||||
const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index;
|
||||
const title = link.pinTitle || shortURL(link);
|
||||
const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`;
|
||||
const title = link.pinTitle || link.hostname;
|
||||
const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`;
|
||||
const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
|
||||
const tippyTopIcon = link.tippyTopIcon;
|
||||
|
||||
let imageClassName;
|
||||
let imageStyle;
|
||||
if (tippyTopIcon) {
|
||||
imageClassName = "tippy-top-icon";
|
||||
imageStyle = {
|
||||
backgroundColor: link.backgroundColor,
|
||||
backgroundImage: `url(${tippyTopIcon})`
|
||||
};
|
||||
} else {
|
||||
imageClassName = `screenshot${link.screenshot ? " active" : ""}`;
|
||||
imageStyle = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
|
||||
}
|
||||
return React.createElement(
|
||||
"li",
|
||||
{ className: topSiteOuterClassName, key: link.guid || link.url },
|
||||
|
@ -2322,7 +2333,7 @@ class TopSite extends React.Component {
|
|||
{ className: "letter-fallback" },
|
||||
title[0]
|
||||
),
|
||||
React.createElement("div", { className: screenshotClassName, style: style })
|
||||
React.createElement("div", { className: imageClassName, style: imageStyle })
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
|
@ -2473,7 +2484,7 @@ module.exports.TopSite = TopSite;
|
|||
module.exports.TopSites = TopSites;
|
||||
|
||||
/***/ }),
|
||||
/* 22 */
|
||||
/* 21 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -2538,7 +2549,7 @@ module.exports._unconnected = Topics;
|
|||
module.exports.Topic = Topic;
|
||||
|
||||
/***/ }),
|
||||
/* 23 */
|
||||
/* 22 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -2549,13 +2560,12 @@ var _require = __webpack_require__(1);
|
|||
const at = _require.actionTypes,
|
||||
ac = _require.actionCreators;
|
||||
|
||||
const shortURL = __webpack_require__(4);
|
||||
|
||||
/**
|
||||
* List of functions that return items that can be included as menu options in a
|
||||
* LinkMenu. All functions take the site as the first parameter, and optionally
|
||||
* the index of the site.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
Separator: () => ({ type: "separator" }),
|
||||
RemoveBookmark: site => ({
|
||||
|
@ -2621,7 +2631,7 @@ module.exports = {
|
|||
icon: "pin",
|
||||
action: ac.SendToMain({
|
||||
type: at.TOP_SITES_PIN,
|
||||
data: { site: { url: site.url, title: shortURL(site) }, index }
|
||||
data: { site: { url: site.url, title: site.hostname }, index }
|
||||
}),
|
||||
userEvent: "PIN"
|
||||
}),
|
||||
|
@ -2649,7 +2659,7 @@ module.exports.CheckBookmark = site => site.bookmarkGuid ? module.exports.Remove
|
|||
module.exports.CheckPinTopSite = (site, index) => site.isPinned ? module.exports.UnpinTopSite(site) : module.exports.PinTopSite(site, index);
|
||||
|
||||
/***/ }),
|
||||
/* 24 */
|
||||
/* 23 */
|
||||
/***/ (function(module, exports) {
|
||||
|
||||
var g;
|
||||
|
@ -2676,35 +2686,35 @@ module.exports = g;
|
|||
|
||||
|
||||
/***/ }),
|
||||
/* 25 */
|
||||
/* 24 */
|
||||
/***/ (function(module, exports) {
|
||||
|
||||
module.exports = Redux;
|
||||
|
||||
/***/ }),
|
||||
/* 26 */
|
||||
/* 25 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
const React = __webpack_require__(0);
|
||||
const ReactDOM = __webpack_require__(12);
|
||||
const Base = __webpack_require__(7);
|
||||
const ReactDOM = __webpack_require__(11);
|
||||
const Base = __webpack_require__(6);
|
||||
|
||||
var _require = __webpack_require__(3);
|
||||
|
||||
const Provider = _require.Provider;
|
||||
|
||||
const initStore = __webpack_require__(9);
|
||||
const initStore = __webpack_require__(8);
|
||||
|
||||
var _require2 = __webpack_require__(11);
|
||||
var _require2 = __webpack_require__(10);
|
||||
|
||||
const reducers = _require2.reducers;
|
||||
|
||||
const DetectUserSessionStart = __webpack_require__(8);
|
||||
const DetectUserSessionStart = __webpack_require__(7);
|
||||
|
||||
var _require3 = __webpack_require__(10);
|
||||
var _require3 = __webpack_require__(9);
|
||||
|
||||
const addSnippetsSubscriber = _require3.addSnippetsSubscriber;
|
||||
|
||||
|
|
|
@ -297,6 +297,17 @@ main {
|
|||
opacity: 0; }
|
||||
.top-sites-list .top-site-outer .screenshot.active {
|
||||
opacity: 1; }
|
||||
.top-sites-list .top-site-outer .tippy-top-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
background-position: center center;
|
||||
background-size: 80px;
|
||||
background-repeat: no-repeat; }
|
||||
.top-sites-list .top-site-outer .title {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
@ -332,12 +343,12 @@ main {
|
|||
height: 16px;
|
||||
width: 16px;
|
||||
display: inline-block; }
|
||||
.sections-list .section-top-bar .section-info-option div {
|
||||
.sections-list .section-top-bar .section-info-option .info-option {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0.2s, opacity 0.2s ease-out;
|
||||
transition-delay: 0.5s; }
|
||||
.sections-list .section-top-bar .section-info-option:hover div {
|
||||
.sections-list .section-top-bar .info-option-icon[aria-expanded="true"] + .info-option {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: visibility 0.2s, opacity 0.2s ease-out; }
|
||||
|
@ -421,7 +432,7 @@ main {
|
|||
text-align: center; }
|
||||
|
||||
.topic {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: #BFC0C7;
|
||||
margin-top: 16px;
|
||||
line-height: 1.6; }
|
||||
|
@ -460,7 +471,8 @@ main {
|
|||
margin-left: 5px;
|
||||
background-image: url("assets/topic-show-more-12.svg");
|
||||
background-repeat: no-repeat;
|
||||
vertical-align: middle; }
|
||||
vertical-align: middle;
|
||||
background-position-y: 1px; }
|
||||
|
||||
.search-wrapper {
|
||||
cursor: default;
|
||||
|
|
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/aliexpress-com.png
Normal file
После Ширина: | Высота: | Размер: 2.5 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/allegro-pl.png
Normal file
После Ширина: | Высота: | Размер: 3.6 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/amazon-com.png
Normal file
После Ширина: | Высота: | Размер: 3.4 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/avito-ru.png
Normal file
После Ширина: | Высота: | Размер: 12 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/bbc-com.png
Normal file
После Ширина: | Высота: | Размер: 1.7 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/ebay-com.png
Normal file
После Ширина: | Высота: | Размер: 1.3 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/facebook-com.png
Normal file
После Ширина: | Высота: | Размер: 1.0 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/leboncoin-fr.png
Normal file
После Ширина: | Высота: | Размер: 2.7 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/ok-ru.png
Normal file
После Ширина: | Высота: | Размер: 3.0 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/olx-pl.png
Normal file
После Ширина: | Высота: | Размер: 6.8 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/reddit-com.png
Normal file
После Ширина: | Высота: | Размер: 4.9 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/twitter-com.png
Normal file
После Ширина: | Высота: | Размер: 2.4 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/vk-com.png
Normal file
После Ширина: | Высота: | Размер: 2.5 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/wikipedia-org.png
Normal file
После Ширина: | Высота: | Размер: 2.9 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/wykop-pl.png
Normal file
После Ширина: | Высота: | Размер: 3.0 KiB |
Двоичные данные
browser/extensions/activity-stream/data/content/tippytop/images/youtube-com.png
Normal file
После Ширина: | Высота: | Размер: 1.7 KiB |
|
@ -0,0 +1,114 @@
|
|||
[
|
||||
{
|
||||
"title": "aliexpress",
|
||||
"url": "https://www.aliexpress.com/",
|
||||
"image_url": "aliexpress-com.png",
|
||||
"background_color": "#f6050a",
|
||||
"domain": "aliexpress.com"
|
||||
},
|
||||
{
|
||||
"title": "allegro",
|
||||
"url": "https://www.allegro.pl/",
|
||||
"image_url": "allegro-pl.png",
|
||||
"background_color": "#ff4900",
|
||||
"domain": "allegro.pl"
|
||||
},
|
||||
{
|
||||
"title": "amazon",
|
||||
"urls": ["https://www.amazon.com/", "https://www.amazon.ca/", "https://www.amazon.de/", "https://www.amazon.co.uk/", "https://www.amazon.fr/"],
|
||||
"image_url": "amazon-com.png",
|
||||
"background_color": "#FFF",
|
||||
"domain": "amazon.com"
|
||||
},
|
||||
{
|
||||
"title": "avito",
|
||||
"url": "https://www.avito.ru/",
|
||||
"image_url": "avito-ru.png",
|
||||
"background_color": "#FFFFFF",
|
||||
"domain": "avito.ru"
|
||||
},
|
||||
{
|
||||
"title": "bbc",
|
||||
"urls": ["http://www.bbc.com/", "https://www.bbc.co.uk/"],
|
||||
"image_url": "bbc-com.png",
|
||||
"background_color": "#000000",
|
||||
"domain": "bbc.com"
|
||||
},
|
||||
{
|
||||
"title": "ebay",
|
||||
"urls": ["https://www.ebay.com", "https://ebay.de", "https://www.ebay.co.uk/"],
|
||||
"image_url": "ebay-com.png",
|
||||
"background_color": "#ededed",
|
||||
"domain": "ebay.com"
|
||||
},
|
||||
{
|
||||
"title": "facebook",
|
||||
"url": "https://www.facebook.com/",
|
||||
"image_url": "facebook-com.png",
|
||||
"background_color": "#3b5998",
|
||||
"domain": "facebook.com"
|
||||
},
|
||||
{
|
||||
"title": "leboncoin",
|
||||
"url": "http://www.leboncoin.fr/",
|
||||
"image_url": "leboncoin-fr.png",
|
||||
"background_color": "#ff6000",
|
||||
"domain": "leboncoin.fr"
|
||||
},
|
||||
{
|
||||
"title": "ok",
|
||||
"url": "https://www.ok.ru/",
|
||||
"image_url": "ok-ru.png",
|
||||
"background_color": "#fb7b00",
|
||||
"domain": "ok.ru"
|
||||
},
|
||||
{
|
||||
"title": "olx",
|
||||
"url": "https://www.olx.pl/",
|
||||
"image_url": "olx-pl.png",
|
||||
"background_color": "#b7c200",
|
||||
"domain": "olx.pl"
|
||||
},
|
||||
{
|
||||
"title": "reddit",
|
||||
"url": "https://www.reddit.com/",
|
||||
"image_url": "reddit-com.png",
|
||||
"background_color": "#cee3f8",
|
||||
"domain": "reddit.com"
|
||||
},
|
||||
{
|
||||
"title": "twitter",
|
||||
"url": "https://twitter.com/",
|
||||
"image_url": "twitter-com.png",
|
||||
"background_color": "#049ff5",
|
||||
"domain": "twitter.com"
|
||||
},
|
||||
{
|
||||
"title": "vk",
|
||||
"url": "https://vk.com/",
|
||||
"image_url": "vk-com.png",
|
||||
"background_color": "#4483be",
|
||||
"domain": "vk.com"
|
||||
},
|
||||
{
|
||||
"title": "youtube",
|
||||
"url": "https://www.youtube.com/",
|
||||
"image_url": "youtube-com.png",
|
||||
"background_color": "#db4338",
|
||||
"domain": "youtube.com"
|
||||
},
|
||||
{
|
||||
"title": "wikipedia",
|
||||
"url": "https://www.wikipedia.org/",
|
||||
"image_url": "wikipedia-org.png",
|
||||
"background_color": "#fff",
|
||||
"domain": "wikipedia.org"
|
||||
},
|
||||
{
|
||||
"title": "wykop",
|
||||
"url": "https://www.wykop.pl/",
|
||||
"image_url": "wykop-pl.png",
|
||||
"background_color": "#157ead",
|
||||
"domain": "wykop.pl"
|
||||
}
|
||||
]
|
|
@ -1756,6 +1756,7 @@
|
|||
"header_stories": "برترین داستانها",
|
||||
"header_visit_again": "مشاهده دوباره",
|
||||
"header_bookmarks": "نشانکهای اخیر",
|
||||
"header_recommended_by": "پیشنهاد شده توسط {provider}",
|
||||
"header_bookmarks_placeholder": "هنوز هیچ نشانکی ندارید.",
|
||||
"header_stories_from": "از",
|
||||
"type_label_visited": "مشاهده شده",
|
||||
|
@ -1764,6 +1765,7 @@
|
|||
"type_label_recommended": "موضوعات داغ",
|
||||
"type_label_open": "باز کردن",
|
||||
"type_label_topic": "موضوع",
|
||||
"type_label_now": "هماکنون",
|
||||
"menu_action_bookmark": "نشانک",
|
||||
"menu_action_remove_bookmark": "حذف نشانک",
|
||||
"menu_action_copy_address": "رونوشت از آدرس",
|
||||
|
@ -1782,6 +1784,7 @@
|
|||
"search_header": "جستوجو {search_engine_name}",
|
||||
"search_web_placeholder": "جستوجوی وب",
|
||||
"search_settings": "تغییر تنظیمات جستوجو",
|
||||
"section_info_option": "اطلاعات",
|
||||
"welcome_title": "به زبانه جدید خوشآمدید",
|
||||
"welcome_body": "فایرفاکس از این فضا برای نمایش نشانکها، مقالات، ویدئوها و صفحات مرتبطی که بهتازگی مشاهده کردهاید استفاده میکند، تا شما به راحتی دوباره به آنها دسترسی داشته باشید.",
|
||||
"welcome_label": "شناسایی گزینههای برجسته شما",
|
||||
|
@ -1826,7 +1829,10 @@
|
|||
"pocket_read_even_more": "مشاهده داستانهای بیشتر",
|
||||
"pocket_feedback_header": "بهترینهای وب، گزینش شده توسط بیش از ۲۵ میلیون نفر.",
|
||||
"pocket_feedback_body": "Pocket، بخشی از خانواده موزیلا، کمک خواهد کرد تا به محتوایی با کیفیت بالا مرتبط شوید که در غیر این صورت ممکن بود پیدا نکنید.",
|
||||
"pocket_send_feedback": "ارسال بازخورد"
|
||||
"pocket_send_feedback": "ارسال بازخورد",
|
||||
"manual_migration_explanation": "فایرفاکس را با سایتهای مورد علاقه و نشانکهای خود در سایر مرورگرها امتحان کنید.",
|
||||
"manual_migration_cancel_button": "نه ممنون",
|
||||
"manual_migration_import_button": "هماکنون وارد شوند"
|
||||
},
|
||||
"ff": {},
|
||||
"fi": {
|
||||
|
@ -3901,18 +3907,18 @@
|
|||
"nn-NO": {
|
||||
"newtab_page_title": "Ny fane",
|
||||
"default_label_loading": "Lastar…",
|
||||
"header_top_sites": "Mest vitja",
|
||||
"header_top_sites": "Mest besøkte nettsider",
|
||||
"header_stories": "Hovudsakene",
|
||||
"header_visit_again": "Bes;kigjen",
|
||||
"header_visit_again": "Besøk igjen",
|
||||
"header_bookmarks": "Nylege bokmerke",
|
||||
"header_recommended_by": "Tilrådd av {provider}",
|
||||
"header_bookmarks_placeholder": "Du har ingen bokmerke enno.",
|
||||
"header_stories_from": "frå",
|
||||
"type_label_visited": "Vitja",
|
||||
"type_label_visited": "Besøkt",
|
||||
"type_label_bookmarked": "Bokmerkte",
|
||||
"type_label_synced": "Synkronisert frå ei anna eining",
|
||||
"type_label_recommended": "Trendar",
|
||||
"type_label_open": "Opna",
|
||||
"type_label_open": "Opne",
|
||||
"type_label_topic": "Emne",
|
||||
"type_label_now": "No",
|
||||
"menu_action_bookmark": "Bokmerke",
|
||||
|
@ -3920,7 +3926,7 @@
|
|||
"menu_action_copy_address": "Kopier adresse",
|
||||
"menu_action_email_link": "E-postlenke…",
|
||||
"menu_action_open_new_window": "Opne i nytt vindauge",
|
||||
"menu_action_open_private_window": "Opna i eit nytt privat vindauge",
|
||||
"menu_action_open_private_window": "Opne i eit nytt privat vindauge",
|
||||
"menu_action_dismiss": "Avslå",
|
||||
"menu_action_delete": "Slett frå historikk",
|
||||
"menu_action_pin": "Fest",
|
||||
|
@ -3935,7 +3941,7 @@
|
|||
"search_settings": "Endra søkjeinnstillingar",
|
||||
"section_info_option": "Info",
|
||||
"welcome_title": "Velkomen til ny fane",
|
||||
"welcome_body": "Firefox vil bruka denne plassen til å visa deg dei mest relevante bokmerka, artiklane, videoane og sidene du nettopp har vitja, slik at du enkelt kan finna tilbake til dei.",
|
||||
"welcome_body": "Firefox vil bruke denne plassen til å vise deg dei mest relevante bokmerka, artiklane, videoane og sidene du nettopp har vitja, slik at du enkelt kan finne tilbake til dei.",
|
||||
"welcome_label": "Identifiserer høgdepunkta dine",
|
||||
"time_label_less_than_minute": "<1 min.",
|
||||
"time_label_minute": "{number} m",
|
||||
|
@ -3966,8 +3972,8 @@
|
|||
"edit_topsites_edit_button": "Rediger denne nettsida",
|
||||
"edit_topsites_dismiss_button": "Avvis denne nettsida",
|
||||
"edit_topsites_add_button": "Legg til",
|
||||
"topsites_form_add_header": "Ny toppstad",
|
||||
"topsites_form_edit_header": "Rediger mest vitja",
|
||||
"topsites_form_add_header": "Ny Mest besøkt",
|
||||
"topsites_form_edit_header": "Rediger Mest besøkt",
|
||||
"topsites_form_title_placeholder": "Skriv inn ein tittel",
|
||||
"topsites_form_url_placeholder": "Skriv eller lim inn ein URL",
|
||||
"topsites_form_add_button": "Legg til",
|
||||
|
|
|
@ -8,7 +8,7 @@ Cu.import("resource://gre/modules/Services.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 {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
const {DefaultPrefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
|
||||
const {LocalizationFeed} = Cu.import("resource://activity-stream/lib/LocalizationFeed.jsm", {});
|
||||
const {ManualMigration} = Cu.import("resource://activity-stream/lib/ManualMigration.jsm", {});
|
||||
|
@ -147,7 +147,7 @@ const FEEDS_DATA = [
|
|||
name: "snippets",
|
||||
factory: () => new SnippetsFeed(),
|
||||
title: "Gets snippets data",
|
||||
value: false
|
||||
value: true
|
||||
},
|
||||
{
|
||||
name: "systemtick",
|
||||
|
@ -197,11 +197,12 @@ this.ActivityStream = class ActivityStream {
|
|||
this._updateDynamicPrefs();
|
||||
this._defaultPrefs.init();
|
||||
|
||||
// Hook up the store and let all feeds and pages initialize
|
||||
this.store.init(this.feeds);
|
||||
this.store.dispatch({
|
||||
this.store.dispatch(ac.BroadcastToContent({
|
||||
type: at.INIT,
|
||||
data: {version: this.options.version}
|
||||
});
|
||||
}));
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
|
|||
|
||||
// Url to fetch snippets, in the urlFormatter service format.
|
||||
const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
|
||||
const TELEMETRY_PREF = "datareporting.healthreport.uploadEnabled";
|
||||
const ONBOARDING_FINISHED_PREF = "browser.onboarding.notification.finished";
|
||||
const FXA_USERNAME_PREF = "services.sync.username";
|
||||
|
||||
// Should be bumped up if the snippets content format changes.
|
||||
const STARTPAGE_VERSION = 5;
|
||||
|
@ -48,22 +48,29 @@ this.SnippetsFeed = class SnippetsFeed {
|
|||
version: STARTPAGE_VERSION,
|
||||
profileCreatedWeeksAgo: profileInfo.createdWeeksAgo,
|
||||
profileResetWeeksAgo: profileInfo.resetWeeksAgo,
|
||||
telemetryEnabled: Services.prefs.getBoolPref(TELEMETRY_PREF),
|
||||
onboardingFinished: Services.prefs.getBoolPref(ONBOARDING_FINISHED_PREF)
|
||||
telemetryEnabled: Services.telemetry.canRecordBase,
|
||||
onboardingFinished: Services.prefs.getBoolPref(ONBOARDING_FINISHED_PREF),
|
||||
fxaccount: Services.prefs.prefHasUserValue(FXA_USERNAME_PREF)
|
||||
};
|
||||
|
||||
this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_DATA, data}));
|
||||
}
|
||||
_refreshCanRecordBase() {
|
||||
// TODO: There is currently no way to listen for changes to this value, so
|
||||
// we are just refreshing it on every new tab instead. A bug is filed
|
||||
// here to fix this: https://bugzilla.mozilla.org/show_bug.cgi?id=1386318
|
||||
this.store.dispatch({type: at.SNIPPETS_DATA, data: {telemetryEnabled: Services.telemetry.canRecordBase}});
|
||||
}
|
||||
async init() {
|
||||
await this._refresh();
|
||||
Services.prefs.addObserver(ONBOARDING_FINISHED_PREF, this._refresh);
|
||||
Services.prefs.addObserver(SNIPPETS_URL_PREF, this._refresh);
|
||||
Services.prefs.addObserver(TELEMETRY_PREF, this._refresh);
|
||||
Services.prefs.addObserver(FXA_USERNAME_PREF, this._refresh);
|
||||
}
|
||||
uninit() {
|
||||
Services.prefs.removeObserver(ONBOARDING_FINISHED_PREF, this._refresh);
|
||||
Services.prefs.removeObserver(SNIPPETS_URL_PREF, this._refresh);
|
||||
Services.prefs.removeObserver(TELEMETRY_PREF, this._refresh);
|
||||
Services.prefs.removeObserver(FXA_USERNAME_PREF, this._refresh);
|
||||
this.store.dispatch({type: at.SNIPPETS_RESET});
|
||||
}
|
||||
onAction(action) {
|
||||
|
@ -74,6 +81,9 @@ this.SnippetsFeed = class SnippetsFeed {
|
|||
case at.FEED_INIT:
|
||||
if (action.data === "feeds.snippets") { this.init(); }
|
||||
break;
|
||||
case at.NEW_TAB_INIT:
|
||||
this._refreshCanRecordBase();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ 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");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "console",
|
||||
"resource://gre/modules/Console.jsm");
|
||||
|
@ -19,8 +20,6 @@ const ENDPOINT_PREF = `${PREF_BRANCH}telemetry.ping.endpoint`;
|
|||
const TELEMETRY_PREF = `${PREF_BRANCH}telemetry`;
|
||||
const LOGGING_PREF = `${PREF_BRANCH}telemetry.log`;
|
||||
|
||||
const FHR_UPLOAD_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
|
||||
|
||||
/**
|
||||
* Observe various notifications and send them to a telemetry endpoint.
|
||||
*
|
||||
|
@ -44,10 +43,6 @@ function TelemetrySender(args) {
|
|||
this._onTelemetryPrefChange = this._onTelemetryPrefChange.bind(this);
|
||||
this._prefs.observe(TELEMETRY_PREF, this._onTelemetryPrefChange);
|
||||
|
||||
this._fhrEnabled = this._prefs.get(FHR_UPLOAD_ENABLED_PREF);
|
||||
this._onFhrPrefChange = this._onFhrPrefChange.bind(this);
|
||||
this._prefs.observe(FHR_UPLOAD_ENABLED_PREF, this._onFhrPrefChange);
|
||||
|
||||
this.logging = this._prefs.get(LOGGING_PREF);
|
||||
this._onLoggingPrefChange = this._onLoggingPrefChange.bind(this);
|
||||
this._prefs.observe(LOGGING_PREF, this._onLoggingPrefChange);
|
||||
|
@ -57,7 +52,9 @@ function TelemetrySender(args) {
|
|||
|
||||
TelemetrySender.prototype = {
|
||||
get enabled() {
|
||||
return this._enabled && this._fhrEnabled;
|
||||
// Note: Services.telemetry.canRecordBase is the general indicator for
|
||||
// opt-out Firefox Telemetry
|
||||
return this._enabled && Services.telemetry.canRecordBase;
|
||||
},
|
||||
|
||||
_onLoggingPrefChange(prefVal) {
|
||||
|
@ -68,10 +65,6 @@ TelemetrySender.prototype = {
|
|||
this._enabled = prefVal;
|
||||
},
|
||||
|
||||
_onFhrPrefChange(prefVal) {
|
||||
this._fhrEnabled = prefVal;
|
||||
},
|
||||
|
||||
sendPing(data) {
|
||||
if (this.logging) {
|
||||
// performance related pings cause a lot of logging, so we mute them
|
||||
|
@ -95,7 +88,6 @@ TelemetrySender.prototype = {
|
|||
try {
|
||||
this._prefs.ignore(TELEMETRY_PREF, this._onTelemetryPrefChange);
|
||||
this._prefs.ignore(LOGGING_PREF, this._onLoggingPrefChange);
|
||||
this._prefs.ignore(FHR_UPLOAD_ENABLED_PREF, this._onFhrPrefChange);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
|
@ -105,7 +97,6 @@ TelemetrySender.prototype = {
|
|||
this.TelemetrySender = TelemetrySender;
|
||||
this.TelemetrySenderConstants = {
|
||||
ENDPOINT_PREF,
|
||||
FHR_UPLOAD_ENABLED_PREF,
|
||||
TELEMETRY_PREF,
|
||||
LOGGING_PREF
|
||||
};
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
|
||||
Cu.importGlobalProperties(["fetch", "URL"]);
|
||||
|
||||
const TIPPYTOP_JSON_PATH = "resource://activity-stream/data/content/tippytop/top_sites.json";
|
||||
const TIPPYTOP_URL_PREFIX = "resource://activity-stream/data/content/tippytop/images/";
|
||||
|
||||
function getDomain(url) {
|
||||
let domain = new URL(url).hostname;
|
||||
if (domain && domain.startsWith("www.")) {
|
||||
domain = domain.slice(4);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
function getPath(url) {
|
||||
return new URL(url).pathname;
|
||||
}
|
||||
|
||||
this.TippyTopProvider = class TippyTopProvider {
|
||||
constructor() {
|
||||
this._sitesByDomain = new Map();
|
||||
}
|
||||
async init() {
|
||||
// Load the Tippy Top sites from the json manifest.
|
||||
try {
|
||||
for (const site of await (await fetch(TIPPYTOP_JSON_PATH)).json()) {
|
||||
// The tippy top manifest can have a url property (string) or a
|
||||
// urls property (array of strings)
|
||||
for (const url of site.url ? [site.url] : site.urls || []) {
|
||||
this._sitesByDomain.set(getDomain(url), site);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Cu.reportError("Failed to load tippy top manifest.");
|
||||
}
|
||||
}
|
||||
processSite(site) {
|
||||
// Skip URLs with a path that isn't the root path /
|
||||
let path;
|
||||
try {
|
||||
path = getPath(site.url);
|
||||
} catch (e) {}
|
||||
if (path !== "/") {
|
||||
return site;
|
||||
}
|
||||
|
||||
const tippyTop = this._sitesByDomain.get(getDomain(site.url));
|
||||
if (tippyTop) {
|
||||
site.tippyTopIcon = TIPPYTOP_URL_PREFIX + tippyTop.image_url;
|
||||
site.backgroundColor = tippyTop.background_color;
|
||||
}
|
||||
return site;
|
||||
}
|
||||
};
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["TippyTopProvider"];
|
|
@ -7,7 +7,10 @@ const {utils: Cu} = Components;
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
const {TippyTopProvider} = Cu.import("resource://activity-stream/lib/TippyTopProvider.jsm", {});
|
||||
const {insertPinned} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
|
||||
const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
|
||||
const {shortURL} = Cu.import("resource://activity-stream/common/ShortURL.jsm", {});
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
|
||||
"resource://gre/modules/NewTabUtils.jsm");
|
||||
|
@ -22,6 +25,12 @@ const DEFAULT_TOP_SITES = [];
|
|||
this.TopSitesFeed = class TopSitesFeed {
|
||||
constructor() {
|
||||
this.lastUpdated = 0;
|
||||
this._tippyTopProvider = new TippyTopProvider();
|
||||
this._tippyTopProvider.init();
|
||||
this.dedupe = new Dedupe(this._dedupeKey);
|
||||
}
|
||||
_dedupeKey(site) {
|
||||
return site && site.hostname;
|
||||
}
|
||||
refreshDefaults(sites) {
|
||||
// Clear out the array of any previous defaults
|
||||
|
@ -54,7 +63,17 @@ this.TopSitesFeed = class TopSitesFeed {
|
|||
frecent = frecent.filter(link => link && link.type !== "affiliate");
|
||||
}
|
||||
|
||||
return insertPinned([...frecent, ...DEFAULT_TOP_SITES], pinned).slice(0, TOP_SITES_SHOWMORE_LENGTH);
|
||||
// Group together websites that require deduping.
|
||||
let topsitesGroup = [];
|
||||
for (const group of [pinned, frecent, DEFAULT_TOP_SITES]) {
|
||||
topsitesGroup.push(group.filter(site => site).map(site => Object.assign({}, site, {hostname: shortURL(site)})));
|
||||
}
|
||||
|
||||
const dedupedGroups = this.dedupe.group(topsitesGroup);
|
||||
// Insert original pinned websites in the result of the dedupe operation.
|
||||
pinned = insertPinned([...dedupedGroups[1], ...dedupedGroups[2]], pinned);
|
||||
|
||||
return pinned.slice(0, TOP_SITES_SHOWMORE_LENGTH);
|
||||
}
|
||||
async refresh(target = null) {
|
||||
const links = await this.getLinksWithDefaults();
|
||||
|
@ -67,9 +86,15 @@ this.TopSitesFeed = class TopSitesFeed {
|
|||
}
|
||||
}
|
||||
|
||||
// Now, get a screenshot for every item
|
||||
// Now, get a tippy top icon or screenshot for every item
|
||||
for (let link of links) {
|
||||
if (!link) { continue; }
|
||||
|
||||
// Check for tippy top icon.
|
||||
link = this._tippyTopProvider.processSite(link);
|
||||
if (link.tippyTopIcon) { continue; }
|
||||
|
||||
// If no tippy top, then we get a screenshot.
|
||||
if (currentScreenshots[link.url]) {
|
||||
link.screenshot = currentScreenshots[link.url];
|
||||
} else {
|
||||
|
@ -110,15 +135,9 @@ this.TopSitesFeed = class TopSitesFeed {
|
|||
}));
|
||||
}
|
||||
onAction(action) {
|
||||
let realRows;
|
||||
switch (action.type) {
|
||||
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);
|
||||
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)
|
||||
|
|
|
@ -11,6 +11,7 @@ Cu.importGlobalProperties(["fetch"]);
|
|||
|
||||
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
|
||||
const {shortURL} = Cu.import("resource://activity-stream/common/ShortURL.jsm", {});
|
||||
|
||||
const STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
|
||||
const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours
|
||||
|
@ -85,6 +86,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
.filter(s => !NewTabUtils.blockedLinks.isBlocked({"url": s.dedupe_url}))
|
||||
.map(s => ({
|
||||
"guid": s.id,
|
||||
"hostname": shortURL(Object.assign({}, s, {url: s.dedupe_url})),
|
||||
"type": (Date.now() - (s.published_timestamp * 1000)) <= STORIES_NOW_THRESHOLD ? "now" : "trending",
|
||||
"title": s.title,
|
||||
"description": s.excerpt,
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
const {Dedupe} = require("common/Dedupe.jsm");
|
||||
|
||||
describe("Dedupe", () => {
|
||||
let instance;
|
||||
beforeEach(() => {
|
||||
instance = new Dedupe();
|
||||
});
|
||||
describe("group", () => {
|
||||
it("should remove duplicates inside the groups", () => {
|
||||
const beforeItems = [[1, 1, 1], [2, 2, 2], [3, 3, 3]];
|
||||
const afterItems = [[1], [2], [3]];
|
||||
assert.deepEqual(instance.group(beforeItems), afterItems);
|
||||
});
|
||||
it("should remove duplicates between groups, favouring earlier groups", () => {
|
||||
const beforeItems = [[1, 2, 3], [2, 3, 4], [3, 4, 5]];
|
||||
const afterItems = [[1, 2, 3], [4], [5]];
|
||||
assert.deepEqual(instance.group(beforeItems), afterItems);
|
||||
});
|
||||
it("should remove duplicates from groups of objects", () => {
|
||||
instance = new Dedupe(item => item.id);
|
||||
const beforeItems = [[{id: 1}, {id: 1}, {id: 2}], [{id: 1}, {id: 3}, {id: 2}], [{id: 1}, {id: 2}, {id: 5}]];
|
||||
const afterItems = [[{id: 1}, {id: 2}], [{id: 3}], [{id: 5}]];
|
||||
assert.deepEqual(instance.group(beforeItems), afterItems);
|
||||
});
|
||||
it("should take a custom comparison function", () => {
|
||||
function compare(previous, current) {
|
||||
return current.amount > previous.amount;
|
||||
}
|
||||
instance = new Dedupe(item => item.id, compare);
|
||||
const beforeItems = [
|
||||
[{id: 1, amount: 50}, {id: 1, amount: 100}],
|
||||
[{id: 1, amount: 200}, {id: 2, amount: 0}, {id: 2, amount: 100}]
|
||||
];
|
||||
const afterItems = [
|
||||
[{id: 1, amount: 100}],
|
||||
[{id: 2, amount: 100}]
|
||||
];
|
||||
|
||||
assert.deepEqual(instance.group(beforeItems), afterItems);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,14 +9,18 @@ describe("Reducers", () => {
|
|||
const nextState = App(undefined, {type: "FOO"});
|
||||
assert.equal(nextState, INITIAL_STATE.App);
|
||||
});
|
||||
it("should not set initialized to true on INIT", () => {
|
||||
it("should set initialized to true on INIT", () => {
|
||||
const nextState = App(undefined, {type: "INIT"});
|
||||
|
||||
assert.propertyVal(nextState, "initialized", true);
|
||||
});
|
||||
it("should set initialized, version, and locale on INIT", () => {
|
||||
const action = {type: "INIT", data: {version: "1.2.3"}};
|
||||
|
||||
const nextState = App(undefined, action);
|
||||
|
||||
assert.propertyVal(nextState, "version", "1.2.3");
|
||||
assert.propertyVal(nextState, "locale", INITIAL_STATE.App.locale);
|
||||
});
|
||||
it("should not update state for empty action.data on LOCALE_UPDATED", () => {
|
||||
const nextState = App(undefined, {type: at.LOCALE_UPDATED});
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
const {shortURL} = require("common/ShortURL.jsm");
|
||||
|
||||
describe("shortURL", () => {
|
||||
it("should return a blank string if url and hostname is falsey", () => {
|
||||
assert.equal(shortURL({url: ""}), "");
|
||||
assert.equal(shortURL({hostname: null}), "");
|
||||
});
|
||||
|
||||
it("should remove the eTLD, if provided", () => {
|
||||
assert.equal(shortURL({hostname: "com.blah.com", eTLD: "com"}), "com.blah");
|
||||
});
|
||||
|
||||
it("should use the hostname, if provided", () => {
|
||||
assert.equal(shortURL({hostname: "foo.com", url: "http://bar.com", eTLD: "com"}), "foo");
|
||||
});
|
||||
|
||||
it("should get the hostname from .url if necessary", () => {
|
||||
assert.equal(shortURL({url: "http://bar.com", eTLD: "com"}), "bar");
|
||||
});
|
||||
|
||||
it("should not strip out www if not first subdomain", () => {
|
||||
assert.equal(shortURL({hostname: "foo.www.com", eTLD: "com"}), "foo.www");
|
||||
});
|
||||
|
||||
it("should convert to lowercase", () => {
|
||||
assert.equal(shortURL({url: "HTTP://FOO.COM", eTLD: "com"}), "foo");
|
||||
});
|
||||
|
||||
it("should return hostname for localhost", () => {
|
||||
assert.equal(shortURL({url: "http://localhost:8000/", eTLD: "localhost"}), "localhost");
|
||||
});
|
||||
|
||||
it("should fallback to link title if it exists", () => {
|
||||
const link = {
|
||||
url: "file:///Users/voprea/Work/activity-stream/logs/coverage/system-addon/report-html/index.html",
|
||||
title: "Code coverage report"
|
||||
};
|
||||
|
||||
assert.equal(shortURL(link), link.title);
|
||||
});
|
||||
|
||||
it("should return the url if no hostname or title is provided", () => {
|
||||
const url = "file://foo/bar.txt";
|
||||
assert.equal(shortURL({url, eTLD: "foo"}), url);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
const injector = require("inject!lib/ActivityStream.jsm");
|
||||
const {CONTENT_MESSAGE_TYPE} = require("common/Actions.jsm");
|
||||
|
||||
const REASON_ADDON_UNINSTALL = 6;
|
||||
|
||||
|
@ -63,6 +64,14 @@ describe("ActivityStream", () => {
|
|||
const action = as.store.dispatch.firstCall.args[0];
|
||||
assert.propertyVal(action.data, "version", "1.2.3");
|
||||
});
|
||||
it("should emit an INIT event to content", () => {
|
||||
sandbox.stub(as.store, "dispatch");
|
||||
|
||||
as.init();
|
||||
|
||||
const action = as.store.dispatch.firstCall.args[0];
|
||||
assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
|
||||
});
|
||||
});
|
||||
describe("#uninit", () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -10,16 +10,21 @@ describe("ActivityStreamMessageChannel", () => {
|
|||
let dispatch;
|
||||
let mm;
|
||||
beforeEach(() => {
|
||||
function RP(url) {
|
||||
function RP(url, isFromAboutNewTab = false) {
|
||||
this.url = url;
|
||||
this.messagePorts = [];
|
||||
this.addMessageListener = globals.sandbox.spy();
|
||||
this.removeMessageListener = globals.sandbox.spy();
|
||||
this.sendAsyncMessage = globals.sandbox.spy();
|
||||
this.destroy = globals.sandbox.spy();
|
||||
this.isFromAboutNewTab = isFromAboutNewTab;
|
||||
}
|
||||
globals = new GlobalOverrider();
|
||||
const override = globals.sandbox.stub();
|
||||
override.withArgs(true).returns(new RP("about:newtab", true));
|
||||
override.withArgs(false).returns(null);
|
||||
globals.set("AboutNewTab", {
|
||||
override: globals.sandbox.spy(),
|
||||
override,
|
||||
reset: globals.sandbox.spy()
|
||||
});
|
||||
globals.set("RemotePages", RP);
|
||||
|
@ -64,6 +69,10 @@ describe("ActivityStreamMessageChannel", () => {
|
|||
mm.createChannel();
|
||||
assert.calledOnce(global.AboutNewTab.override);
|
||||
});
|
||||
it("should use the channel passed by AboutNewTab on override", () => {
|
||||
mm.createChannel();
|
||||
assert.ok(mm.channel.isFromAboutNewTab);
|
||||
});
|
||||
it("should not override AboutNewTab if the pageURL is not about:newtab", () => {
|
||||
mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
|
||||
mm.createChannel();
|
||||
|
@ -76,17 +85,14 @@ describe("ActivityStreamMessageChannel", () => {
|
|||
mm.createChannel();
|
||||
channel = mm.channel;
|
||||
});
|
||||
it("should call channel.destroy()", () => {
|
||||
mm.destroyChannel();
|
||||
assert.calledOnce(channel.destroy);
|
||||
});
|
||||
it("should set .channel to null", () => {
|
||||
mm.destroyChannel();
|
||||
assert.isNull(mm.channel);
|
||||
});
|
||||
it("should reset AboutNewTab", () => {
|
||||
it("should reset AboutNewTab, and pass back its channel", () => {
|
||||
mm.destroyChannel();
|
||||
assert.calledOnce(global.AboutNewTab.reset);
|
||||
assert.calledWith(global.AboutNewTab.reset, channel);
|
||||
});
|
||||
it("should not reset AboutNewTab if the pageURL is not about:newtab", () => {
|
||||
mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
|
||||
|
@ -94,6 +100,13 @@ describe("ActivityStreamMessageChannel", () => {
|
|||
mm.destroyChannel();
|
||||
assert.notCalled(global.AboutNewTab.reset);
|
||||
});
|
||||
it("should call channel.destroy() if pageURL is not about:newtab", () => {
|
||||
mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
|
||||
mm.createChannel();
|
||||
channel = mm.channel;
|
||||
mm.destroyChannel();
|
||||
assert.calledOnce(channel.destroy);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Message handling", () => {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
const {SnippetsFeed} = require("lib/SnippetsFeed.jsm");
|
||||
const {actionTypes: at} = require("common/Actions.jsm");
|
||||
const {GlobalOverrider} = require("test/unit/utils");
|
||||
const {createStore, combineReducers} = require("redux");
|
||||
const {reducers} = require("common/Reducers.jsm");
|
||||
|
||||
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
|
@ -8,6 +10,7 @@ let overrider = new GlobalOverrider();
|
|||
|
||||
describe("SnippetsFeed", () => {
|
||||
let sandbox;
|
||||
let store;
|
||||
let clock;
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers();
|
||||
|
@ -20,6 +23,8 @@ describe("SnippetsFeed", () => {
|
|||
}
|
||||
});
|
||||
sandbox = sinon.sandbox.create();
|
||||
store = createStore(combineReducers(reducers));
|
||||
sinon.spy(store, "dispatch");
|
||||
});
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
|
@ -30,34 +35,43 @@ describe("SnippetsFeed", () => {
|
|||
const url = "foo.com/%STARTPAGE_VERSION%";
|
||||
sandbox.stub(global.Services.prefs, "getStringPref").returns(url);
|
||||
sandbox.stub(global.Services.prefs, "getBoolPref")
|
||||
.withArgs("datareporting.healthreport.uploadEnabled")
|
||||
.returns(true)
|
||||
.withArgs("browser.onboarding.notification.finished")
|
||||
.returns(false);
|
||||
sandbox.stub(global.Services.prefs, "prefHasUserValue")
|
||||
.withArgs("services.sync.username")
|
||||
.returns(true);
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
|
||||
|
||||
const feed = new SnippetsFeed();
|
||||
feed.store = {dispatch: sandbox.stub()};
|
||||
|
||||
feed.store = store;
|
||||
clock.tick(WEEK_IN_MS * 2);
|
||||
|
||||
await feed.init();
|
||||
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
const state = store.getState().Snippets;
|
||||
|
||||
const action = feed.store.dispatch.firstCall.args[0];
|
||||
assert.propertyVal(action, "type", at.SNIPPETS_DATA);
|
||||
assert.isObject(action.data);
|
||||
assert.propertyVal(action.data, "snippetsURL", "foo.com/5");
|
||||
assert.propertyVal(action.data, "version", 5);
|
||||
assert.propertyVal(action.data, "profileCreatedWeeksAgo", 2);
|
||||
assert.propertyVal(action.data, "profileResetWeeksAgo", 1);
|
||||
assert.propertyVal(action.data, "telemetryEnabled", true);
|
||||
assert.propertyVal(action.data, "onboardingFinished", false);
|
||||
assert.propertyVal(state, "snippetsURL", "foo.com/5");
|
||||
assert.propertyVal(state, "version", 5);
|
||||
assert.propertyVal(state, "profileCreatedWeeksAgo", 2);
|
||||
assert.propertyVal(state, "profileResetWeeksAgo", 1);
|
||||
assert.propertyVal(state, "telemetryEnabled", false);
|
||||
assert.propertyVal(state, "onboardingFinished", false);
|
||||
assert.propertyVal(state, "fxaccount", true);
|
||||
});
|
||||
it("should update telemetryEnabled on each new tab", () => {
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
|
||||
const feed = new SnippetsFeed();
|
||||
feed.store = store;
|
||||
|
||||
feed.onAction({type: at.NEW_TAB_INIT});
|
||||
|
||||
const state = store.getState().Snippets;
|
||||
assert.propertyVal(state, "telemetryEnabled", false);
|
||||
});
|
||||
it("should call .init on an INIT aciton", () => {
|
||||
const feed = new SnippetsFeed();
|
||||
sandbox.stub(feed, "init");
|
||||
feed.store = {dispatch: sandbox.stub()};
|
||||
feed.store = store;
|
||||
|
||||
feed.onAction({type: at.INIT});
|
||||
assert.calledOnce(feed.init);
|
||||
|
@ -65,7 +79,7 @@ describe("SnippetsFeed", () => {
|
|||
it("should call .init when a FEED_INIT happens for feeds.snippets", () => {
|
||||
const feed = new SnippetsFeed();
|
||||
sandbox.stub(feed, "init");
|
||||
feed.store = {dispatch: sandbox.stub()};
|
||||
feed.store = store;
|
||||
|
||||
feed.onAction({type: at.FEED_INIT, data: "feeds.snippets"});
|
||||
|
||||
|
@ -73,7 +87,7 @@ describe("SnippetsFeed", () => {
|
|||
});
|
||||
it("should dispatch a SNIPPETS_RESET on uninit", () => {
|
||||
const feed = new SnippetsFeed();
|
||||
feed.store = {dispatch: sandbox.stub()};
|
||||
feed.store = store;
|
||||
|
||||
feed.uninit();
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
|
||||
const {TelemetrySender, TelemetrySenderConstants} = require("lib/TelemetrySender.jsm");
|
||||
const {ENDPOINT_PREF, FHR_UPLOAD_ENABLED_PREF, TELEMETRY_PREF, LOGGING_PREF} =
|
||||
const {ENDPOINT_PREF, TELEMETRY_PREF, LOGGING_PREF} =
|
||||
TelemetrySenderConstants;
|
||||
|
||||
/**
|
||||
|
@ -52,15 +52,15 @@ describe("TelemetrySender", () => {
|
|||
|
||||
describe("#enabled", () => {
|
||||
let testParams = [
|
||||
{enabledPref: true, fhrPref: true, result: true},
|
||||
{enabledPref: false, fhrPref: true, result: false},
|
||||
{enabledPref: true, fhrPref: false, result: false},
|
||||
{enabledPref: false, fhrPref: false, result: false}
|
||||
{enabledPref: true, canRecordBase: true, result: true},
|
||||
{enabledPref: false, canRecordBase: true, result: false},
|
||||
{enabledPref: true, canRecordBase: false, result: false},
|
||||
{enabledPref: false, canRecordBase: false, result: false}
|
||||
];
|
||||
|
||||
function testEnabled(p) {
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = p.enabledPref;
|
||||
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = p.fhrPref;
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(p.canRecordBase);
|
||||
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
|
@ -68,7 +68,7 @@ describe("TelemetrySender", () => {
|
|||
}
|
||||
|
||||
for (let p of testParams) {
|
||||
it(`should return ${p.result} if the fhrPref is ${p.fhrPref} and telemetry.enabled is ${p.enabledPref}`, () => {
|
||||
it(`should return ${p.result} if the Services.telemetry.canRecordBase is ${p.canRecordBase} and telemetry.enabled is ${p.enabledPref}`, () => {
|
||||
testEnabled(p);
|
||||
});
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ describe("TelemetrySender", () => {
|
|||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
|
||||
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
assert.propertyVal(tSender, "enabled", true);
|
||||
});
|
||||
|
@ -92,7 +92,7 @@ describe("TelemetrySender", () => {
|
|||
describe("telemetry.enabled pref changes from false to true", () => {
|
||||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
|
@ -106,26 +106,26 @@ describe("TelemetrySender", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("FHR enabled pref changes from true to false", () => {
|
||||
describe("canRecordBase changes from true to false", () => {
|
||||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
|
||||
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
assert.propertyVal(tSender, "enabled", true);
|
||||
});
|
||||
|
||||
it("should set the enabled property to false", () => {
|
||||
fakePrefs.set(FHR_UPLOAD_ENABLED_PREF, false);
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
|
||||
|
||||
assert.propertyVal(tSender, "enabled", false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FHR enabled pref changes from false to true", () => {
|
||||
describe("canRecordBase changes from false to true", () => {
|
||||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = false;
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
|
@ -133,7 +133,7 @@ describe("TelemetrySender", () => {
|
|||
});
|
||||
|
||||
it("should set the enabled property to true", () => {
|
||||
fakePrefs.set(FHR_UPLOAD_ENABLED_PREF, true);
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
|
||||
|
||||
assert.propertyVal(tSender, "enabled", true);
|
||||
});
|
||||
|
@ -143,7 +143,7 @@ describe("TelemetrySender", () => {
|
|||
describe("#sendPing()", () => {
|
||||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
|
||||
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
|
||||
FakePrefs.prototype.prefs[ENDPOINT_PREF] = fakeEndpointUrl;
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
@ -208,15 +208,6 @@ describe("TelemetrySender", () => {
|
|||
assert.notProperty(fakePrefs.observers, TELEMETRY_PREF);
|
||||
});
|
||||
|
||||
it("should remove the fhrpref listener", () => {
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
assert.property(fakePrefs.observers, FHR_UPLOAD_ENABLED_PREF);
|
||||
|
||||
tSender.uninit();
|
||||
|
||||
assert.notProperty(fakePrefs.observers, FHR_UPLOAD_ENABLED_PREF);
|
||||
});
|
||||
|
||||
it("should remove the telemetry log listener", () => {
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
assert.property(fakePrefs.observers, LOGGING_PREF);
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
"use strict";
|
||||
const {TippyTopProvider} = require("lib/TippyTopProvider.jsm");
|
||||
const {GlobalOverrider} = require("test/unit/utils");
|
||||
|
||||
describe("TippyTopProvider", () => {
|
||||
let instance;
|
||||
let globals;
|
||||
beforeEach(async () => {
|
||||
globals = new GlobalOverrider();
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve([{
|
||||
"title": "facebook",
|
||||
"url": "https://www.facebook.com/",
|
||||
"image_url": "facebook-com.png",
|
||||
"background_color": "#3b5998",
|
||||
"domain": "facebook.com"
|
||||
}, {
|
||||
"title": "gmail",
|
||||
"urls": ["https://www.gmail.com/", "https://mail.google.com"],
|
||||
"image_url": "gmail-com.png",
|
||||
"background_color": "#000000",
|
||||
"domain": "gmail.com"
|
||||
}])
|
||||
});
|
||||
instance = new TippyTopProvider();
|
||||
await instance.init();
|
||||
});
|
||||
it("should provide an icon for facebook.com", () => {
|
||||
const site = instance.processSite({url: "https://facebook.com"});
|
||||
assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/facebook-com.png");
|
||||
assert.equal(site.backgroundColor, "#3b5998");
|
||||
});
|
||||
it("should provide an icon for www.facebook.com", () => {
|
||||
const site = instance.processSite({url: "https://www.facebook.com"});
|
||||
assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/facebook-com.png");
|
||||
assert.equal(site.backgroundColor, "#3b5998");
|
||||
});
|
||||
it("should not provide an icon for facebook.com/foobar", () => {
|
||||
const site = instance.processSite({url: "https://facebook.com/foobar"});
|
||||
assert.isUndefined(site.tippyTopIcon);
|
||||
assert.isUndefined(site.backgroundColor);
|
||||
});
|
||||
it("should provide an icon for gmail.com", () => {
|
||||
const site = instance.processSite({url: "https://gmail.com"});
|
||||
assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/gmail-com.png");
|
||||
assert.equal(site.backgroundColor, "#000000");
|
||||
});
|
||||
it("should provide an icon for mail.google.com", () => {
|
||||
const site = instance.processSite({url: "https://mail.google.com"});
|
||||
assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/gmail-com.png");
|
||||
assert.equal(site.backgroundColor, "#000000");
|
||||
});
|
||||
it("should handle garbage URLs gracefully", () => {
|
||||
const site = instance.processSite({url: "garbagejlfkdsa"});
|
||||
assert.isUndefined(site.tippyTopIcon);
|
||||
assert.isUndefined(site.backgroundColor);
|
||||
});
|
||||
it("should handle error when fetching and parsing manifest", async () => {
|
||||
globals = new GlobalOverrider();
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
fetchStub.rejects("whaaaa");
|
||||
instance = new TippyTopProvider();
|
||||
await instance.init();
|
||||
instance.processSite("https://facebook.com");
|
||||
});
|
||||
});
|
|
@ -5,9 +5,15 @@ const {FakePrefs, GlobalOverrider} = require("test/unit/utils");
|
|||
const action = {meta: {fromTarget: {}}};
|
||||
const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
|
||||
const {insertPinned} = require("common/Reducers.jsm");
|
||||
const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `site${i}.com`}));
|
||||
const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `http://www.site${i}.com`}));
|
||||
const FAKE_SCREENSHOT = "data123";
|
||||
|
||||
function FakeTippyTopProvider() {}
|
||||
FakeTippyTopProvider.prototype = {
|
||||
init() {},
|
||||
processSite(site) { return site; }
|
||||
};
|
||||
|
||||
describe("Top Sites Feed", () => {
|
||||
let TopSitesFeed;
|
||||
let DEFAULT_TOP_SITES;
|
||||
|
@ -18,6 +24,7 @@ describe("Top Sites Feed", () => {
|
|||
let clock;
|
||||
let fakeNewTabUtils;
|
||||
let fakeScreenshot;
|
||||
let shortURLStub;
|
||||
|
||||
beforeEach(() => {
|
||||
globals = new GlobalOverrider();
|
||||
|
@ -32,15 +39,21 @@ describe("Top Sites Feed", () => {
|
|||
}
|
||||
};
|
||||
fakeScreenshot = {getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))};
|
||||
shortURLStub = sinon.stub().callsFake(site => site.url);
|
||||
const fakeDedupe = function() {};
|
||||
globals.set("NewTabUtils", fakeNewTabUtils);
|
||||
FakePrefs.prototype.prefs["default.sites"] = "https://foo.com/";
|
||||
({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
|
||||
"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
|
||||
"common/Dedupe.jsm": {Dedupe: fakeDedupe},
|
||||
"common/Reducers.jsm": {insertPinned},
|
||||
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot}
|
||||
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
|
||||
"lib/TippyTopProvider.jsm": {TippyTopProvider: FakeTippyTopProvider},
|
||||
"common/ShortURL.jsm": {shortURL: shortURLStub}
|
||||
}));
|
||||
feed = new TopSitesFeed();
|
||||
feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
|
||||
feed.dedupe.group = sites => sites;
|
||||
links = FAKE_LINKS;
|
||||
clock = sinon.useFakeTimers();
|
||||
});
|
||||
|
@ -84,19 +97,42 @@ describe("Top Sites Feed", () => {
|
|||
|
||||
it("should get the links from NewTabUtils", async () => {
|
||||
const result = await feed.getLinksWithDefaults();
|
||||
assert.deepEqual(result, links);
|
||||
const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
|
||||
|
||||
assert.deepEqual(result, reference);
|
||||
assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
|
||||
});
|
||||
it("should call dedupe on the links", async () => {
|
||||
const stub = sinon.stub(feed.dedupe, "group", id => id);
|
||||
|
||||
await feed.getLinksWithDefaults();
|
||||
|
||||
assert.calledOnce(stub);
|
||||
});
|
||||
it("should dedupe the links by hostname", async () => {
|
||||
const site = {url: "foo", hostname: "bar"};
|
||||
const result = feed._dedupeKey(site);
|
||||
|
||||
assert.equal(result, site.hostname);
|
||||
});
|
||||
it("should add defaults if there are are not enough links", async () => {
|
||||
links = [{url: "foo.com"}];
|
||||
|
||||
const result = await feed.getLinksWithDefaults();
|
||||
assert.deepEqual(result, [{url: "foo.com"}, ...DEFAULT_TOP_SITES]);
|
||||
const reference = [...links, ...DEFAULT_TOP_SITES].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
|
||||
|
||||
assert.deepEqual(result, reference);
|
||||
});
|
||||
it("should only add defaults up to TOP_SITES_SHOWMORE_LENGTH", async () => {
|
||||
links = new Array(TOP_SITES_SHOWMORE_LENGTH - 1).fill({url: "foo.com"});
|
||||
links = [];
|
||||
for (let i = 0; i < TOP_SITES_SHOWMORE_LENGTH - 1; i++) {
|
||||
links.push({url: `foo${i}.com`});
|
||||
}
|
||||
const result = await feed.getLinksWithDefaults();
|
||||
const reference = [...links, DEFAULT_TOP_SITES[0]].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
|
||||
|
||||
assert.lengthOf(result, TOP_SITES_SHOWMORE_LENGTH);
|
||||
assert.deepEqual(result, [...links, DEFAULT_TOP_SITES[0]]);
|
||||
assert.deepEqual(result, reference);
|
||||
});
|
||||
it("should not throw if NewTabUtils returns null", () => {
|
||||
links = null;
|
||||
|
@ -104,14 +140,60 @@ describe("Top Sites Feed", () => {
|
|||
feed.getLinksWithDefaults(action);
|
||||
});
|
||||
});
|
||||
describe("deduping", () => {
|
||||
beforeEach(() => {
|
||||
({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
|
||||
"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
|
||||
"common/Reducers.jsm": {insertPinned},
|
||||
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot}
|
||||
}));
|
||||
feed = new TopSitesFeed();
|
||||
});
|
||||
it("should not dedupe pinned sites", async () => {
|
||||
fakeNewTabUtils.pinnedLinks.links = [
|
||||
{url: "https://developer.mozilla.org/en-US/docs/Web"},
|
||||
{url: "https://developer.mozilla.org/en-US/docs/Learn"}
|
||||
];
|
||||
|
||||
const sites = await feed.getLinksWithDefaults();
|
||||
|
||||
assert.lengthOf(sites, 12);
|
||||
assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
|
||||
assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url);
|
||||
assert.equal(sites[0].hostname, sites[1].hostname);
|
||||
});
|
||||
it("should not dedupe pinned sites", async () => {
|
||||
fakeNewTabUtils.pinnedLinks.links = [
|
||||
{url: "https://developer.mozilla.org/en-US/docs/Web"},
|
||||
{url: "https://developer.mozilla.org/en-US/docs/Learn"}
|
||||
];
|
||||
// These will be the frecent results.
|
||||
links = [
|
||||
{url: "https://developer.mozilla.org/en-US/docs/Web"},
|
||||
{url: "https://developer.mozilla.org/en-US/docs/Learn"}
|
||||
];
|
||||
|
||||
const sites = await feed.getLinksWithDefaults();
|
||||
|
||||
// Frecent results are removed and only pinned are kept.
|
||||
assert.lengthOf(sites, 2);
|
||||
});
|
||||
it("should check against null entries", async () => {
|
||||
fakeNewTabUtils.pinnedLinks.links = [null];
|
||||
|
||||
await feed.getLinksWithDefaults();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("#refresh", () => {
|
||||
it("should dispatch an action with the links returned", async () => {
|
||||
sandbox.stub(feed, "getScreenshot");
|
||||
await feed.refresh(action);
|
||||
const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
|
||||
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
|
||||
assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, links);
|
||||
assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, reference);
|
||||
});
|
||||
it("should reuse screenshots for existing links, and call feed.getScreenshot for others", async () => {
|
||||
sandbox.stub(feed, "getScreenshot");
|
||||
|
@ -136,6 +218,17 @@ describe("Top Sites Feed", () => {
|
|||
await feed.refresh(action);
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
});
|
||||
it("should skip getting screenshot if there is a tippy top icon", async () => {
|
||||
sandbox.stub(feed, "getScreenshot");
|
||||
feed._tippyTopProvider.processSite = site => {
|
||||
site.tippyTopIcon = "icon.png";
|
||||
site.backgroundColor = "#fff";
|
||||
return site;
|
||||
};
|
||||
await feed.refresh(action);
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
assert.notCalled(feed.getScreenshot);
|
||||
});
|
||||
});
|
||||
describe("getScreenshot", () => {
|
||||
it("should call Screenshots.getScreenshotForURL with the right url", async () => {
|
||||
|
@ -146,18 +239,6 @@ describe("Top Sites Feed", () => {
|
|||
});
|
||||
describe("#onAction", () => {
|
||||
const newTabAction = {type: at.NEW_TAB_LOAD, meta: {fromTarget: "target"}};
|
||||
it("should call refresh if there are not enough sites on NEW_TAB_LOAD", () => {
|
||||
feed.store.getState = function() { return {TopSites: {rows: []}}; };
|
||||
sinon.stub(feed, "refresh");
|
||||
feed.onAction(newTabAction);
|
||||
assert.calledWith(feed.refresh, newTabAction.meta.fromTarget);
|
||||
});
|
||||
it("should call refresh if there are not sites on NEW_TAB_LOAD, not counting defaults", () => {
|
||||
feed.store.getState = function() { return {TopSites: {rows: [{url: "foo.com"}, ...DEFAULT_TOP_SITES]}}; };
|
||||
sinon.stub(feed, "refresh");
|
||||
feed.onAction(newTabAction);
|
||||
assert.calledWith(feed.refresh, newTabAction.meta.fromTarget);
|
||||
});
|
||||
it("should not call refresh if there are enough sites on NEW_TAB_LOAD", () => {
|
||||
feed.lastUpdated = Date.now();
|
||||
sinon.stub(feed, "refresh");
|
||||
|
|
|
@ -14,6 +14,7 @@ describe("Top Stories Feed", () => {
|
|||
let instance;
|
||||
let clock;
|
||||
let globals;
|
||||
let shortURLStub;
|
||||
|
||||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
|
||||
|
@ -32,7 +33,12 @@ describe("Top Stories Feed", () => {
|
|||
globals.set("Services", {locale: {getRequestedLocale: () => "en-CA"}});
|
||||
clock = sinon.useFakeTimers();
|
||||
|
||||
({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID, FEED_PREF, SECTION_OPTIONS_PREF} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
|
||||
shortURLStub = sinon.stub().callsFake(site => site.url);
|
||||
|
||||
({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID, FEED_PREF, SECTION_OPTIONS_PREF} = injector({
|
||||
"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
|
||||
"common/ShortURL.jsm": {shortURL: shortURLStub}
|
||||
}));
|
||||
instance = new TopStoriesFeed();
|
||||
instance.store = {getState() { return {}; }, dispatch: sinon.spy()};
|
||||
instance.storiesLastUpdated = 0;
|
||||
|
@ -161,7 +167,8 @@ describe("Top Stories Feed", () => {
|
|||
"image": "image-url",
|
||||
"referrer": "referrer",
|
||||
"url": "rec-url",
|
||||
"eTLD": ""
|
||||
"eTLD": "",
|
||||
"hostname": "rec-url"
|
||||
}];
|
||||
|
||||
instance.stories_endpoint = "stories-endpoint";
|
||||
|
@ -170,6 +177,7 @@ describe("Top Stories Feed", () => {
|
|||
await instance.fetchStories();
|
||||
|
||||
assert.calledOnce(fetchStub);
|
||||
assert.calledOnce(shortURLStub);
|
||||
assert.calledWithExactly(fetchStub, instance.stories_endpoint);
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE);
|
||||
|
|
|
@ -28,6 +28,10 @@ overrider.set({
|
|||
fetch() {},
|
||||
Preferences: FakePrefs,
|
||||
Services: {
|
||||
telemetry: {
|
||||
canRecordBase: true,
|
||||
canRecordExtended: true
|
||||
},
|
||||
locale: {
|
||||
getAppLocalesAsLangTags() {},
|
||||
getRequestedLocale() {},
|
||||
|
|