FIX: profile picture selector

This commit is contained in:
Régis Hanol 2015-09-11 12:56:34 +02:00
parent 6437cd0341
commit 0c58f08207
11 changed files with 116 additions and 117 deletions

View File

@ -1,3 +1,4 @@
import computed from "ember-addons/ember-computed-decorators";
import UploadMixin from "discourse/mixins/upload"; import UploadMixin from "discourse/mixins/upload";
export default Em.Component.extend(UploadMixin, { export default Em.Component.extend(UploadMixin, {
@ -5,21 +6,23 @@ export default Em.Component.extend(UploadMixin, {
tagName: "span", tagName: "span",
imageIsNotASquare: false, imageIsNotASquare: false,
uploadButtonText: function() { @computed("uploading")
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture"); uploadButtonText(uploading) {
}.property("uploading"), return uploading ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
},
uploadDone(upload) { uploadDone(upload) {
this.setProperties({ this.setProperties({
imageIsNotASquare: upload.width !== upload.height, imageIsNotASquare: upload.width !== upload.height,
uploadedAvatarTemplate: upload.url, uploadedAvatarTemplate: upload.url,
custom_avatar_upload_id: upload.id, uploadedAvatarId: upload.id,
}); });
this.sendAction("done"); this.sendAction("done");
}, },
data: function() { @computed("user_id")
return { user_id: this.get("user_id") }; data(user_id) {
}.property("user_id") return { user_id };
}
}); });

View File

@ -1,21 +1,29 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality'; import computed from "ember-addons/ember-computed-decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Ember.Controller.extend(ModalFunctionality, { export default Ember.Controller.extend(ModalFunctionality, {
uploadedAvatarTemplate: null, @computed("selected", "system_avatar_upload_id", "gravatar_avatar_upload_id", "custom_avatar_upload_id")
saveDisabled: Em.computed.alias("uploading"), selectedUploadId(selected, system, gravatar, custom) {
hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'custom_avatar_upload_id'), switch (selected) {
case "system": return system;
selectedUploadId: function() { case "gravatar": return gravatar;
switch (this.get("selected")) { default: return custom;
case "system": return this.get("system_avatar_upload_id");
case "gravatar": return this.get("gravatar_avatar_upload_id");
default: return this.get("custom_avatar_upload_id");
} }
}.property('selected', 'system_avatar_upload_id', 'gravatar_avatar_upload_id', 'custom_avatar_upload_id'), },
allowImageUpload: function() { @computed("selected", "system_avatar_template", "gravatar_avatar_template", "custom_avatar_template")
selectedAvatarTemplate(selected, system, gravatar, custom) {
switch (selected) {
case "system": return system;
case "gravatar": return gravatar;
default: return custom;
}
},
@computed()
allowImageUpload() {
return Discourse.Utilities.allowsImages(); return Discourse.Utilities.allowsImages();
}.property(), },
actions: { actions: {
useUploadedAvatar() { this.set("selected", "uploaded"); }, useUploadedAvatar() { this.set("selected", "uploaded"); },
@ -25,8 +33,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
refreshGravatar() { refreshGravatar() {
this.set("gravatarRefreshDisabled", true); this.set("gravatarRefreshDisabled", true);
return Discourse return Discourse
.ajax("/user_avatar/" + this.get("username") + "/refresh_gravatar.json", { method: 'POST' }) .ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" })
.then(result => this.set("gravatar_avatar_upload_id", result.upload_id)) .then(result => this.setProperties({
gravatar_avatar_template: result.gravatar_avatar_template,
gravatar_upload_id: result.gravatar_upload_id,
}))
.finally(() => this.set("gravatarRefreshDisabled", false)); .finally(() => this.set("gravatarRefreshDisabled", false));
} }
} }

View File

