Skip to content

Commit ee4ffe7

Browse files
committed
Merge pull request discourse#1372 from ZogStriP/site-setting-for-allowing-animated-avatars
add a site setting for allowing animated avatars
2 parents 663adde + 43a8bff commit ee4ffe7

7 files changed

Lines changed: 79 additions & 6 deletions

File tree

app/assets/javascripts/discourse/components/utilities.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,54 @@ Discourse.Utilities = {
291291
}
292292
// otherwise, display a generic error message
293293
bootbox.alert(I18n.t('post.errors.upload'));
294+
},
295+
296+
/**
297+
Crop an image to be used as avatar.
298+
Simulate the "centered square thumbnail" generation done server-side.
299+
Uses only the first frame of animated gifs when they are disabled.
300+
301+
@method cropAvatar
302+
@param {String} url The url of the avatar
303+
@param {String} fileType The file type of the uploaded file
304+
@returns {Ember.Deferred} a promise that will eventually be the cropped avatar.
305+
**/
306+
cropAvatar: function(url, fileType) {
307+
if (Discourse.SiteSettings.allow_animated_avatars && fileType === "image/gif") {
308+
// can't crop animated gifs... let the browser stretch the gif
309+
return Ember.RSVP.resolve(url);
310+
} else {
311+
return Ember.Deferred.promise(function(promise) {
312+
var image = document.createElement("img");
313+
// this event will be fired as soon as the image is loaded
314+
image.onload = function(e) {
315+
var img = e.target;
316+
// computes the dimension & position (x, y) of the largest square we can fit in the image
317+
var width = img.width, height = img.height, dimension, center, x, y;
318+
if (width <= height) {
319+
dimension = width;
320+
center = height / 2;
321+
x = 0;
322+
y = center - (dimension / 2);
323+
} else {
324+
dimension = height;
325+
center = width / 2;
326+
x = center - (dimension / 2);
327+
y = 0;
328+
}
329+
// set the size of the canvas to the maximum available size for avatars (browser will take care of downsizing the image)
330+
var canvas = document.createElement("canvas");
331+
var size = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize("huge"));
332+
canvas.height = canvas.width = size;
333+
// draw the image into the canvas
334+
canvas.getContext("2d").drawImage(img, x, y, dimension, dimension, 0, 0, size, size);
335+
// retrieve the image from the canvas
336+
promise.resolve(canvas.toDataURL(fileType));
337+
};
338+
// launch the onload event
339+
image.src = url;
340+
});
341+
}
294342
}
295343

296344
};

app/assets/javascripts/discourse/views/modal/avatar_selector_view.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,17 @@ Discourse.AvatarSelectorView = Discourse.ModalBodyView.extend({
5151

5252
// when the upload is successful
5353
$upload.on("fileuploaddone", function (e, data) {
54-
// set some properties
54+
// indicates the users is using an uploaded avatar
5555
view.get("controller").setProperties({
5656
has_uploaded_avatar: true,
57-
use_uploaded_avatar: true,
58-
uploaded_avatar_template: data.result.url
57+
use_uploaded_avatar: true
58+
});
59+
// in order to be as much responsive as possible, we're cheating a bit here
60+
// indeed, the server gives us back the url to the file we've just uploaded
61+
// often, this file is not a square, so we need to crop it properly
62+
// this will also capture the first frame of animated avatars when they're not allowed
63+
Discourse.Utilities.cropAvatar(data.result.url, data.files[0].type).then(function(avatarTemplate) {
64+
view.get("controller").set("uploaded_avatar_template", avatarTemplate);
5965
});
6066
});
6167

app/models/optimized_image.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def self.create_for(upload, width, height)
1919
temp_file = Tempfile.new(["discourse-thumbnail", File.extname(original_path)])
2020
temp_path = temp_file.path
2121

22-
if ImageSorcery.new(original_path).convert(temp_path, resize: "#{width}x#{height}")
22+
if ImageSorcery.new("#{original_path}[0]").convert(temp_path, resize: "#{width}x#{height}")
2323
thumbnail = OptimizedImage.create!(
2424
upload_id: upload.id,
2525
sha1: Digest::SHA1.file(temp_path).hexdigest,

app/models/site_setting.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ class SiteSetting < ActiveRecord::Base
245245
setting(:username_change_period, 3) # days
246246

247247
client_setting(:allow_uploaded_avatars, true)
248+
client_setting(:allow_animated_avatars, false)
248249

249250
def self.generate_api_key!
250251
self.api_key = SecureRandom.hex(32)

config/locales/server.en.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,8 @@ en:
665665
delete_all_posts_max: "The maximum number of posts that can be deleted at once with the Delete All Posts button. If a user has more than this many posts, the posts cannot all be deleted at once and the user can't be deleted."
666666
username_change_period: "The number of days after registration that accounts can change their username."
667667

668-
allow_uploaded_avatars: "Allow support for uploaded avatars"
668+
allow_uploaded_avatars: "Allow users to upload their custom avatars"
669+
allow_animated_avatars: "Allow users to use animated gif for avatars"
669670

670671
notification_types:
671672
mentioned: "%{display_username} mentioned you in %{link}"

lib/jobs/generate_avatars.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ def execute(args)
1515
Discourse.store.path_for(upload)
1616
end
1717

18+
# we'll extract the first frame when it's a gif
19+
source = original_path
20+
source << "[0]" unless SiteSetting.allow_animated_avatars
21+
1822
[120, 45, 32, 25, 20].each do |s|
1923
# handle retina too
2024
[s, s * 2].each do |size|
2125
# create a temp file with the same extension as the original
2226
temp_file = Tempfile.new(["discourse-avatar", File.extname(original_path)])
2327
temp_path = temp_file.path
2428
# create a centered square thumbnail
25-
if ImageSorcery.new(original_path).convert(temp_path, gravity: "center", thumbnail: "#{size}x#{size}^", extent: "#{size}x#{size}", background: "transparent")
29+
if ImageSorcery.new(source).convert(temp_path, gravity: "center", thumbnail: "#{size}x#{size}^", extent: "#{size}x#{size}", background: "transparent")
2630
Discourse.store.store_avatar(temp_file, upload, size)
2731
end
2832
# close && remove temp file

test/javascripts/components/utilities_test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,16 @@ test("avatarImg", function() {
132132
blank(Discourse.Utilities.avatarImg({avatarTemplate: "", size: 'tiny'}),
133133
"it doesn't render avatars for invalid avatar template");
134134
});
135+
136+
module("Discourse.Utilities.cropAvatar with animated avatars", {
137+
setup: function() { Discourse.SiteSettings.allow_animated_avatars = true; }
138+
});
139+
140+
asyncTestDiscourse("cropAvatar", function() {
141+
expect(1);
142+
143+
Discourse.Utilities.cropAvatar("/path/to/avatar.gif", "image/gif").then(function(avatarTemplate) {
144+
equal(avatarTemplate, "/path/to/avatar.gif", "returns the url to the gif when animated gif are enabled");
145+
start();
146+
});
147+
});

0 commit comments

Comments
 (0)