Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
Additional check of privacy.useGravatar for gh-profile-image component (
Browse files Browse the repository at this point in the history
#761)

closes TryGhost/Ghost#8612

* Additional check of privacy.useGravatar for gh-profile-image component
- added a check for privacy.useGravatar flag
- checked: tests are OK

* fix other issues in gh-profile-image
- we had CPs with side-effects 🤢
  - replace CP with basic properties that can be set within the component
  - use `didReceiveAttrs` and `ember-concurrency` to debounce changes to the email property and make the functionality easier to reason about
- fix the broken fade-in animation when the avatar changes
- fix tests - `.to.be.blank` was always returning true, replaced with `to.be.empty` which caused the tests to fail properly then replaced them with the expectations for the actual values
  • Loading branch information
letsjustfixit authored and aileen committed Jul 6, 2017
1 parent 9e61497 commit 5cd6335
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 81 deletions.
107 changes: 56 additions & 51 deletions app/components/gh-profile-image.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import AjaxService from 'ember-ajax/services/ajax';
import Component from 'ember-component';
import computed, {notEmpty} from 'ember-computed';
import injectService from 'ember-service/inject';
import request from 'ember-ajax/request';
import run from 'ember-runloop';
import {htmlSafe} from 'ember-string';
import {isBlank} from 'ember-utils';
import {isNotFoundError} from 'ember-ajax/errors';
import {task, timeout} from 'ember-concurrency';

const ANIMATION_TIMEOUT = 1000;

/**
* A component to manage a user profile image. By default it just handles picture uploads,
Expand All @@ -26,69 +26,29 @@ export default Component.extend({
size: 180,
debounce: 300,

validEmail: '',
hasUploadedImage: false,
ajax: AjaxService.create(),

config: injectService(),
ghostPaths: injectService(),

displayGravatar: notEmpty('validEmail'),
placeholderStyle: htmlSafe('background-image: url()'),
avatarStyle: htmlSafe('display: none'),

_defaultImageUrl: '',

init() {
this._super(...arguments);
// Fire this immediately in case we're initialized with a valid email
this.trySetValidEmail();
},

defaultImage: computed('ghostPaths', function () {
let url = `${this.get('ghostPaths.assetRoot')}/img/user-image.png`;
return htmlSafe(`background-image: url(${url})`);
}),

trySetValidEmail() {
if (!this.get('isDestroyed')) {
let email = this.get('email');
this.set('validEmail', validator.isEmail(email) ? email : '');
}
this._defaultImageUrl = `${this.get('ghostPaths.assetRoot')}img/user-image.png`;
this._setPlaceholderImage(this._defaultImageUrl);
},

didReceiveAttrs() {
didInsertElement() {
this._super(...arguments);
let timeout = parseInt(this.get('throttle') || this.get('debounce'));
run.debounce(this, 'trySetValidEmail', timeout);
},

imageBackground: computed('validEmail', 'size', function () {
let email = this.get('validEmail');
let size = this.get('size');
let style = '';

if (!isBlank(email)) {
let gravatarUrl = `//www.gravatar.com/avatar/${window.md5(email)}?s=${size}&d=404`;

this.get('ajax').request(gravatarUrl)
.catch((error) => {
let defaultImageUrl = `url("${this.get('ghostPaths.assetRoot')}/img/user-image.png")`;

if (isNotFoundError(error)) {
this.$('.placeholder-img')[0].style.backgroundImage = htmlSafe(defaultImageUrl);
} else {
this.$('.placeholder-img')[0].style.backgroundImage = 'url()';
}
});

style = `background-image: url(${gravatarUrl})`;
}
return htmlSafe(style);
}),

didInsertElement() {
let size = this.get('size');
let uploadElement = this.$('.js-file-input');

this._super(...arguments);

// while theoretically the 'add' and 'processalways' functions could be
// added as properties of the hash passed to fileupload(), for some reason
// they needed to be placed in an on() call for the add method to work correctly
Expand All @@ -105,6 +65,51 @@ export default Component.extend({
.on('fileuploadprocessalways', run.bind(this, this.triggerPreview));
},

didReceiveAttrs() {
this._super(...arguments);

if (this.get('config.useGravatar')) {
this.get('setGravatar').perform();
}
},

setGravatar: task(function* () {
yield timeout(this.get('debounce'));

let email = this.get('email');

if (validator.isEmail(email)) {
let size = this.get('size');
let gravatarUrl = `//www.gravatar.com/avatar/${window.md5(email)}?s=${size}&d=404`;

try {
// HEAD request is needed otherwise jquery attempts to process
// binary data as JSON and throws an error
yield request(gravatarUrl, {type: 'HEAD'});
// gravatar exists so switch style and let browser load it
this._setAvatarImage(gravatarUrl);
// wait for fade-in animation to finish before removing placeholder
yield timeout(ANIMATION_TIMEOUT);
this._setPlaceholderImage('');

} catch (e) {
// gravatar doesn't exist so make sure we're still showing the placeholder
this._setPlaceholderImage(this._defaultImageUrl);
// then make sure the avatar isn't visible
this._setAvatarImage('');
}
}
}).restartable(),

_setPlaceholderImage(url) {
this.set('placeholderStyle', htmlSafe(`background-image: url(${url});`));
},

_setAvatarImage(url) {
let display = url ? 'block' : 'none';
this.set('avatarStyle', htmlSafe(`background-image: url(${url}); display: ${display}`));
},

willDestroyElement() {
let $input = this.$('.js-file-input');

Expand Down
8 changes: 4 additions & 4 deletions app/templates/components/gh-profile-image.hbs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<figure class="account-image js-file-upload">
{{#unless hasUploadedImage}}
<div class="placeholder-img" style={{defaultImage}}></div>
<div id="account-image" class="gravatar-img" style={{imageBackground}}>
<span class="sr-only">User image</span>
</div>
<div class="placeholder-img" style={{placeholderStyle}}></div>
<div id="account-image" class="gravatar-img" style={{avatarStyle}}>
<span class="sr-only">User image</span>
</div>
{{/unless}}

<div class="js-img-preview"></div>
Expand Down
80 changes: 54 additions & 26 deletions tests/integration/components/gh-profile-image-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import wait from 'ember-test-helpers/wait';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {timeout} from 'ember-concurrency';

let pathsStub = Service.extend({
url: {
Expand All @@ -24,14 +25,26 @@ const stubKnownGravatar = function (server) {
server.get('http://www.gravatar.com/avatar/:md5', function () {
return [200, {'Content-Type': 'image/png'}, ''];
});

server.head('http://www.gravatar.com/avatar/:md5', function () {
return [200, {'Content-Type': 'image/png'}, ''];
});
};

const stubUnknownGravatar = function (server) {
server.get('http://www.gravatar.com/avatar/:md5', function () {
return [404, {}, ''];
});

server.head('http://www.gravatar.com/avatar/:md5', function () {
return [404, {}, ''];
});
};

let configStubuseGravatar = Service.extend({
useGravatar: true
});

describe('Integration: Component: gh-profile-image', function () {
setupComponentTest('gh-profile-image', {
integration: true
Expand All @@ -42,6 +55,8 @@ describe('Integration: Component: gh-profile-image', function () {
beforeEach(function () {
this.register('service:ghost-paths', pathsStub);
this.inject.service('ghost-paths', {as: 'ghost-paths'});
this.register('service:config', configStubuseGravatar);
this.inject.service('config', {as: 'config'});

server = new Pretender();
stubKnownGravatar(server);
Expand Down Expand Up @@ -69,10 +84,10 @@ describe('Integration: Component: gh-profile-image', function () {
`);

expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.be.blank;
.to.equal('display: none');
});

it('renders the gravatar if valid email supplied', function (done) {
it('renders the gravatar if valid email supplied and privacy.useGravatar allows it', async function () {
let email = 'test@example.com';
let expectedUrl = `//www.gravatar.com/avatar/${md5(email)}?s=100&d=404`;

Expand All @@ -83,28 +98,42 @@ describe('Integration: Component: gh-profile-image', function () {
`);

// wait for the ajax request to complete
wait().then(() => {
expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.equal(`background-image: url(${expectedUrl})`);
done();
});
await wait();

expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.equal(`background-image: url(${expectedUrl}); display: block`);
});

it('doesn\'t render the gravatar if valid email supplied but privacy.useGravatar forbids it', async function () {
let email = 'test@example.com';

this.set('email', email);
this.set('config.useGravatar', false);

this.render(hbs`
{{gh-profile-image email=email size=100 debounce=50}}
`);

await wait();

expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.equal('display: none');
});

it('doesn\'t add background url if gravatar image doesn\'t exist', function (done) {
it('doesn\'t add background url if gravatar image doesn\'t exist', async function () {
stubUnknownGravatar(server);

this.render(hbs`
{{gh-profile-image email="test@example.com" size=100 debounce=50}}
`);

wait().then(() => {
expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.be.blank;
done();
});
await wait();

expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.equal('background-image: url(); display: none');
});

it('throttles gravatar loading as email is changed', function (done) {
it('throttles gravatar loading as email is changed', async function () {
let email = 'test@example.com';
let expectedUrl = `//www.gravatar.com/avatar/${md5(email)}?s=100&d=404`;

Expand All @@ -119,17 +148,16 @@ describe('Integration: Component: gh-profile-image', function () {
});

expect(this.$('.gravatar-img').attr('style'), '.gravatar-img background not immediately changed on email change')
.to.be.blank;

run.later(this, function () {
expect(this.$('.gravatar-img').attr('style'), '.gravatar-img background still not changed before debounce timeout')
.to.be.blank;
}, 250);

run.later(this, function () {
expect(this.$('.gravatar-img').attr('style'), '.gravatar-img background changed after debounce timeout')
.to.equal(`background-image: url(${expectedUrl})`);
done();
}, 400);
.to.equal('display: none');

await timeout(250);

expect(this.$('.gravatar-img').attr('style'), '.gravatar-img background still not changed before debounce timeout')
.to.equal('display: none');

await timeout(100);

expect(this.$('.gravatar-img').attr('style'), '.gravatar-img background changed after debounce timeout')
.to.equal(`background-image: url(${expectedUrl}); display: block`);
});
});

0 comments on commit 5cd6335

Please sign in to comment.