Bug 1069962: show a promo area in the contacts list for Gravatars. Show Gravatar icons upon granting permission. r=Standard8

This commit is contained in:
Mike de Boer 2015-02-27 11:08:28 +01:00
parent e914ee6242
commit 58485f44cb
6 changed files with 235 additions and 5 deletions

View File

@ -1718,15 +1718,17 @@ pref("loop.debug.dispatcher", false);
pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);
#ifdef DEBUG
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
#else
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
#endif
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
pref("loop.fxa_oauth.tokendata", "");
pref("loop.fxa_oauth.profile", "");
pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");
pref("loop.contacts.gravatars.show", false);
pref("loop.contacts.gravatars.promo", true);
// serverURL to be assigned by services team
pref("services.push.serverURL", "wss://push.services.mozilla.com/");

View File

@ -117,6 +117,15 @@ const cloneValueInto = function(value, targetWindow) {
return clone;
};
/**
* Get the two-digit hexadecimal code for a byte
*
* @param {byte} charCode
*/
const toHexString = function(charCode) {
return ("0" + charCode.toString(16)).slice(-2);
};
/**
* Inject any API containing _only_ function properties into the given window.
*
@ -740,6 +749,43 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* Compose a URL pointing to the location of an avatar by email address.
* At the moment we use the Gravatar service to match email addresses with
* avatars. This might change in the future as avatars might come from another
* source.
*
* @param {String} emailAddress Users' email address
* @param {Number} size Size of the avatar image to return in pixels.
* Optional. Default value: 40.
* @return the URL pointing to an avatar matching the provided email address.
*/
getUserAvatar: {
enumerable: true,
writable: true,
value: function(emailAddress, size = 40) {
const kEmptyGif = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
if (!emailAddress || !MozLoopService.getLoopPref("contacts.gravatars.show")) {
return kEmptyGif;
}
// Do the MD5 dance.
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(Ci.nsICryptoHash.MD5);
let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stringStream.data = emailAddress.trim().toLowerCase();
hasher.updateFromStream(stringStream, -1);
let hash = hasher.finish(false);
// Convert the binary hash data to a hex string.
let md5Email = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
// Compose the Gravatar URL.
return "https://www.gravatar.com/avatar/" + md5Email + ".jpg?default=blank&s=" + size;
}
},
/**
* Associates a session-id and a call-id with a window for debugging.
*

View File

@ -229,3 +229,44 @@
.contact-form > .button-group {
margin-top: 1rem;
}
.contacts-gravatar-promo {
position: relative;
border: 1px dashed #c1c1c1;
border-radius: 2px;
background-color: #fbfbfb;
padding: 10px;
margin-top: 10px;
}
.contacts-gravatar-promo > p {
margin-top: 2px;
margin-bottom: 8px;
margin-right: 4px;
word-wrap: break-word;
}
body[dir=rtl] .contacts-gravatar-promo > p {
margin-right: 0;
margin-left: 4px;
}
.contacts-gravatar-promo > p > a {
color: #0295df;
text-decoration: none;
}
.contacts-gravatar-promo > p > a:hover {
text-decoration: underline;
}
.contacts-gravatar-promo > .button-close {
position: absolute;
top: 8px;
right: 8px;
}
body[dir=rtl] .contacts-gravatar-promo > .button-close {
right: auto;
left: 8px;
}

View File

@ -390,6 +390,23 @@ body {
color: #fff;
}
.button-close {
background-color: transparent;
background-image: url(../shared/img/icons-10x10.svg#close);
background-repeat: no-repeat;
background-size: 8px 8px;
border: none;
padding: 0;
height: 8px;
width: 8px;
}
.button-close:hover,
.button-close:hover:active {
background-color: transparent;
border: none;
}
/* Dropdown menu */
.dropdown {

View File

@ -81,6 +81,59 @@ loop.contacts = (function(_, mozL10n) {
contact[field][0].value = value;
};
const GravatarPromo = React.createClass({displayName: "GravatarPromo",
propTypes: {
handleUse: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
showMe: navigator.mozLoop.getLoopPref("contacts.gravatars.promo") &&
!navigator.mozLoop.getLoopPref("contacts.gravatars.show")
};
},
handleCloseButtonClick: function() {
navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
this.setState({ showMe: false });
},
handleUseButtonClick: function() {
navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
navigator.mozLoop.setLoopPref("contacts.gravatars.show", true);
this.setState({ showMe: false });
this.props.handleUse();
},
render: function() {
if (!this.state.showMe) {
return null;
}
let privacyUrl = navigator.mozLoop.getLoopPref("legal.privacy_url");
let message = mozL10n.get("gravatars_promo_message", {
"learn_more": React.renderToStaticMarkup(
React.createElement("a", {href: privacyUrl, target: "_blank"},
mozL10n.get("gravatars_promo_message_learnmore")
)
)
});
return (
React.createElement("div", {className: "contacts-gravatar-promo"},
React.createElement(Button, {additionalClass: "button-close", onClick: this.handleCloseButtonClick}),
React.createElement("p", {dangerouslySetInnerHTML: {__html: message}}),
React.createElement(ButtonGroup, null,
React.createElement(Button, {caption: mozL10n.get("gravatars_promo_button_nothanks"),
onClick: this.handleCloseButtonClick}),
React.createElement(Button, {caption: mozL10n.get("gravatars_promo_button_use"),
additionalClass: "button-accept",
onClick: this.handleUseButtonClick})
)
)
);
}
});
const ContactDropdown = React.createClass({displayName: "ContactDropdown",
propTypes: {
handleAction: React.PropTypes.func.isRequired,
@ -232,7 +285,9 @@ loop.contacts = (function(_, mozL10n) {
return (
React.createElement("li", {className: contactCSSClass, onMouseLeave: this.hideDropdownMenu},
React.createElement("div", {className: "avatar"}),
React.createElement("div", {className: "avatar"},
React.createElement("img", {src: navigator.mozLoop.getUserAvatar(email.value)})
),
React.createElement("div", {className: "details"},
React.createElement("div", {className: "username"}, React.createElement("strong", null, names.firstName), " ", names.lastName,
React.createElement("i", {className: cx({"icon icon-google": this.props.contact.category[0] == "google"})}),
@ -462,6 +517,12 @@ loop.contacts = (function(_, mozL10n) {
}
},
handleUseGravatar: function() {
// We got permission to use Gravatar icons now, so we need to redraw the
// list entirely to show them.
this.refresh();
},
sortContacts: function(contact1, contact2) {
let comp = contact1.name[0].localeCompare(contact2.name[0]);
if (comp !== 0) {
@ -522,7 +583,8 @@ loop.contacts = (function(_, mozL10n) {
React.createElement("input", {className: "contact-filter",
placeholder: mozL10n.get("contacts_search_placesholder"),
valueLink: this.linkState("filter")})
: null
: null,
React.createElement(GravatarPromo, {handleUse: this.handleUseGravatar})
),
React.createElement("ul", {className: "contact-list"},
shownContacts.available ?

View File

@ -81,6 +81,59 @@ loop.contacts = (function(_, mozL10n) {
contact[field][0].value = value;
};
const GravatarPromo = React.createClass({
propTypes: {
handleUse: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
showMe: navigator.mozLoop.getLoopPref("contacts.gravatars.promo") &&
!navigator.mozLoop.getLoopPref("contacts.gravatars.show")
};
},
handleCloseButtonClick: function() {
navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
this.setState({ showMe: false });
},
handleUseButtonClick: function() {
navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
navigator.mozLoop.setLoopPref("contacts.gravatars.show", true);
this.setState({ showMe: false });
this.props.handleUse();
},
render: function() {
if (!this.state.showMe) {
return null;
}
let privacyUrl = navigator.mozLoop.getLoopPref("legal.privacy_url");
let message = mozL10n.get("gravatars_promo_message", {
"learn_more": React.renderToStaticMarkup(
<a href={privacyUrl} target="_blank">
{mozL10n.get("gravatars_promo_message_learnmore")}
</a>
)
});
return (
<div className="contacts-gravatar-promo">
<Button additionalClass="button-close" onClick={this.handleCloseButtonClick}/>
<p dangerouslySetInnerHTML={{__html: message}}></p>
<ButtonGroup>
<Button caption={mozL10n.get("gravatars_promo_button_nothanks")}
onClick={this.handleCloseButtonClick}/>
<Button caption={mozL10n.get("gravatars_promo_button_use")}
additionalClass="button-accept"
onClick={this.handleUseButtonClick}/>
</ButtonGroup>
</div>
);
}
});
const ContactDropdown = React.createClass({
propTypes: {
handleAction: React.PropTypes.func.isRequired,
@ -232,7 +285,9 @@ loop.contacts = (function(_, mozL10n) {
return (
<li className={contactCSSClass} onMouseLeave={this.hideDropdownMenu}>
<div className="avatar" />
<div className="avatar">
<img src={navigator.mozLoop.getUserAvatar(email.value)} />
</div>
<div className="details">
<div className="username"><strong>{names.firstName}</strong> {names.lastName}
<i className={cx({"icon icon-google": this.props.contact.category[0] == "google"})} />
@ -462,6 +517,12 @@ loop.contacts = (function(_, mozL10n) {
}
},
handleUseGravatar: function() {
// We got permission to use Gravatar icons now, so we need to redraw the
// list entirely to show them.
this.refresh();
},
sortContacts: function(contact1, contact2) {
let comp = contact1.name[0].localeCompare(contact2.name[0]);
if (comp !== 0) {
@ -523,6 +584,7 @@ loop.contacts = (function(_, mozL10n) {
placeholder={mozL10n.get("contacts_search_placesholder")}
valueLink={this.linkState("filter")} />
: null }
<GravatarPromo handleUse={this.handleUseGravatar}/>
</div>
<ul className="contact-list">
{shownContacts.available ?