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

Commit

Permalink
feat (systemaddon): #2944 add tippy top icons
Browse files Browse the repository at this point in the history
  • Loading branch information
rlr committed Aug 1, 2017
1 parent 2445903 commit 202375f
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ common/vendor.js
*.update.rdf
system-addon/data/content/activity-stream.bundle.js
system-addon/data/content/activity-stream.css
system-addon/data/content/tippytop
system-addon/data/locales.json
pocket.json
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,12 @@
"bundlestats": "NODE_ENV=production webpack --json | webpack-bundle-size-analyzer",
"mochitest": "(cd ../mozilla-central && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest )",
"mochitest-debug": "(cd ../mozilla-central && ./mach mochitest --jsdebugger browser/extensions/activity-stream/test/functional/mochitest )",
"cleanmc": "rimraf ../mozilla-central/browser/extensions/activity-stream",
"cleanTippyTop": "rimraf system-addon/data/content/tippytop",
"copyTippyTopManifest": "cpx \"node_modules/tippy-top-sites/top_sites.json\" system-addon/data/content/tippytop",
"copyTippyTopImages": "cpx \"node_modules/tippy-top-sites/images/**/*\" system-addon/data/content/tippytop/images",
"buildmc": "npm-run-all buildmc:*",
"prebuildmc": "rimraf ../mozilla-central/browser/extensions/activity-stream",
"prebuildmc": "npm run cleanmc && npm run cleanTippyTop && npm run copyTippyTopManifest && npm run copyTippyTopImages",
"buildmc:webpack": "webpack --config webpack.system-addon.config.js",
"buildmc:css": "node-sass system-addon/content-src/activity-stream.scss -o system-addon/data/content",
"buildmc:locales": "pontoon-to-json --src locales --dest system-addon/data",
Expand Down
16 changes: 14 additions & 2 deletions system-addon/content-src/components/TopSites/TopSites.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,24 @@ class TopSite extends React.Component {
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")};
const tippyTopIcon = link.tippyTopIcon;
const showScreenshot = !tippyTopIcon;
let style;
if (showScreenshot) {
style = {backgroundImage: (link.screenshot ? `url(${link.screenshot})` : "none")};
} else {
style = {backgroundImage: `url(${tippyTopIcon})`, backgroundColor: link.backgroundColor || "none"};
}
return (<li className={topSiteOuterClassName} key={link.guid || link.url}>
<a href={link.url} onClick={this.onLinkClick}>
<div className="tile" aria-hidden={true}>
<span className="letter-fallback">{title[0]}</span>
<div className={screenshotClassName} style={style} />
{showScreenshot &&
<div className={screenshotClassName} style={style} />
}
{!showScreenshot &&
<div className="tippy-top-icon" style={style} />
}
</div>
<div className={`title ${link.isPinned ? "pinned" : ""}`}>
{link.isPinned && <div className="icon icon-pin-small" />}
Expand Down
14 changes: 14 additions & 0 deletions system-addon/content-src/components/TopSites/_TopSites.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
$top-sites-title-height: 30px;
$top-sites-vertical-space: 18px;
$screenshot-size: cover;
$tippy-top-size: 80px;

list-style: none;
margin: 0;
Expand Down Expand Up @@ -118,6 +119,19 @@
}
}

.tippy-top-icon {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
border-radius: $top-sites-border-radius;
box-shadow: inset $inner-box-shadow;
background-position: center center;
background-size: $tippy-top-size;
background-repeat: no-repeat;
}

