diff --git a/app/assets/javascripts/discourse/components/bbcode.js b/app/assets/javascripts/discourse/components/bbcode.js
index 50cd2fdbd65..27c8372fa42 100644
--- a/app/assets/javascripts/discourse/components/bbcode.js
+++ b/app/assets/javascripts/discourse/components/bbcode.js
@@ -252,12 +252,22 @@ Discourse.BBCode = {
// remove leading
s
var content = matches[2].trim();
+ var avatarImg;
+ if (opts.lookupAvatarByPostNumber) {
+ // client-side, we can retrieve the avatar from the post
+ var postNumber = parseInt(_.find(params, { 'key' : 'post' }).value, 10);
+ avatarImg = opts.lookupAvatarByPostNumber(postNumber);
+ } else if (opts.lookupAvatar) {
+ // server-side, we need to lookup the avatar from the username
+ avatarImg = opts.lookupAvatar(username);
+ }
+
// Arguments for formatting
args = {
- username: I18n.t('user.said',{username: username}),
+ username: I18n.t('user.said', {username: username}),
params: params,
quote: content,
- avatarImg: opts.lookupAvatar ? opts.lookupAvatar(username) : void 0
+ avatarImg: avatarImg
};
// Name of the template
diff --git a/app/assets/javascripts/discourse/components/utilities.js b/app/assets/javascripts/discourse/components/utilities.js
index 454e0c61834..bf2b0f0ec32 100644
--- a/app/assets/javascripts/discourse/components/utilities.js
+++ b/app/assets/javascripts/discourse/components/utilities.js
@@ -16,6 +16,7 @@ Discourse.Utilities = {
case 'small': return 25;
case 'medium': return 32;
case 'large': return 45;
+ case 'huge': return 120;
}
return size;
},
@@ -50,18 +51,20 @@ Discourse.Utilities = {
return result + "style=\"background-color: #" + color + "; color: #" + textColor + ";\">" + name + "";
},
- avatarUrl: function(username, size, template) {
- if (!username) return "";
- var rawSize = (Discourse.Utilities.translateSize(size) * (window.devicePixelRatio || 1)).toFixed();
+ avatarUrl: function(template, size) {
+ if (!template) { return ""; }
+ var rawSize = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize(size));
+ return template.replace(/\{size\}/g, rawSize);
+ },
- if (username.match(/[^A-Za-z0-9_]/)) { return ""; }
- if (template) return template.replace(/\{size\}/g, rawSize);
- return Discourse.getURL("/users/") + username.toLowerCase() + "/avatar/" + rawSize + "?__ws=" + encodeURIComponent(Discourse.BaseUrl || "");
+ getRawSize: function(size) {
+ var pixelRatio = window.devicePixelRatio || 1;
+ return pixelRatio >= 1.5 ? size * 2 : size;
},
avatarImg: function(options) {
var size = Discourse.Utilities.translateSize(options.size);
- var url = Discourse.Utilities.avatarUrl(options.username, options.size, options.avatarTemplate);
+ var url = Discourse.Utilities.avatarUrl(options.avatarTemplate, size);
// We won't render an invalid url
if (!url || url.length === 0) { return ""; }
@@ -71,8 +74,8 @@ Discourse.Utilities = {
return "";
},
- tinyAvatar: function(username) {
- return Discourse.Utilities.avatarImg({ username: username, size: 'tiny' });
+ tinyAvatar: function(avatarTemplate) {
+ return Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny' });
},
postUrl: function(slug, topicId, postNumber) {
@@ -266,6 +269,28 @@ Discourse.Utilities = {
authorizedExtensions: function() {
return Discourse.SiteSettings.authorized_extensions.replace(/\|/g, ", ");
+ },
+
+ displayErrorForUpload: function(data) {
+ // deal with meaningful errors first
+ if (data.jqXHR) {
+ switch (data.jqXHR.status) {
+ // cancel from the user
+ case 0: return;
+ // entity too large, usually returned from the web server
+ case 413:
+ var maxSizeKB = Discourse.SiteSettings.max_image_size_kb;
+ bootbox.alert(I18n.t('post.errors.image_too_large', { max_size_kb: maxSizeKB }));
+ return;
+ // the error message is provided by the server
+ case 415: // media type not authorized
+ case 422: // there has been an error on the server (mostly due to FastImage)
+ bootbox.alert(data.jqXHR.responseText);
+ return;
+ }
+ }
+ // otherwise, display a generic error message
+ bootbox.alert(I18n.t('post.errors.upload'));
}
};
diff --git a/app/assets/javascripts/discourse/controllers/preferences_avatar_controller.js b/app/assets/javascripts/discourse/controllers/preferences_avatar_controller.js
new file mode 100644
index 00000000000..41180495972
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/preferences_avatar_controller.js
@@ -0,0 +1,84 @@
+/**
+ 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") });
+ }
+});
diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js b/app/assets/javascripts/discourse/helpers/application_helpers.js
index e806482412a..b941f29144b 100644
--- a/app/assets/javascripts/discourse/helpers/application_helpers.js
+++ b/app/assets/javascripts/discourse/helpers/application_helpers.js
@@ -116,16 +116,22 @@ Handlebars.registerHelper('lower', function(property, options) {
@for Handlebars
**/
Handlebars.registerHelper('avatar', function(user, options) {
-
if (typeof user === 'string') {
user = Ember.Handlebars.get(this, user, options);
}
- if( user ) {
+ if (user) {
var username = Em.get(user, 'username');
if (!username) username = Em.get(user, options.hash.usernamePath);
- var avatarTemplate = Ember.get(user, 'avatar_template');
+ var avatarTemplate;
+ var template = options.hash.template;
+ if (template && template !== 'avatar_template') {
+ avatarTemplate = Em.get(user, template);
+ if (!avatarTemplate) avatarTemplate = Em.get(user, 'user.' + template);
+ }
+
+ if (!avatarTemplate) avatarTemplate = Em.get(user, 'avatar_template');
if (!avatarTemplate) avatarTemplate = Em.get(user, 'user.avatar_template');
var title;
@@ -147,7 +153,6 @@ Handlebars.registerHelper('avatar', function(user, options) {
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
size: options.hash.imageSize,
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
- username: username,
title: title || username,
avatarTemplate: avatarTemplate
}));
@@ -158,18 +163,32 @@ Handlebars.registerHelper('avatar', function(user, options) {
/**
Bound avatar helper.
+ Will rerender whenever the "avatar_template" changes.
@method boundAvatar
@for Handlebars
**/
Ember.Handlebars.registerBoundHelper('boundAvatar', function(user, options) {
- var username = Em.get(user, 'username');
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
size: options.hash.imageSize,
- username: username,
- avatarTemplate: Ember.get(user, 'avatar_template')
+ avatarTemplate: Em.get(user, 'avatar_template')
}));
-});
+}, 'avatar_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.
diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js
index 72cbff882f3..dbe43748da7 100644
--- a/app/assets/javascripts/discourse/models/composer.js
+++ b/app/assets/javascripts/discourse/models/composer.js
@@ -70,14 +70,14 @@ Discourse.Composer = Discourse.Model.extend({
if (post) {
postDescription = I18n.t('post.' + this.get('action'), {
link: postLink,
- replyAvatar: Discourse.Utilities.tinyAvatar(post.get('username')),
+ replyAvatar: Discourse.Utilities.tinyAvatar(post.get('avatar_template')),
username: this.get('post.username')
});
var replyUsername = post.get('reply_to_user.username');
- if (replyUsername && this.get('action') === EDIT) {
- postDescription += " " + I18n.t("post.in_reply_to") + " " +
- Discourse.Utilities.tinyAvatar(replyUsername) + " " + replyUsername;
+ var replyAvatarTemplate = post.get('reply_to_user.avatar_template');
+ if (replyUsername && replyAvatarTemplate && this.get('action') === EDIT) {
+ postDescription += " " + I18n.t("post.in_reply_to") + " " + Discourse.Utilities.tinyAvatar(replyAvatarTemplate) + " " + replyUsername;
}
}
diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js
index c60b937844c..5eadf489a7a 100644
--- a/app/assets/javascripts/discourse/routes/application_routes.js
+++ b/app/assets/javascripts/discourse/routes/application_routes.js
@@ -57,6 +57,7 @@ Discourse.Route.buildRoutes(function() {
this.route('username', { path: '/username' });
this.route('email', { path: '/email' });
this.route('about', { path: '/about-me' });
+ this.route('avatar', { path: '/avatar' });
});
this.route('invited', { path: 'invited' });
diff --git a/app/assets/javascripts/discourse/routes/preferences_routes.js b/app/assets/javascripts/discourse/routes/preferences_routes.js
index 6e6e14160ee..55427f4b9ff 100644
--- a/app/assets/javascripts/discourse/routes/preferences_routes.js
+++ b/app/assets/javascripts/discourse/routes/preferences_routes.js
@@ -116,4 +116,33 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
setupController: function(controller, user) {
controller.setProperties({ model: user, newUsername: user.get('username') });
}
-});
\ No newline at end of file
+});
+
+
+/**
+ 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 });
+ }
+});
diff --git a/app/assets/javascripts/discourse/templates/header.js.handlebars b/app/assets/javascripts/discourse/templates/header.js.handlebars
index d1784efaa39..7349a442a93 100644
--- a/app/assets/javascripts/discourse/templates/header.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/header.js.handlebars
@@ -30,7 +30,7 @@
{{#unless showExtraInfo}}