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 += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">";
iconsHtml += Discourse.Utilities.avatarImg({ iconsHtml += Discourse.Utilities.avatarImg({
size: 'small', size: 'small',
avatarTemplate: u.get('avatarTemplate'), avatarTemplate: u.get('avatar_template'),
title: u.get('username') title: u.get('username')
}); });
iconsHtml += "</a>"; iconsHtml += "</a>";

View file

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

View file

@ -1,15 +1,14 @@
import registerUnbound from 'discourse/helpers/register-unbound'; import registerUnbound from 'discourse/helpers/register-unbound';
import avatarTemplate from 'discourse/lib/avatar-template';
function renderAvatar(user, options) { function renderAvatar(user, options) {
options = options || {}; options = options || {};
if (user) { if (user) {
let username = Em.get(user, 'username');
if (!username) { const username = Em.get(user, options.usernamePath || 'username');
if (!options.usernamePath) { return ''; } const avatarTemplate = Em.get(user, options.avatarTemplatePath || 'avatar_template');
username = Em.get(user, options.usernamePath);
} if (!username || !avatarTemplate) { return ''; }
let title; let title;
if (!options.ignoreTitle) { 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({ return Discourse.Utilities.avatarImg({
size: options.imageSize, size: options.imageSize,
extraClasses: Em.get(user, 'extras') || options.extraClasses, extraClasses: Em.get(user, 'extras') || options.extraClasses,
title: title || username, title: title || username,
avatarTemplate: Em.get("avatar_template") || avatarTemplate(username, uploadedAvatarId, letterAvatarColor) avatarTemplate: avatarTemplate
}); });
} else { } else {
return ''; 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'), username: user.get('username'),
user_id: user.get('id'), user_id: user.get('id'),
user_title: user.get('title'), 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'), user_custom_fields: user.get('custom_fields'),
post_type: this.site.get('post_types.regular'), post_type: this.site.get('post_types.regular'),
actions_summary: [], actions_summary: [],
@ -587,7 +587,7 @@ const Composer = RestModel.extend({
reply_to_post_number: post.get('post_number'), reply_to_post_number: post.get('post_number'),
reply_to_user: { reply_to_user: {
username: post.get('username'), 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() { switchToActing() {
this.setProperties({ this.setProperties({
username: this.get('acting_username'), 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') name: this.get('actingDisplayName')
}); });
} }

View file

@ -1,6 +1,5 @@
import { url } from 'discourse/lib/computed'; import { url } from 'discourse/lib/computed';
import RestModel from 'discourse/models/rest'; import RestModel from 'discourse/models/rest';
import avatarTemplate from 'discourse/lib/avatar-template';
import UserStream from 'discourse/models/user-stream'; import UserStream from 'discourse/models/user-stream';
import UserPostsStream from 'discourse/models/user-posts-stream'; import UserPostsStream from 'discourse/models/user-posts-stream';
import Singleton from 'discourse/mixins/singleton'; 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 Change avatar selection
*/ */

View file

@ -23,7 +23,7 @@
{{fa-icon 'times'}} {{i18n "bookmarks.remove"}} {{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
</button> </button>
{{else}} {{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 grandChild.edit_reason}} &mdash; <span class="edit-reason">{{grandChild.edit_reason}}</span>{{/if}}
{{/if}} {{/if}}
{{/each}} {{/each}}

View file

@ -1,5 +1,5 @@
<td class='posters'> <td class='posters'>
{{#each poster in 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}} {{/each}}
</td> </td>

View file

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

View file

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

View file

@ -518,7 +518,7 @@ class UsersController < ApplicationController
user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id] user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id]
user_fields << :name if SiteSetting.enable_names? 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" if params[:include_groups] == "true"
to_render[:groups] = Group.search_group(term, current_user).map {|m| {:name=>m.name, :usernames=> m.usernames.split(",")} } 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] split_avatars[hash.abs % split_avatars.size]
end end
else else
letter_avatar_template(username) system_avatar_template(username)
end end
end end
@ -468,10 +468,15 @@ class User < ActiveRecord::Base
UserAvatar.local_avatar_template(hostname, username.downcase, uploaded_avatar_id) UserAvatar.local_avatar_template(hostname, username.downcase, uploaded_avatar_id)
end end
def self.letter_avatar_template(username) def self.system_avatar_template(username)
if SiteSetting.external_letter_avatars_enabled # 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) 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 else
"#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png"
end end
@ -484,7 +489,7 @@ class User < ActiveRecord::Base
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]
color.map { |c| c.to_s(16) }.join color.map { |c| c.to_s(16).rjust(2, '0') }.join
end end
def avatar_template def avatar_template

View file

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

View file

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

View file

@ -177,9 +177,7 @@ class PostSerializer < BasicPostSerializer
def reply_to_user def reply_to_user
{ {
username: object.reply_to_user.username, username: object.reply_to_user.username,
avatar_template: object.reply_to_user.avatar_template, 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,
} }
end end

View file

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

View file

@ -979,8 +979,8 @@ en:
avatar_sizes: "List of automatically generated avatar sizes." avatar_sizes: "List of automatically generated avatar sizes."
external_letter_avatars_enabled: "Use external letter avatars service." external_system_avatars_enabled: "Use external system avatars service."
external_letter_avatars_url: "URL of the external letter 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." 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: avatar_sizes:
default: '20|25|32|45|60|120' default: '20|25|32|45|60|120'
type: list type: list
external_letter_avatars_enabled: external_system_avatars_enabled:
default: false default: false
client: true client: true
external_letter_avatars_url: external_system_avatars_url:
default: "https://avatars.discourse.org" default: "https://avatars.discourse.org/letter/{first_letter}/{color}/{size}.png"
client: true client: true
regex: '^https?:\/\/.+[^\/]$' regex: '^https?:\/\/.+[^\/]'
trust: trust:
default_trust_level: default_trust_level: