Merge pull request #1347 from ZogStriP/change-your-avatar-in-a-modal

change your avatar in a modal
This commit is contained in:
Jeff Atwood 2013-08-16 21:01:30 -07:00
commit b6e66372d4
14 changed files with 197 additions and 200 deletions

View File

@ -0,0 +1,46 @@
/**
The modal for selecting an avatar
@class AvatarSelectorController
@extends Discourse.Controller
@namespace Discourse
@uses Discourse.ModalFunctionality
@module Discourse
**/
Discourse.AvatarSelectorController = Discourse.Controller.extend(Discourse.ModalFunctionality, {
init: function() {
// copy some data to support the cancel action
this.setProperties(this.get("currentUser").getProperties(
"username",
"has_uploaded_avatar",
"use_uploaded_avatar",
"gravatar_template",
"uploaded_avatar_template"
));
},
toggleUseUploadedAvatar: function(toggle) {
this.set("use_uploaded_avatar", toggle);
},
saveAvatarSelection: function() {
// sends the information to the server if it has changed
if (this.get("use_uploaded_avatar") !== this.get("currentUser.use_uploaded_avatar")) {
var data = { use_uploaded_avatar: this.get("use_uploaded_avatar") };
Discourse.ajax("/users/" + this.get("currentUser.username") + "/preferences/avatar/toggle", { type: 'PUT', data: data });
}
// saves the data back to the currentUser object
var currentUser = this.get("currentUser");
currentUser.setProperties(this.getProperties(
"has_uploaded_avatar",
"use_uploaded_avatar",
"gravatar_template",
"uploaded_avatar_template"
));
if (this.get("use_uploaded_avatar")) {
currentUser.set("avatar_template", this.get("uploaded_avatar_template"));
} else {
currentUser.set("avatar_template", this.get("gravatar_template"));
}
}
});

View File