.title {
height: $top-sites-title-height;
line-height: $top-sites-title-height;
Expand Down
66 changes: 66 additions & 0 deletions system-addon/lib/TippyTopProvider.jsm
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* 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 = {};
}
async init() {
// Load the Tippy Top sites from the json manifest.
fetch(TIPPYTOP_JSON_PATH)
.then(response => response.json())
.then(sites => {
for (let site of sites) {
if ("url" in site) {
this._sitesByDomain[getDomain(site.url)] = site;
}
if ("urls" in site) {
for (let url of site.urls) {
this._sitesByDomain[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;
}

let key = getDomain(site.url);
if (key && key in this._sitesByDomain) {
let tippyTop = this._sitesByDomain[key];
site.tippyTopIcon = TIPPYTOP_URL_PREFIX + tippyTop.image_url;
site.backgroundColor = tippyTop.background_color;
}
return site;
}
};

this.EXPORTED_SYMBOLS = ["TippyTopProvider"];
12 changes: 11 additions & 1 deletion system-addon/lib/TopSitesFeed.jsm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");

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

XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
Expand All @@ -22,8 +23,11 @@ const DEFAULT_TOP_SITES = [];
this.TopSitesFeed = class TopSitesFeed {
constructor() {
this.lastUpdated = 0;
this._tippyTopProvider = new TippyTopProvider();
}
init() {
this._tippyTopProvider.init();

// Add default sites if any based on the pref
let sites = new Prefs().get("default.sites");
if (sites) {
Expand Down Expand Up @@ -65,9 +69,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 {
Expand Down
10 changes: 10 additions & 0 deletions system-addon/test/unit/content-src/components/TopSites.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,16 @@ describe("<TopSite>", () => {
assert.deepEqual(linkMenuProps.options,
["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"]);
});
it("should render the tippy top icon if provided", () => {
link.tippyTopIcon = "foo.png";
link.backgroundColor = "#FFFFFF";
const wrapper = shallow(<TopSite link={link} />);
assert.equal(wrapper.find(".screenshot").length, 0);
const tippyTop = wrapper.find(".tippy-top-icon");
assert.propertyVal(tippyTop.props().style, "backgroundImage", "url(foo.png)");
assert.propertyVal(tippyTop.props().style, "backgroundColor", "#FFFFFF");
});

describe("#trackClick", () => {
it("should call dispatch when the link is clicked", () => {
const dispatch = sinon.stub();
Expand Down
71 changes: 71 additions & 0 deletions system-addon/test/unit/lib/TippyTopProvider.test.js
Original file line number Diff line number Diff line change
@@ -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");
});
});
20 changes: 19 additions & 1 deletion system-addon/test/unit/lib/TopSitesFeed.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ 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_SCREENSHOT = "data123";

function FakeTippyTopProvider() {}
FakeTippyTopProvider.prototype = {
init() {},
processSite(site) { return site; }
};

describe("Top Sites Feed", () => {
let TopSitesFeed;
let DEFAULT_TOP_SITES;
Expand Down Expand Up @@ -37,7 +43,8 @@ describe("Top Sites Feed", () => {
({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
"common/Reducers.jsm": {insertPinned},
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot}
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
"lib/TippyTopProvider.jsm": {TippyTopProvider: FakeTippyTopProvider}
}));
feed = new TopSitesFeed();
feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
Expand Down Expand Up @@ -123,6 +130,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 () => {
Expand Down
10 changes: 9 additions & 1 deletion yamscripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,17 @@ scripts:
# mochitest somewhere.
mochitest-debug: (cd ../mozilla-central && ./mach mochitest --jsdebugger browser/extensions/activity-stream/test/functional/mochitest )

cleanmc: rimraf ../mozilla-central/browser/extensions/activity-stream

cleanTippyTop: rimraf system-addon/data/content/tippytop

copyTippyTopManifest: cpx "node_modules/tippy-top-sites/top_sites.json" system-addon/data/content/tippytop

copyTippyTopImages: cpx "node_modules/tippy-top-sites/images/**/*" system-addon/data/content/tippytop/images

# buildmc: Export the bootstraped add-on to mozilla central
buildmc:
pre: rimraf ../mozilla-central/browser/extensions/activity-stream
pre: =>cleanmc && =>cleanTippyTop && =>copyTippyTopManifest && =>copyTippyTopImages
webpack: webpack --config webpack.system-addon.config.js
css: node-sass system-addon/content-src/activity-stream.scss -o system-addon/data/content
locales: pontoon-to-json --src locales --dest system-addon/data
Expand Down

0 comments on commit 202375f

Please sign in to comment.