Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
fix(avatars): show a spinner icon when loading images with latency
Browse files Browse the repository at this point in the history
This refactors the button progress indicator into something more generic
and uses that to implement progress indicators for profile image loading.

Fixes #1527.
  • Loading branch information
zaach committed Sep 18, 2014
1 parent abf38b1 commit 690249b
Show file tree
Hide file tree
Showing 17 changed files with 124 additions and 68 deletions.
6 changes: 3 additions & 3 deletions app/scripts/lib/image-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// A utility for pre-loading images
'use strict';

define(['lib/promise'],
function (p) {
define(['underscore', 'lib/promise'],
function (_, p) {

/**
* Returns true if given "uri" has HTTP or HTTPS scheme
Expand All @@ -19,7 +19,7 @@ function (p) {

var img = new Image();
img.onerror = defer.reject;
img.onload = defer.resolve;
img.onload = _.partial(defer.resolve, img);
img.src = src;

return defer.promise;
Expand Down
19 changes: 19 additions & 0 deletions app/scripts/views/avatar_base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* 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';

define([
'underscore',
'jquery',
'lib/promise',
'views/form',
'views/decorators/progress_indicator',
],
function (_, $, p, FormView, showProgressIndicator) {
var AvatarView = FormView.extend({
});

return AvatarView;
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@

define([
'lib/promise',
'views/button_progress_indicator'
'views/progress_indicator'
],
function (p, ButtonProgressIndicator) {
function (p, ProgressIndicator) {

function showButtonProgressIndicator(handler, _el) {
function showProgressIndicator(handler, _el, _property) {
var el = _el || 'button[type=submit]';
var property = _property || '_progressIndicator';

return function () {
var self = this;
var args = arguments;

var buttonProgressIndicator = getButtonProgressIndicator.call(self);
buttonProgressIndicator.start(self.$(el));
var progressIndicator = getProgressIndicator.call(self, property);
progressIndicator.start(self.$(el));

return p()
.then(function () {
Expand All @@ -37,26 +38,26 @@ function (p, ButtonProgressIndicator) {
.then(function (value) {
// Stop the progress indicator unless the page is navigating
if (! (value && value.pageNavigation)) {
buttonProgressIndicator.done();
progressIndicator.done();
}
return value;
}, function(err) {
buttonProgressIndicator.done();
progressIndicator.done();
throw err;
});
};
}

function getButtonProgressIndicator() {
function getProgressIndicator(property) {
/*jshint validthis: true*/
var self = this;
if (! self._buttonProgressIndicator) {
self._buttonProgressIndicator = new ButtonProgressIndicator();
self.trackSubview(self._buttonProgressIndicator);
if (! self[property]) {
self[property] = new ProgressIndicator();
self.trackSubview(self[property]);
}

return self._buttonProgressIndicator;
return self[property];
}

return showButtonProgressIndicator;
return showProgressIndicator;
});
2 changes: 1 addition & 1 deletion app/scripts/views/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ define([
'lib/auth-errors',
'views/base',
'views/tooltip',
'views/decorators/button_progress_indicator',
'views/decorators/progress_indicator',
'views/decorators/notify_delayed_request',
'views/decorators/allow_only_one_submit'
],
Expand Down
6 changes: 3 additions & 3 deletions app/scripts/views/mixins/service-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
define([
'lib/promise',
'views/base',
'views/decorators/button_progress_indicator',
'views/decorators/progress_indicator',
'lib/url',
'lib/oauth-client',
'lib/assertion',
Expand All @@ -19,7 +19,7 @@ define([
'lib/session',
'lib/service-name',
'lib/channels'
], function (p, BaseView, buttonProgressIndicator, Url, OAuthClient, Assertion, OAuthErrors,
], function (p, BaseView, progressIndicator, Url, OAuthClient, Assertion, OAuthErrors,
ConfigLoader, Session, ServiceName, Channels) {
/* jshint camelcase: false */

Expand Down Expand Up @@ -156,7 +156,7 @@ define([
});
},

finishOAuthFlow: buttonProgressIndicator(function (viewOptions) {
finishOAuthFlow: progressIndicator(function (viewOptions) {
var self = this;
return this._configLoader.fetch().then(function(config) {
return self.assertionLibrary.generate(config.oauthUrl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ define([

var View = Backbone.View.extend({
tagName: 'div',
className: 'spinner-white',
className: 'spinner',

// `_count` ensures the progress indicator is only hidden if all calls
// to `start` have a matching call to `done`.
Expand All @@ -41,7 +41,7 @@ define([
*
* @method start
*/
start: function (buttonEl) {
start: function (progressEl) {
var self = this;

self._count++;
Expand All @@ -63,7 +63,7 @@ define([
self._showIndicatorTimeout = setTimeout(function () {
self._showIndicatorTimeout = null;

self.showIndicator(buttonEl);
self.showIndicator(progressEl);
}, SHOW_DELAY_MS);
},

Expand Down Expand Up @@ -118,22 +118,22 @@ define([
/**
* Show the progress indicator now
*/
showIndicator: function (buttonEl) {
buttonEl = $(buttonEl);
if (buttonEl.length) {
this._buttonEl = buttonEl;
this._buttonHTML = buttonEl.html();
buttonEl.html(this.$el);
showIndicator: function (progressEl) {
progressEl = $(progressEl);
if (progressEl.length) {
this._progressEl = progressEl;
this._progressHTML = progressEl.html();
progressEl.html(this.$el);
}
},

/**
* Remove the progress indicator now
*/
removeIndicator: function (buttonEl) {
buttonEl = this._buttonEl;
if (buttonEl && buttonEl.length) {
buttonEl.html(this._buttonHTML);
removeIndicator: function (progressEl) {
progressEl = this._progressEl;
if (progressEl && progressEl.length) {
progressEl.html(this._progressHTML);
}
},

Expand Down
19 changes: 17 additions & 2 deletions app/scripts/views/settings/avatar_camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ define([
'underscore',
'canvasToBlob',
'views/form',
'views/progress_indicator',
'stache!templates/settings/avatar_camera',
'lib/constants',
'lib/promise',
'lib/session',
'lib/auth-errors',
'lib/url'
],
function (_, canvasToBlob, FormView, Template, Constants, p, Session, AuthErrors, Url) {
function (_, canvasToBlob, FormView, ProgressIndicator, Template, Constants, p, Session, AuthErrors, Url) {
// a blank 1x1 png
var pngSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==';

Expand Down Expand Up @@ -66,6 +67,7 @@ function (_, canvasToBlob, FormView, Template, Constants, p, Session, AuthErrors
nav.msGetUserMedia;

if (! getUserMedia) {
this._avatarProgressIndicator.done();
return this.displayError(AuthErrors.toCode('NO_CAMERA'));
}

Expand All @@ -77,6 +79,9 @@ function (_, canvasToBlob, FormView, Template, Constants, p, Session, AuthErrors
audio: false
},
function(stream) {
self.$('.avatar-wrapper').append(self.video);
//self.video = self.$('#video');

self.stream = stream;
if (nav.mozGetUserMedia) {
self.video[0].mozSrcObject = stream;
Expand All @@ -87,6 +92,7 @@ function (_, canvasToBlob, FormView, Template, Constants, p, Session, AuthErrors
self.video[0].play();
},
function(err) {
self._avatarProgressIndicator.done();
self.displayError(AuthErrors.toCode('NO_CAMERA'));
}
);
Expand All @@ -96,6 +102,9 @@ function (_, canvasToBlob, FormView, Template, Constants, p, Session, AuthErrors
afterRender: function () {
var self = this;

this._avatarProgressIndicator = new ProgressIndicator();
this._avatarProgressIndicator.start(this.$('.avatar-wrapper'));

this._getMedia();

this.video = this.$('#video');
Expand All @@ -105,7 +114,12 @@ function (_, canvasToBlob, FormView, Template, Constants, p, Session, AuthErrors
this.height = 0;

self.video[0].addEventListener('canplay', function(ev){
if (!self.streaming) {
if (! self.streaming) {

if (! self.canvas) {
self.canvas = self.$('#canvas')[0];
}

var pos = self.centeredPos(self.width, self.height, self.displayLength);
self.height = self.video[0].videoHeight / (self.video[0].videoWidth / self.width);
self.video.width(self.width);
Expand All @@ -119,6 +133,7 @@ function (_, canvasToBlob, FormView, Template, Constants, p, Session, AuthErrors

self.enableSubmitIfValid();
}
self._avatarProgressIndicator.done();
}, false);
},

Expand Down
23 changes: 12 additions & 11 deletions app/scripts/views/settings/avatar_change.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ define([
'views/form',
'stache!templates/settings/avatar_change',
'lib/session',
'lib/auth-errors'
'lib/auth-errors',
'lib/image-loader'
],
function ($, _, FormView, Template, Session, AuthErrors) {
function ($, _, FormView, Template, Session, AuthErrors, ImageLoader) {

// a blank 1x1 png
var pngSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==';
Expand Down Expand Up @@ -78,17 +79,16 @@ function ($, _, FormView, Template, Session, AuthErrors) {
var self = this;
var file = e.target.files[0];

// Define our callbacks here to avoid a circular DOM reference
var imgOnload = function () {
var imgOnload = function (img) {
// Store the width and height for the cropper view
Session.set('cropImgWidth', this.width);
Session.set('cropImgHeight', this.height);
Session.set('cropImgWidth', img.width);
Session.set('cropImgHeight', img.height);
require(['draggable', 'touch-punch'], function () {
self.navigate('settings/avatar/crop');
});
};

var imgOnerrer = function () {
var imgOnerrer = function (e) {
self.navigate('settings/avatar', {
error: AuthErrors.toMessage('UNUSABLE_IMAGE')
});
Expand All @@ -103,10 +103,11 @@ function ($, _, FormView, Template, Session, AuthErrors) {
Session.set('cropImgSrc', src);
Session.set('cropImgType', file.type);

var img = new Image();
img.src = src;
img.onload = imgOnload;
img.onerror = imgOnerrer;
console.log('src', event, src);

ImageLoader.load(src)
.then(imgOnload)
.then(null, imgOnerrer);
};
reader.readAsDataURL(file);
} else {
Expand Down
21 changes: 12 additions & 9 deletions app/scripts/views/settings/avatar_gravatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ define([
'stache!templates/settings/avatar_gravatar',
'lib/constants',
'lib/session',
'lib/profile'
'lib/profile',
'lib/image-loader',
'views/decorators/progress_indicator'
],
function ($, _, md5, FormView, Template, Constants, Session, Profile) {
function ($, _, md5, FormView, Template, Constants, Session, Profile, ImageLoader, showProgressIndicator) {

function t (s) { return s; }

Expand Down Expand Up @@ -44,20 +46,21 @@ function ($, _, md5, FormView, Template, Constants, Session, Profile) {

afterRender: function () {
if (! this.gravatar) {
var img = new Image();
img.onerror = _.bind(this.notFound, this);

img.onload = _.bind(this.found, this);
img.src = this.gravatarUrl();
this._showGravatar();
}
},

found: function () {
_showGravatar: showProgressIndicator(function () {
return ImageLoader.load(this.gravatarUrl())
.then(_.bind(this._found, this), _.bind(this._notFound, this));
}, '.avatar-wrapper', '_gravatarProgressIndicator'),

_found: function () {
this.gravatar = this.gravatarUrl();
this.render();
},

notFound: function () {
_notFound: function () {
this.navigate('settings/avatar', {
error: t('No Gravatar found')
});
Expand Down
6 changes: 3 additions & 3 deletions app/scripts/views/sign_in.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ define([
'lib/auth-errors',
'lib/validate',
'views/mixins/service-mixin',
'views/decorators/button_progress_indicator'
'views/decorators/progress_indicator'
],
function (_, p, BaseView, FormView, SignInTemplate, Session, PasswordMixin, AuthErrors, Validate, ServiceMixin, showButtonProgressIndicator) {
function (_, p, BaseView, FormView, SignInTemplate, Session, PasswordMixin, AuthErrors, Validate, ServiceMixin, showProgressIndicator) {
var t = BaseView.t;

var View = FormView.extend({
Expand Down Expand Up @@ -171,7 +171,7 @@ function (_, p, BaseView, FormView, SignInTemplate, Session, PasswordMixin, Auth
* Used for the special "Sign In" button
* which is present when there is already a logged in user in the session
*/
useLoggedInAccount: showButtonProgressIndicator(function() {
useLoggedInAccount: showProgressIndicator(function() {
var self = this;

return this._signIn(Session.cachedCredentials.email, {
Expand Down
1 change: 1 addition & 0 deletions app/styles/_general.scss
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,7 @@ input[type="text"] {
width: 36px;
}

button .spinner,
.spinner-white {
animation: 0.9s spin infinite linear;
background-image: image-url('spinnerwhite.png');
Expand Down

0 comments on commit 690249b

Please sign in to comment.