@ -3,32 +3,27 @@ import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatt
const safe = Handlebars.SafeString; const safe = Handlebars.SafeString;
Em.Handlebars.helper('bound-avatar', function(user, size) { Em.Handlebars.helper('bound-avatar', (user, size) => {
if (Em.isEmpty(user)) { if (Em.isEmpty(user)) {
return new safe("<div class='avatar-placeholder'></div>"); return new safe("<div class='avatar-placeholder'></div>");
} }
const avatar = Em.get(user, 'avatar_template'); const avatar = Em.get(user, 'avatar_template');
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar })); return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar }));
}, 'username', 'avatar_template'); }, 'username', 'avatar_template');
/* /*
* Used when we only have a template * Used when we only have a template
*/ */
Em.Handlebars.helper('bound-avatar-template', function(at, size) { Em.Handlebars.helper('bound-avatar-template', (at, size) => {
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: at })); return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: at }));
}); });
registerUnbound('raw-date', function(dt) { registerUnbound('raw-date', dt => longDate(new Date(dt)));
return longDate(new Date(dt));
});
registerUnbound('age-with-tooltip', function(dt) { registerUnbound('age-with-tooltip', dt => new safe(autoUpdatingRelativeAge(new Date(dt), {title: true})));
return new safe(autoUpdatingRelativeAge(new Date(dt), {title: true}));
});
registerUnbound('number', function(orig, params) { registerUnbound('number', (orig, params) => {
orig = parseInt(orig, 10); orig = parseInt(orig, 10);
if (isNaN(orig)) { orig = 0; } if (isNaN(orig)) { orig = 0; }

View File

@ -256,47 +256,33 @@ const User = RestModel.extend({
}); });
}, },
/* pickAvatar(upload_id, avatar_template) {
Change avatar selection
*/
pickAvatar(uploadId) {
return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, { return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, {
type: 'PUT', type: 'PUT',
data: { upload_id: uploadId } data: { upload_id }
}).then(() => this.set('uploaded_avatar_id', uploadId)); }).then(() => this.setProperties({
avatar_template,
uploaded_avatar_id: upload_id
}));
}, },
/**
Determines whether the current user is allowed to upload a file.
@method isAllowedToUploadAFile
@param {String} type The type of the upload (image, attachment)
@returns true if the current user is allowed to upload a file
**/
isAllowedToUploadAFile(type) { isAllowedToUploadAFile(type) {
return this.get('staff') || return this.get('staff') ||
this.get('trust_level') > 0 || this.get('trust_level') > 0 ||
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0; Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
}, },
/** createInvite(email, group_names) {
Invite a user to the site
@method createInvite
@param {String} email The email address of the user to invite to the site
@returns {Promise} the result of the server call
**/
createInvite(email, groupNames) {
return Discourse.ajax('/invites', { return Discourse.ajax('/invites', {
type: 'POST', type: 'POST',
data: {email: email, group_names: groupNames} data: { email, group_names }
}); });
}, },
generateInviteLink(email, groupNames, topicId) { generateInviteLink(email, group_names, topic_id) {
return Discourse.ajax('/invites/link', { return Discourse.ajax('/invites/link', {
type: 'POST', type: 'POST',
data: {email: email, group_names: groupNames, topic_id: topicId} data: { email, group_names, topic_id }
}); });
}, },

View File

@ -18,50 +18,51 @@ export default RestrictedUserRoute.extend({
showModal('avatar-selector'); showModal('avatar-selector');
// all the properties needed for displaying the avatar selector modal // all the properties needed for displaying the avatar selector modal
const controller = this.controllerFor('avatar-selector'), const props = this.modelFor('user').getProperties(
props = this.modelFor('user').getProperties(
'id', 'id',
'email', 'email',
'username', 'username',
'uploaded_avatar_id', 'avatar_template',
'system_avatar_template',
'gravatar_avatar_template',
'custom_avatar_template',
'system_avatar_upload_id', 'system_avatar_upload_id',
'gravatar_avatar_upload_id', 'gravatar_avatar_upload_id',
'custom_avatar_upload_id' 'custom_avatar_upload_id'
); );
switch (props.uploaded_avatar_id) { switch (props.avatar_template) {
case props.system_avatar_upload_id: case props.system_avatar_template:
props.selected = "system"; props.selected = "system";
break; break;
case props.gravatar_avatar_upload_id: case props.gravatar_avatar_template:
props.selected = "gravatar"; props.selected = "gravatar";
break; break;
default: default:
props.selected = "uploaded"; props.selected = "uploaded";
} }
controller.setProperties(props); this.controllerFor('avatar-selector').setProperties(props);
}, },
saveAvatarSelection() { saveAvatarSelection() {
const user = this.modelFor('user'), const user = this.modelFor('user'),
avatarSelector = this.controllerFor('avatar-selector'); controller = this.controllerFor('avatar-selector'),
selectedUploadId = controller.get("selectedUploadId"),
selectedAvatarTemplate = controller.get("selectedAvatarTemplate");
// sends the information to the server if it has changed user.pickAvatar(selectedUploadId, selectedAvatarTemplate)
if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) { .then(() => {
user.pickAvatar(avatarSelector.get('selectedUploadId')) user.setProperties(controller.getProperties(
.then(() => { 'system_avatar_template',
user.setProperties(avatarSelector.getProperties( 'gravatar_avatar_template',
'system_avatar_upload_id', 'custom_avatar_template'
'gravatar_avatar_upload_id', ));
'custom_avatar_upload_id' bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
)); });
bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
});
}
// saves the data back // saves the data back
avatarSelector.send('closeModal'); controller.send('closeModal');
}, },
} }

