Skip to content
This repository has been archived by the owner on Feb 29, 2020. It is now read-only.

Commit

Permalink
Merge pull request #3787 from rlr/tippy-top
Browse files Browse the repository at this point in the history
feat (tippytop): #3218 implement TippyTopFeed with remote manifest fetching
  • Loading branch information
rlr committed Nov 10, 2017
2 parents b5a4e79 + 80baa63 commit 3e6d701
Show file tree
Hide file tree
Showing 9 changed files with 381 additions and 6 deletions.
1 change: 1 addition & 0 deletions system-addon/common/Actions.jsm
Expand Up @@ -54,6 +54,7 @@ for (const type of [
"PLACES_LINK_BLOCKED",
"PREFS_INITIAL_VALUES",
"PREF_CHANGED",
"RICH_ICON_MISSING",
"SAVE_SESSION_PERF_DATA",
"SAVE_TO_POCKET",
"SCREENSHOT_UPDATED",
Expand Down
11 changes: 11 additions & 0 deletions system-addon/lib/ActivityStream.jsm
Expand Up @@ -19,6 +19,7 @@ const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
const {SnippetsFeed} = Cu.import("resource://activity-stream/lib/SnippetsFeed.jsm", {});
const {SystemTickFeed} = Cu.import("resource://activity-stream/lib/SystemTickFeed.jsm", {});
const {TelemetryFeed} = Cu.import("resource://activity-stream/lib/TelemetryFeed.jsm", {});
const {FaviconFeed} = Cu.import("resource://activity-stream/lib/FaviconFeed.jsm", {});
const {TopSitesFeed} = Cu.import("resource://activity-stream/lib/TopSitesFeed.jsm", {});
const {TopStoriesFeed} = Cu.import("resource://activity-stream/lib/TopStoriesFeed.jsm", {});
const {HighlightsFeed} = Cu.import("resource://activity-stream/lib/HighlightsFeed.jsm", {});
Expand Down Expand Up @@ -133,6 +134,10 @@ const PREFS_CONFIG = new Map([
["section.topstories.showDisclaimer", {
title: "Boolean flag that decides whether or not to show the topstories disclaimer.",
value: true
}],
["tippyTop.service.endpoint", {
title: "Tippy Top service manifest url",
value: "https://s3.amazonaws.com/activitystream-dev-default-resources-s3bucket-1qw8m6s29v3dq/tippytop/icons.json"
}]
]);

Expand Down Expand Up @@ -206,6 +211,12 @@ const FEEDS_DATA = [
title: "Relays telemetry-related actions to PingCentre",
value: true
},
{
name: "favicon",
factory: () => new FaviconFeed(),
title: "Fetches tippy top manifests from remote service",
value: true
},
{
name: "topsites",
factory: () => new TopSitesFeed(),
Expand Down
136 changes: 136 additions & 0 deletions system-addon/lib/FaviconFeed.jsm
@@ -0,0 +1,136 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const {utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

Cu.importGlobalProperties(["fetch", "URL"]);

const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {PersistentCache} = Cu.import("resource://activity-stream/lib/PersistentCache.jsm", {});
const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
const {getDomain} = Cu.import("resource://activity-stream/lib/TippyTopProvider.jsm", {});

XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");

const TIPPYTOP_UPDATE_TIME = 24 * 60 * 60 * 1000; // 24 hours

this.FaviconFeed = class FaviconFeed {
constructor() {
this.tippyTopLastUpdated = 0;
this.cache = new PersistentCache("tippytop", true);
this.prefs = new Prefs();
this._sitesByDomain = null;
}

async loadCachedData() {
const data = await this.cache.get("sites");
if (data && data._timestamp) {
this._sitesByDomain = data;
this.tippyTopLastUpdated = data._timestamp;
}
}

async maybeRefresh() {
if (Date.now() - this.tippyTopLastUpdated >= TIPPYTOP_UPDATE_TIME) {
await this.refresh();
}
}

async refresh() {
let headers = new Headers();
if (this._sitesByDomain && this._sitesByDomain._etag) {
headers.set("If-None-Match", this._sitesByDomain._etag);
}
let {data, etag, status} = await this.loadFromURL(this.prefs.get("tippyTop.service.endpoint"), headers);
if (status === 304) {
// Not modified, we are done. No need to dispatch actions or update cache. We'll check again in 24+ hours.
this.tippyTopLastUpdated = Date.now();
return;
}
if (status === 200 && data) {
let sitesByDomain = this._sitesArrayToObjectByDomain(data);
sitesByDomain._etag = etag;
this.tippyTopLastUpdated = sitesByDomain._timestamp = Date.now();
this.cache.set("sites", sitesByDomain);
this._sitesByDomain = sitesByDomain;
}
}

async loadFromURL(url, headers) {
let data = [];
let etag;
let status;
try {
let response = await fetch(url, {headers});
status = response.status;
if (status === 200) {
data = await response.json();
etag = response.headers.get("ETag");
}
} catch (error) {
Cu.reportError(`Failed to load tippy top manifest from ${url}`);
}
return {data, etag, status};
}

_sitesArrayToObjectByDomain(sites) {
let sitesByDomain = {};
for (const site of sites) {
// 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 || []) {
sitesByDomain[getDomain(url)] = site;
}
}
return sitesByDomain;
}

getSitesByDomain() {
// return an already loaded object or a promise for that object
return this._sitesByDomain || (this._sitesByDomain = new Promise(async resolve => {
await this.loadCachedData();
await this.maybeRefresh();
resolve(this._sitesByDomain);
}));
}

async fetchIcon(url) {
const sitesByDomain = await this.getSitesByDomain();
const domain = getDomain(url);
if (domain in sitesByDomain) {
let iconUri = Services.io.newURI(sitesByDomain[domain].image_url);
// The #tippytop is to be able to identify them for telemetry.
iconUri.ref = "tippytop";
PlacesUtils.favicons.setAndFetchFaviconForPage(
Services.io.newURI(url),
iconUri,
false,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
null,
Services.scriptSecurityManager.getSystemPrincipal()
);
}
}

onAction(action) {
switch (action.type) {
case at.SYSTEM_TICK:
if (this._sitesByDomain) {
// No need to refresh if we haven't been initialized.
this.maybeRefresh();
}
break;
case at.RICH_ICON_MISSING:
this.fetchIcon(action.data.url);
break;
}
}
};

this.EXPORTED_SYMBOLS = ["FaviconFeed"];
3 changes: 2 additions & 1 deletion system-addon/lib/TippyTopProvider.jsm
Expand Up @@ -16,6 +16,7 @@ function getDomain(url) {
}
return domain;
}
this.getDomain = getDomain;

function getPath(url) {
return new URL(url).pathname;
Expand Down Expand Up @@ -60,4 +61,4 @@ this.TippyTopProvider = class TippyTopProvider {
}
};

this.EXPORTED_SYMBOLS = ["TippyTopProvider"];
this.EXPORTED_SYMBOLS = ["TippyTopProvider", "getDomain"];
21 changes: 17 additions & 4 deletions system-addon/lib/TopSitesFeed.jsm
Expand Up @@ -161,11 +161,17 @@ this.TopSitesFeed = class TopSitesFeed {
* Get an image for the link preferring tippy top, rich favicon, screenshots.
*/
async _fetchIcon(link) {
// Check for tippy top icon or a rich icon.
// Check for tippy top icon and rich icon.
this._tippyTopProvider.processSite(link);
if (!link.tippyTopIcon &&
(!link.favicon || link.faviconSize < MIN_FAVICON_SIZE) &&
!link.screenshot) {
let hasTippyTop = !!link.tippyTopIcon;
let hasRichIcon = link.favicon && link.faviconSize >= MIN_FAVICON_SIZE;

if (!hasTippyTop && !hasRichIcon) {
this._requestRichIcon(link.url);
}

// Request a screenshot if needed.
if (!hasTippyTop && !hasRichIcon && !link.screenshot) {
const {url} = link;
await Screenshots.maybeCacheScreenshot(link, url, "screenshot",
screenshot => this.store.dispatch(ac.BroadcastToContent({
Expand All @@ -175,6 +181,13 @@ this.TopSitesFeed = class TopSitesFeed {
}
}

_requestRichIcon(url) {
this.store.dispatch({
type: at.RICH_ICON_MISSING,
data: {url}
});
}

/**
* Inform others that top sites data has been updated due to pinned changes.
*/
Expand Down
5 changes: 5 additions & 0 deletions system-addon/test/unit/lib/ActivityStream.test.js
Expand Up @@ -21,6 +21,7 @@ describe("ActivityStream", () => {
"lib/SnippetsFeed.jsm": {SnippetsFeed: Fake},
"lib/SystemTickFeed.jsm": {SystemTickFeed: Fake},
"lib/TelemetryFeed.jsm": {TelemetryFeed: Fake},
"lib/FaviconFeed.jsm": {FaviconFeed: Fake},
"lib/TopSitesFeed.jsm": {TopSitesFeed: Fake},
"lib/TopStoriesFeed.jsm": {TopStoriesFeed: Fake},
"lib/HighlightsFeed.jsm": {HighlightsFeed: Fake}
Expand Down Expand Up @@ -148,6 +149,10 @@ describe("ActivityStream", () => {
const feed = as.feeds.get("feeds.systemtick")();
assert.instanceOf(feed, Fake);
});
it("should create a Favicon feed", () => {
const feed = as.feeds.get("feeds.favicon")();
assert.instanceOf(feed, Fake);
});
});
describe("_updateDynamicPrefs topstories default value", () => {
it("should be false with no geo/locale", () => {
Expand Down

0 comments on commit 3e6d701

Please sign in to comment.