FEATURE: add support for generic external avatar services

This changes it so we only ship an avatar template down to the client
it has no magic, all it knows is how to plug in size
This commit is contained in:
Sam 2015-09-11 18:14:34 +10:00 committed by Régis Hanol
parent 22688a31ee
commit 6437cd0341
19 changed files with 35 additions and 118 deletions

View file

@ -13,7 +13,7 @@ export default Ember.Component.extend(StringBuffer, {
iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">";
iconsHtml += Discourse.Utilities.avatarImg({
size: 'small',
avatarTemplate: u.get('avatarTemplate'),
avatarTemplate: u.get('avatar_template'),
title: u.get('username')
});
iconsHtml += "</a>";

View file

@ -1,22 +1,17 @@
import registerUnbound from 'discourse/helpers/register-unbound';
import avatarTemplate from 'discourse/lib/avatar-template';
import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatter';
const safe = Handlebars.SafeString;
Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
Em.Handlebars.helper('bound-avatar', function(user, size) {
if (Em.isEmpty(user)) {
return new safe("<div class='avatar-placeholder'></div>");
}
const username = Em.get(user, 'username'),
letterAvatarColor = Em.get(user, 'letter_avatar_color');
if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); }
const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId, letterAvatarColor);
const avatar = Em.get(user, 'avatar_template');
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar }));
}, 'username', 'uploaded_avatar_id', 'letter_avatar_color', 'avatar_template');
}, 'username', 'avatar_template');
/*
* Used when we only have a template

View file

@ -1,15 +1,14 @@
import registerUnbound from 'discourse/helpers/register-unbound';
import avatarTemplate from 'discourse/lib/avatar-template';
function renderAvatar(user, options) {
options = options || {};
if (user) {
let username = Em.get(user, 'username');
if (!username) {
if (!options.usernamePath) { return ''; }
username = Em.get(user, options.usernamePath);
}
const username = Em.get(user, options.usernamePath || 'username');
const avatarTemplate = Em.get(user, options.avatarTemplatePath || 'avatar_template');
if (!username || !avatarTemplate) { return ''; }
let title;
if (!options.ignoreTitle) {
@ -27,15 +26,11 @@ function renderAvatar(user, options) {
}
}
// this is simply done to ensure we cache images correctly
const uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id'),
letterAvatarColor = Em.get(user, 'letter_avatar_color') || Em.get(user, 'user.letter_avatar_color');
return Discourse.Utilities.avatarImg({
size: options.imageSize,
extraClasses: Em.get(user, 'extras') || options.extraClasses,
title: title || username,
avatarTemplate: Em.get("avatar_template") || avatarTemplate(username, uploadedAvatarId, letterAvatarColor)
avatarTemplate: avatarTemplate
});
} else {
return '';

View file

@ -1,31 +0,0 @@
import { hashString } from 'discourse/lib/hash';
let _splitAvatars;
function defaultAvatar(username, letterAvatarColor) {
const defaultAvatars = Discourse.SiteSettings.default_avatars,
version = Discourse.LetterAvatarVersion;
if (defaultAvatars && defaultAvatars.length) {
_splitAvatars = _splitAvatars || defaultAvatars.split("\n");
if (_splitAvatars.length) {
const hash = hashString(username);
return _splitAvatars[Math.abs(hash) % _splitAvatars.length];
}
}
if (Discourse.SiteSettings.external_letter_avatars_enabled) {
const url = Discourse.SiteSettings.external_letter_avatars_url;
return `${url}/letter/${username[0]}/${letterAvatarColor}/{size}.png`;
} else {
return Discourse.getURLWithCDN(`/letter_avatar/${username.toLowerCase()}/{size}/${version}.png`);
}
}
export default function(username, uploadedAvatarId, letterAvatarColor) {
if (uploadedAvatarId) {
return Discourse.getURLWithCDN(`/user_avatar/${Discourse.BaseUrl}/${username.toLowerCase()}/{size}/${uploadedAvatarId}.png`);
}
return defaultAvatar(username, letterAvatarColor);
}

View file

@ -567,7 +567,7 @@ const Composer = RestModel.extend({
username: user.get('username'),
user_id: user.get('id'),
user_title: user.get('title'),
uploaded_avatar_id: user.get('uploaded_avatar_id'),
avatar_template: user.get('avatar_template'),
user_custom_fields: user.get('custom_fields'),
post_type: this.site.get('post_types.regular'),
actions_summary: [],
@ -587,7 +587,7 @@ const Composer = RestModel.extend({
reply_to_post_number: post.get('post_number'),
reply_to_user: {
username: post.get('username'),
uploaded_avatar_id: post.get('uploaded_avatar_id')
avatar_template: post.get('avatar_template')
}
});
}

View file

@ -154,8 +154,6 @@ const UserAction = RestModel.extend({
switchToActing() {
this.setProperties({
username: this.get('acting_username'),
uploaded_avatar_id: this.get('acting_uploaded_avatar_id'),
letter_avatar_color: this.get('action_letter_avatar_color'),
name: this.get('actingDisplayName')
});
}

View file

@ -1,6 +1,5 @@
import { url } from 'discourse/lib/computed';
import RestModel from 'discourse/models/rest';
import avatarTemplate from 'discourse/lib/avatar-template';
import UserStream from 'discourse/models/user-stream';
import UserPostsStream from 'discourse/models/user-posts-stream';
import Singleton from 'discourse/mixins/singleton';
@ -257,11 +256,6 @@ const User = RestModel.extend({
});
},
@computed("username", "uploaded_avatar_id", "letter_avatar_color")
avatarTemplate(username, uploadedAvatarId, letterAvatarColor) {
return avatarTemplate(username, uploadedAvatarId, letterAvatarColor);
},
/*
Change avatar selection
*/