View File

@ -2,32 +2,27 @@
<div> <div>
<div> <div>
<input type="radio" id="system-avatar" name="avatar" value="system" {{action "useSystem"}}> <input type="radio" id="system-avatar" name="avatar" value="system" {{action "useSystem"}}>
<label class="radio" for="system-avatar">{{bound-avatar controller "large" system_avatar_upload_id}} {{{i18n 'user.change_avatar.letter_based'}}}</label> <label class="radio" for="system-avatar">{{bound-avatar-template system_avatar_template "large"}} {{{i18n 'user.change_avatar.letter_based'}}}</label>
</div> </div>
<div> <div>
<input type="radio" id="gravatar" name="avatar" value="gravatar" {{action "useGravatar"}}> <input type="radio" id="gravatar" name="avatar" value="gravatar" {{action "useGravatar"}}>
<label class="radio" for="gravatar">{{bound-avatar controller "large" gravatar_avatar_upload_id}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label> <label class="radio" for="gravatar">{{bound-avatar-template gravatar_avatar_template "large"}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
{{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}} {{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}}
</div> </div>
{{#if allowImageUpload}} {{#if allowImageUpload}}
<div> <div>
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action "useUploadedAvatar"}}> <input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action "useUploadedAvatar"}}>
<label class="radio" for="uploaded_avatar"> <label class="radio" for="uploaded_avatar">
{{#if hasUploadedAvatar}} {{#if custom_avatar_template}}
{{#if uploadedAvatarTemplate}} {{bound-avatar-template custom_avatar_template "large"}}
{{bound-avatar-template uploadedAvatarTemplate "large"}}
{{else}}
{{bound-avatar controller "large" custom_avatar_upload_id}}
{{/if}}
{{i18n 'user.change_avatar.uploaded_avatar'}} {{i18n 'user.change_avatar.uploaded_avatar'}}
{{else}} {{else}}
{{i18n 'user.change_avatar.uploaded_avatar_empty'}} {{i18n 'user.change_avatar.uploaded_avatar_empty'}}
{{/if}} {{/if}}
</label> </label>
{{avatar-uploader username=username {{avatar-uploader user_id=id
user_id=id uploadedAvatarTemplate=custom_avatar_template
uploadedAvatarTemplate=uploadedAvatarTemplate uploadedAvatarId=custom_avatar_upload_id
custom_avatar_upload_id=custom_avatar_upload_id
uploading=uploading uploading=uploading
done="useUploadedAvatar"}} done="useUploadedAvatar"}}
</div> </div>
@ -36,6 +31,6 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
{{d-button action="saveAvatarSelection" class="btn-primary" disabled=saveDisabled label="save"}} {{d-button action="saveAvatarSelection" class="btn-primary" disabled=uploading label="save"}}
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a> <a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
</div> </div>

View File

@ -1,3 +1,4 @@
import { on, observes } from "ember-addons/ember-computed-decorators";
import ModalBodyView from "discourse/views/modal-body"; import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({ export default ModalBodyView.extend({
@ -6,11 +7,14 @@ export default ModalBodyView.extend({
title: I18n.t('user.change_avatar.title'), title: I18n.t('user.change_avatar.title'),
// *HACK* used to select the proper radio button, because {{action}} stops the default behavior // *HACK* used to select the proper radio button, because {{action}} stops the default behavior
selectedChanged: function() { @on("didInsertElement")
@observes("controller.selected")
selectedChanged() {
Em.run.next(() => $('input:radio[name="avatar"]').val([this.get('controller.selected')])); Em.run.next(() => $('input:radio[name="avatar"]').val([this.get('controller.selected')]));
}.observes('controller.selected').on("didInsertElement"), },
_focusSelectedButton: function() { @on("didInsertElement")
_focusSelectedButton() {
Em.run.next(() => $('input:radio[value="' + this.get('controller.selected') + '"]').focus()); Em.run.next(() => $('input:radio[value="' + this.get('controller.selected') + '"]').focus());
}.on("didInsertElement") }
}); });

View File

@ -13,7 +13,10 @@ class UserAvatarsController < ApplicationController
user.create_user_avatar(user_id: user.id) unless user.user_avatar user.create_user_avatar(user_id: user.id) unless user.user_avatar
user.user_avatar.update_gravatar! user.user_avatar.update_gravatar!
render json: { upload_id: user.user_avatar.gravatar_upload_id } render json: {
gravatar_upload_id: user.user_avatar.gravatar_upload_id,
gravatar_avatar_template: User.avatar_template(user.username, user.user_avatar.gravatar_upload_id)
}
else else
raise Discourse::NotFound raise Discourse::NotFound
end end

View File

@ -471,9 +471,8 @@ class User < ActiveRecord::Base
def self.system_avatar_template(username) def self.system_avatar_template(username)
# TODO it may be worth caching this in a distributed cache, should be benched # TODO it may be worth caching this in a distributed cache, should be benched
if SiteSetting.external_system_avatars_enabled if SiteSetting.external_system_avatars_enabled
color = letter_avatar_color(username)
url = SiteSetting.external_system_avatars_url.dup url = SiteSetting.external_system_avatars_url.dup
url.gsub! "{color}", color url.gsub! "{color}", letter_avatar_color(username)
url.gsub! "{username}", username url.gsub! "{username}", username
url.gsub! "{first_letter}", username[0].downcase url.gsub! "{first_letter}", username[0].downcase
url url
@ -482,10 +481,6 @@ class User < ActiveRecord::Base
end end
end end
def letter_avatar_color
self.class.letter_avatar_color(username)
end
def self.letter_avatar_color(username) def self.letter_avatar_color(username)
username = username || "" username = username || ""
color = LetterAvatar::COLORS[Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length] color = LetterAvatar::COLORS[Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length]

View File

@ -93,8 +93,12 @@ class UserSerializer < BasicUserSerializer
:watched_category_ids, :watched_category_ids,
:private_messages_stats, :private_messages_stats,
:disable_jump_reply, :disable_jump_reply,
:system_avatar_upload_id,
:system_avatar_template,
:gravatar_avatar_upload_id, :gravatar_avatar_upload_id,
:gravatar_avatar_template,
:custom_avatar_upload_id, :custom_avatar_upload_id,
:custom_avatar_template,
:has_title_badges, :has_title_badges,
:card_image_badge, :card_image_badge,
:card_image_badge_id, :card_image_badge_id,
@ -278,14 +282,32 @@ class UserSerializer < BasicUserSerializer
UserAction.private_messages_stats(object.id, scope) UserAction.private_messages_stats(object.id, scope)
end end
def system_avatar_upload_id
# should be left blank
end
def system_avatar_template
User.system_avatar_template(object.username)
end
def gravatar_avatar_upload_id def gravatar_avatar_upload_id
object.user_avatar.try(:gravatar_upload_id) object.user_avatar.try(:gravatar_upload_id)
end end
def gravatar_avatar_template
return unless gravatar_upload_id = object.user_avatar.try(:gravatar_upload_id)
User.avatar_template(object.username, gravatar_upload_id)
end
def custom_avatar_upload_id def custom_avatar_upload_id
object.user_avatar.try(:custom_upload_id) object.user_avatar.try(:custom_upload_id)
end end
def custom_avatar_template
return unless custom_upload_id = object.user_avatar.try(:custom_upload_id)
User.avatar_template(object.username, custom_upload_id)
end
def has_title_badges def has_title_badges
object.badges.where(allow_title: true).count > 0 object.badges.where(allow_title: true).count > 0
end end

View File

@ -1,16 +0,0 @@
import avatarTemplate from 'discourse/lib/avatar-template';
module('lib:avatar-template');
test("avatarTemplate", function(){
var oldCDN = Discourse.CDN;
var oldBase = Discourse.BaseUrl;
Discourse.BaseUrl = "frogs.com";
equal(avatarTemplate("sam", 1), "/user_avatar/frogs.com/sam/{size}/1.png");
Discourse.CDN = "http://awesome.cdn.com";
equal(avatarTemplate("sam", 1), "http://awesome.cdn.com/user_avatar/frogs.com/sam/{size}/1.png");
Discourse.CDN = oldCDN;
Discourse.BaseUrl = oldBase;
});