@ -1,84 +0,0 @@
/**
This controller supports actions related to updating one's avatar
@class PreferencesAvatarController
@extends Discourse.ObjectController
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesAvatarController = Discourse.ObjectController.extend({
uploading: false,
uploadProgress: 0,
uploadDisabled: Em.computed.or("uploading"),
useGravatar: Em.computed.not("use_uploaded_avatar"),
useUploadedAvatar: Em.computed.alias("use_uploaded_avatar"),
toggleUseUploadedAvatar: function(toggle) {
if (this.get("use_uploaded_avatar") !== toggle) {
var controller = this;
this.set("use_uploaded_avatar", toggle);
Discourse.ajax("/users/" + this.get("username") + "/preferences/avatar/toggle", { type: 'PUT', data: { use_uploaded_avatar: toggle }})
.then(function(result) { controller.set("avatar_template", result.avatar_template); });
}
},
uploadButtonText: function() {
return this.get("uploading") ? I18n.t("user.change_avatar.uploading") : I18n.t("user.change_avatar.upload");
}.property("uploading"),
uploadAvatar: function() {
var controller = this;
var $upload = $("#avatar-input");
// do nothing if no file is selected
if (Em.isEmpty($upload.val())) { return; }
this.set("uploading", true);
// define the upload endpoint
$upload.fileupload({
url: Discourse.getURL("/users/" + this.get("username") + "/preferences/avatar"),
dataType: "json",
timeout: 20000
});
// when there is a progression for the upload
$upload.on("fileuploadprogressall", function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
controller.set("uploadProgress", progress);
});
// when the upload is successful
$upload.on("fileuploaddone", function (e, data) {
// set some properties
controller.setProperties({
has_uploaded_avatar: true,
use_uploaded_avatar: true,
avatar_template: data.result.url,
uploaded_avatar_template: data.result.url
});
});
// when there has been an error with the upload
$upload.on("fileuploadfail", function (e, data) {
Discourse.Utilities.displayErrorForUpload(data);
});
// when the upload is done
$upload.on("fileuploadalways", function (e, data) {
// prevent automatic upload when selecting a file
$upload.fileupload("destroy");
$upload.off();
// clear file input
$upload.val("");
// indicate upload is done
controller.setProperties({
uploading: false,
uploadProgress: 0
});
});
// *actually* launch the upload
$("#avatar-input").fileupload("add", { fileInput: $("#avatar-input") });
}
});

View File

@ -171,24 +171,9 @@ Handlebars.registerHelper('avatar', function(user, options) {
Ember.Handlebars.registerBoundHelper('boundAvatar', function(user, options) { Ember.Handlebars.registerBoundHelper('boundAvatar', function(user, options) {
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({ return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
size: options.hash.imageSize, size: options.hash.imageSize,
avatarTemplate: Em.get(user, 'avatar_template') avatarTemplate: Em.get(user, options.hash.template || 'avatar_template')
})); }));
}, 'avatar_template'); }, 'avatar_template', 'uploaded_avatar_template', 'gravatar_template');
/**
Bound avatar helper.
Will rerender whenever the "uploaded_avatar_template" changes.
Only available for the current user.
@method boundUploadedAvatar
@for Handlebars
**/
Ember.Handlebars.registerBoundHelper('boundUploadedAvatar', function(user, options) {
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
size: options.hash.imageSize,
avatarTemplate: Em.get(user, 'uploaded_avatar_template')
}));
}, 'uploaded_avatar_template');
/** /**
Nicely format a date without a binding since the date doesn't need to change. Nicely format a date without a binding since the date doesn't need to change.

View File

@ -13,6 +13,13 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
renderTemplate: function() { renderTemplate: function() {
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' }); this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
},
events: {
showAvatarSelector: function() {
Discourse.Route.showModal(this, 'avatarSelector');
this.controllerFor("avatarSelector").init();
}
} }
}); });
@ -117,32 +124,3 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
controller.setProperties({ model: user, newUsername: user.get('username') }); controller.setProperties({ model: user, newUsername: user.get('username') });
} }
}); });
/**
The route for updating a user's avatar
@class PreferencesAvatarRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesAvatarRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
return this.render({ into: 'user', outlet: 'userOutlet' });
},
// A bit odd, but if we leave to /preferences we need to re-render that outlet
exit: function() {
this._super();
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
},
setupController: function(controller, user) {
controller.setProperties({ model: user });
}
});

View File

@ -5,6 +5,6 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class='btn btn-primary' {{action saveAutoClose}} data-dismiss="modal">{{i18n topic.auto_close_save}}</button> <button class='btn btn-primary' {{action saveAutoClose}} data-dismiss="modal">{{i18n topic.auto_close_save}}</button>
<button class='btn' data-dismiss="modal">{{i18n topic.auto_close_cancel}}</button> <a data-dismiss="modal">{{i18n topic.auto_close_cancel}}</a>
<button class='btn pull-right' {{action removeAutoClose}} data-dismiss="modal">{{i18n topic.auto_close_remove}}</button> <button class='btn pull-right' {{action removeAutoClose}} data-dismiss="modal">{{i18n topic.auto_close_remove}}</button>
</div> </div>

View File

@ -0,0 +1,29 @@
<div class="modal-body">
<div>
<input type="radio" id="avatar" name="avatar" value="gravatar" {{action toggleUseUploadedAvatar false}}>
<label class="radio" for="avatar">{{avatar controller imageSize="large" template="gravatar_template"}} {{{i18n user.change_avatar.gravatar}}} {{currentUser.email}}</label>
<a href="//gravatar.com/emails" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn"><i class="icon-pencil"></i></a>
<div>
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded_avatar" {{action toggleUseUploadedAvatar true}} {{bindAttr disabled="view.uploadedAvatarDisabled" }}>
<label class="radio" for="uploaded_avatar">
{{#if has_uploaded_avatar}}
{{boundAvatar controller imageSize="large" template="uploaded_avatar_template"}} {{i18n user.change_avatar.uploaded_avatar}}
{{else}}
{{i18n user.change_avatar.uploaded_avatar_empty}}
{{/if}}
</label>
<button id="fake-avatar-input" class="btn" {{bindAttr disabled="view.uploading"}} title="{{i18n user.change_avatar.upload_title}}">
<i class="icon-picture"></i>&nbsp;{{view.uploadButtonText}}
</button>
<input type="file" id="avatar-input" accept="image/*" style="display:none">
{{#if view.uploading}}
<span>{{i18n upload_selector.uploading}} {{view.uploadProgress}}%</span>
{{/if}}
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" {{action saveAvatarSelection}} data-dismiss="modal">{{i18n save}}</button>
<a data-dismiss="modal">{{i18n cancel}}</a>
</div>

View File

@ -1,39 +0,0 @@
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<h3>{{i18n user.change_avatar.title}}</h3>
</div>
</div>
<div class="control-group">
<label class="control-label">{{i18n user.avatar.title}}</label>
<div class="controls">
<label class="radio">
<input type="radio" name="avatar" value="gravatar" {{action toggleUseUploadedAvatar false}}> {{avatar this imageSize="large" template="gravatar_template"}} {{i18n user.change_avatar.gravatar}} <a href="//gravatar.com/emails/" target="_blank" class="btn pad-left" title="{{i18n user.change_avatar.gravatar_title}}">{{i18n user.change}}</a>
</label>
{{#if has_uploaded_avatar}}
<label class="radio">
<input type="radio" name="avatar" value="uploaded_avatar" {{action toggleUseUploadedAvatar true}}> {{boundUploadedAvatar this imageSize="large"}} {{i18n user.change_avatar.uploaded_avatar}}
</label>
{{/if}}
</div>
</div>
<div class="control-group">
<div class="instructions">{{i18n user.change_avatar.upload_instructions}}</div>
<div class="controls">
<div>
<input type="file" id="avatar-input" accept="image/*">
</div>
<button {{action uploadAvatar}} {{bindAttr disabled="uploadDisabled"}} class="btn btn-primary">
<span class="add-upload"><i class="icon-picture"></i><i class="icon-plus"></i></span>
{{uploadButtonText}}
</button>
{{#if uploading}}
<span>{{i18n upload_selector.uploading}} {{uploadProgress}}%</span>
{{/if}}
</div>
</div>
</form>

View File

@ -44,7 +44,8 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">{{i18n user.avatar.title}}</label> <label class="control-label">{{i18n user.avatar.title}}</label>
<div class="controls"> <div class="controls">
{{avatar model imageSize="large"}} {{boundAvatar model imageSize="large"}}
<button {{action showAvatarSelector}} class="btn pad-left">{{i18n user.change}}</button>
</div> </div>
<div class='instructions'> <div class='instructions'>
{{#if Discourse.SiteSettings.allow_uploaded_avatars}} {{#if Discourse.SiteSettings.allow_uploaded_avatars}}
@ -53,7 +54,6 @@
{{else}} {{else}}
{{{i18n user.avatar.instructions.gravatar}}} {{email}} {{{i18n user.avatar.instructions.gravatar}}} {{email}}
{{/if}} {{/if}}
{{#linkTo "preferences.avatar" class="btn pad-left"}}{{i18n user.change}}{{/linkTo}}
{{else}} {{else}}
{{{i18n user.avatar.instructions.gravatar}}} {{email}} {{{i18n user.avatar.instructions.gravatar}}} {{email}}
<a href="//gravatar.com/emails/" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn pad-left">{{i18n user.change}}</a> <a href="//gravatar.com/emails/" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn pad-left">{{i18n user.change}}</a>

View File

@ -0,0 +1,89 @@
/**
This view handles the avatar selection interface
@class AvatarSelectorView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.AvatarSelectorView = Discourse.ModalBodyView.extend({
templateName: 'modal/avatar_selector',
classNames: ['avatar-selector'],
title: I18n.t('user.change_avatar.title'),
uploading: false,
uploadProgress: 0,
uploadedAvatarDisabled: Em.computed.not("controller.has_uploaded_avatar"),
didInsertElement: function() {
var view = this;
var $upload = $("#avatar-input");
this._super();
// simulate a click on the hidden file input when clicking on our fake file input
$("#fake-avatar-input").on("click", function(e) {
// do *NOT* use the cached `$upload` variable, because fileupload is cloning & replacing the input
// cf. https://github.com/blueimp/jQuery-File-Upload/wiki/Frequently-Asked-Questions#why-is-the-file-input-field-cloned-and-replaced-after-each-selection
$("#avatar-input").click();
e.preventDefault();
});
// define the upload endpoint
$upload.fileupload({
url: Discourse.getURL("/users/" + this.get("controller.username") + "/preferences/avatar"),
dataType: "json",
timeout: 20000,
fileInput: $upload
});
// when a file has been selected
$upload.on("fileuploadadd", function (e, data) {
view.set("uploading", true);
});
// when there is a progression for the upload
$upload.on("fileuploadprogressall", function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
view.set("uploadProgress", progress);
});
// when the upload is successful
$upload.on("fileuploaddone", function (e, data) {
// set some properties
view.get("controller").setProperties({
has_uploaded_avatar: true,
use_uploaded_avatar: true,
uploaded_avatar_template: data.result.url
});
});
// when there has been an error with the upload
$upload.on("fileuploadfail", function (e, data) {
Discourse.Utilities.displayErrorForUpload(data);
});
// when the upload is done
$upload.on("fileuploadalways", function (e, data) {
view.setProperties({ uploading: false, uploadProgress: 0 });
});
},
willDestroyElement: function() {
$("#fake-avatar-input").off("click");
$("#avatar-input").fileupload("destroy");
},
// *HACK* used to select the proper radio button
selectedChanged: function() {
var view = this;
Em.run.next(function() {
var value = view.get('controller.use_uploaded_avatar') ? 'uploaded_avatar' : 'gravatar';
view.$('input:radio[name="avatar"]').val([value]);
});
}.observes('controller.use_uploaded_avatar'),
uploadButtonText: function() {
return this.get("uploading") ? I18n.t("uploading") : I18n.t("upload");
}.property("uploading")
});

View File

@ -1,21 +0,0 @@
/**
This view handles rendering of a user's avatar uploader
@class PreferencesAvatarView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesAvatarView = Discourse.View.extend({
templateName: "user/avatar",
classNames: ["user-preferences"],
selectedChanged: function() {
var view = this;
Em.run.next(function() {
var value = view.get("controller.use_uploaded_avatar") ? "uploaded_avatar" : "gravatar";
view.$('input:radio[name="avatar"]').val([value]);
});
}.observes('controller.use_uploaded_avatar')
});

View File

@ -323,3 +323,18 @@
width: 680px; width: 680px;
} }
} }
.avatar-selector {
label {
display: inline-block;
margin-right: 10px;
}
#avatar-input {
width: 0;
height: 0;
overflow: hidden;
}
.avatar {
margin: 5px 10px 5px 0;
}
}

View File

@ -376,7 +376,7 @@ class UsersController < ApplicationController
user.use_uploaded_avatar = params[:use_uploaded_avatar] user.use_uploaded_avatar = params[:use_uploaded_avatar]
user.save! user.save!
render json: { avatar_template: user.avatar_template } render nothing: true
end end
private private

View File

@ -218,12 +218,11 @@ en:
change_avatar: change_avatar:
title: "Change your avatar" title: "Change your avatar"
upload_instructions: "Or you could upload an image" gravatar: "<a href='//gravatar.com/emails' target='_blank'>Gravatar</a>, based on"
upload: "Upload a picture"
uploading: "Uploading the picture..."
gravatar: "Gravatar"
gravatar_title: "Change your avatar on Gravatar's website" gravatar_title: "Change your avatar on Gravatar's website"
uploaded_avatar: "Uploaded picture" uploaded_avatar: "Custom picture"
uploaded_avatar_empty: "Add a custom picture"
upload_title: "Upload your picture"
email: email:
title: "Email" title: "Email"