View file

@ -23,7 +23,7 @@
{{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
</button>
{{else}}
<a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
<a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true" avatarTemplatePath="acting_avatar_template"}}</div></a>
{{#if grandChild.edit_reason}} &mdash; <span class="edit-reason">{{grandChild.edit_reason}}</span>{{/if}}
{{/if}}
{{/each}}

View file

@ -1,5 +1,5 @@
<td class='posters'>
{{#each poster in posters}}
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster usernamePath="user.username" imageSize="small"}}</a>
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" imageSize="small"}}</a>
{{/each}}
</td>

View file

@ -1,7 +1,6 @@
import userSearch from 'discourse/lib/user-search';
import afterTransition from 'discourse/lib/after-transition';
import loadScript from 'discourse/lib/load-script';
import avatarTemplate from 'discourse/lib/avatar-template';
import positioningWorkaround from 'discourse/lib/safari-hacks';
import debounce from 'discourse/lib/debounce';
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
@ -251,11 +250,7 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
if (posts && topicId === self.get('controller.controllers.topic.model.id')) {
const quotedPost = posts.findProperty("post_number", postNumber);
if (quotedPost) {
const username = quotedPost.get('username'),
uploadId = quotedPost.get('uploaded_avatar_id'),
letterAvatarColor = quotedPost.get("letter_avatar_color");
return Discourse.Utilities.tinyAvatar(avatarTemplate(username, uploadId, letterAvatarColor));
return Discourse.Utilities.tinyAvatar(quotedPost.get('avatar_template'));
}
}
}

View file

@ -14,7 +14,6 @@
//= require ./discourse/lib/load-script
//= require ./discourse/lib/notification-levels
//= require ./discourse/lib/app-events
//= require ./discourse/lib/avatar-template
//= require ./discourse/lib/url
//= require ./discourse/lib/debounce
//= require ./discourse/lib/quote
@ -41,7 +40,6 @@
//= require ./discourse/lib/autocomplete
//= require ./discourse/lib/after-transition
//= require ./discourse/lib/debounce
//= require ./discourse/lib/avatar-template
//= require ./discourse/lib/safari-hacks
//= require_tree ./discourse/adapters
//= require ./discourse/models/rest

View file

@ -518,7 +518,7 @@ class UsersController < ApplicationController
user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id]
user_fields << :name if SiteSetting.enable_names?
to_render = { users: results.as_json(only: user_fields, methods: [:avatar_template, :letter_avatar_color]) }
to_render = { users: results.as_json(only: user_fields, methods: [:avatar_template]) }
if params[:include_groups] == "true"
to_render[:groups] = Group.search_group(term, current_user).map {|m| {:name=>m.name, :usernames=> m.usernames.split(",")} }

View file

@ -457,7 +457,7 @@ class User < ActiveRecord::Base
split_avatars[hash.abs % split_avatars.size]
end
else
letter_avatar_template(username)
system_avatar_template(username)
end
end
@ -468,10 +468,15 @@ class User < ActiveRecord::Base
UserAvatar.local_avatar_template(hostname, username.downcase, uploaded_avatar_id)
end
def self.letter_avatar_template(username)
if SiteSetting.external_letter_avatars_enabled
def self.system_avatar_template(username)
# TODO it may be worth caching this in a distributed cache, should be benched
if SiteSetting.external_system_avatars_enabled
color = letter_avatar_color(username)
"#{SiteSetting.external_letter_avatars_url}/letter/#{username[0]}/#{color}/{size}.png"
url = SiteSetting.external_system_avatars_url.dup
url.gsub! "{color}", color
url.gsub! "{username}", username
url.gsub! "{first_letter}", username[0].downcase
url
else
"#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png"
end
@ -484,7 +489,7 @@ class User < ActiveRecord::Base
def self.letter_avatar_color(username)
username = username || ""
color = LetterAvatar::COLORS[Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length]
color.map { |c| c.to_s(16) }.join
color.map { |c| c.to_s(16).rjust(2, '0') }.join
end
def avatar_template

View file

@ -4,8 +4,6 @@ class BasicPostSerializer < ApplicationSerializer
:name,
:username,
:avatar_template,
:uploaded_avatar_id,
:letter_avatar_color,
:created_at,
:cooked,
:cooked_hidden
@ -22,14 +20,6 @@ class BasicPostSerializer < ApplicationSerializer
object.user.try(:avatar_template)
end
def uploaded_avatar_id
object.user.try(:uploaded_avatar_id)
end
def letter_avatar_color
object.user.try(:letter_avatar_color)
end
def cooked_hidden
object.hidden && !scope.is_staff?
end

View file

@ -1,5 +1,5 @@
class BasicUserSerializer < ApplicationSerializer
attributes :id, :username, :uploaded_avatar_id, :avatar_template, :letter_avatar_color
attributes :id, :username, :avatar_template
def include_name?
SiteSetting.enable_names?
@ -17,12 +17,4 @@ class BasicUserSerializer < ApplicationSerializer
object[:user] || object
end
def letter_avatar_color
if Hash === object
User.letter_avatar_color(user[:username])
else
user.try(:letter_avatar_color)
end
end
end

View file

@ -177,9 +177,7 @@ class PostSerializer < BasicPostSerializer
def reply_to_user
{
username: object.reply_to_user.username,
avatar_template: object.reply_to_user.avatar_template,
uploaded_avatar_id: object.reply_to_user.uploaded_avatar_id,
letter_avatar_color: object.reply_to_user.letter_avatar_color,
avatar_template: object.reply_to_user.avatar_template
}
end

View file

@ -26,12 +26,8 @@ class UserActionSerializer < ApplicationSerializer
:action_code,
:edit_reason,
:category_id,
:uploaded_avatar_id,
:letter_avatar_color,
:closed,
:archived,
:acting_uploaded_avatar_id,
:acting_letter_avatar_color
:archived
def excerpt
cooked = object.cooked || PrettyText.cook(object.raw)
@ -86,12 +82,4 @@ class UserActionSerializer < ApplicationSerializer
object.topic_archived
end
def letter_avatar_color
User.letter_avatar_color(username)
end
def acting_letter_avatar_color
User.letter_avatar_color(acting_username)
end
end

View file

@ -979,8 +979,8 @@ en:
avatar_sizes: "List of automatically generated avatar sizes."
external_letter_avatars_enabled: "Use external letter avatars service."
external_letter_avatars_url: "URL of the external letter avatars service."
external_system_avatars_enabled: "Use external system avatars service."
external_system_avatars_url: "URL of the external system avatars service. Allowed substitions are {username} {first_letter} {color} {size}"
enable_flash_video_onebox: "Enable embedding of swf and flv (Adobe Flash) links in oneboxes. WARNING: may introduce security risks."

View file

@ -572,13 +572,13 @@ files:
avatar_sizes:
default: '20|25|32|45|60|120'
type: list
external_letter_avatars_enabled:
external_system_avatars_enabled:
default: false
client: true
external_letter_avatars_url:
default: "https://avatars.discourse.org"
external_system_avatars_url:
default: "https://avatars.discourse.org/letter/{first_letter}/{color}/{size}.png"
client: true
regex: '^https?:\/\/.+[^\/]$'
regex: '^https?:\/\/.+[^\/]'
trust:
default_trust_level: