diff --git a/app/assets/javascripts/discourse/components/utilities.js b/app/assets/javascripts/discourse/components/utilities.js
index bf2b0f0ec..f9bbd02e7 100644
--- a/app/assets/javascripts/discourse/components/utilities.js
+++ b/app/assets/javascripts/discourse/components/utilities.js
@@ -291,6 +291,54 @@ Discourse.Utilities = {
     }
     // otherwise, display a generic error message
     bootbox.alert(I18n.t('post.errors.upload'));
+  },
+
+  /**
+    Crop an image to be used as avatar.
+    Simulate the "centered square thumbnail" generation done server-side.
+    Uses only the first frame of animated gifs when they are disabled.
+
+    @method cropAvatar
+    @param {String} url The url of the avatar
+    @param {String} fileType The file type of the uploaded file
+    @returns {Ember.Deferred} a promise that will eventually be the cropped avatar.
+  **/
+  cropAvatar: function(url, fileType) {
+    if (Discourse.SiteSettings.allow_animated_avatars && fileType === "image/gif") {
+      // can't crop animated gifs... let the browser stretch the gif
+      return Ember.RSVP.resolve(url);
+    } else {
+      return Ember.Deferred.promise(function(promise) {
+        var image = document.createElement("img");
+        // this event will be fired as soon as the image is loaded
+        image.onload = function(e) {
+          var img = e.target;
+          // computes the dimension & position (x, y) of the largest square we can fit in the image
+          var width = img.width, height = img.height, dimension, center, x, y;
+          if (width <= height) {
+            dimension = width;
+            center = height / 2;
+            x = 0;
+            y = center - (dimension / 2);
+          } else {
+            dimension = height;
+            center = width / 2;
+            x = center - (dimension / 2);
+            y = 0;
+          }
+          // set the size of the canvas to the maximum available size for avatars (browser will take care of downsizing the image)
+          var canvas = document.createElement("canvas");
+          var size = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize("huge"));
+          canvas.height = canvas.width = size;
+          // draw the image into the canvas
+          canvas.getContext("2d").drawImage(img, x, y, dimension, dimension, 0, 0, size, size);
+          // retrieve the image from the canvas
+          promise.resolve(canvas.toDataURL(fileType));
+        };
+        // launch the onload event
+        image.src = url;
+      });
+    }
   }
 
 };
diff --git a/app/assets/javascripts/discourse/views/modal/avatar_selector_view.js b/app/assets/javascripts/discourse/views/modal/avatar_selector_view.js
index 0f685ed8a..287a16973 100644
--- a/app/assets/javascripts/discourse/views/modal/avatar_selector_view.js
+++ b/app/assets/javascripts/discourse/views/modal/avatar_selector_view.js
@@ -51,11 +51,17 @@ Discourse.AvatarSelectorView = Discourse.ModalBodyView.extend({
 
     // when the upload is successful
     $upload.on("fileuploaddone", function (e, data) {
-      // set some properties
+      // indicates the users is using an uploaded avatar
       view.get("controller").setProperties({
         has_uploaded_avatar: true,
-        use_uploaded_avatar: true,
-        uploaded_avatar_template: data.result.url
+        use_uploaded_avatar: true
+      });
+      // in order to be as much responsive as possible, we're cheating a bit here
+      // indeed, the server gives us back the url to the file we've just uploaded
+      // often, this file is not a square, so we need to crop it properly
+      // this will also capture the first frame of animated avatars when they're not allowed
+      Discourse.Utilities.cropAvatar(data.result.url, data.files[0].type).then(function(avatarTemplate) {
+        view.get("controller").set("uploaded_avatar_template", avatarTemplate);
       });
     });
 
diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb
index 681059c8c..80a7b2045 100644
--- a/app/models/optimized_image.rb
+++ b/app/models/optimized_image.rb
@@ -19,7 +19,7 @@ class OptimizedImage < ActiveRecord::Base
     temp_file = Tempfile.new(["discourse-thumbnail", File.extname(original_path)])
     temp_path = temp_file.path
 
-    if ImageSorcery.new(original_path).convert(temp_path, resize: "#{width}x#{height}")
+    if ImageSorcery.new("#{original_path}[0]").convert(temp_path, resize: "#{width}x#{height}")
       thumbnail = OptimizedImage.create!(
         upload_id: upload.id,
         sha1: Digest::SHA1.file(temp_path).hexdigest,
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index bf560381f..062099c10 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -245,6 +245,7 @@ class SiteSetting < ActiveRecord::Base
   setting(:username_change_period, 3) # days
 
   client_setting(:allow_uploaded_avatars, true)
+  client_setting(:allow_animated_avatars, false)
 
   def self.generate_api_key!
     self.api_key = SecureRandom.hex(32)
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index ca78b45b1..015edf89c 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -665,7 +665,8 @@ en:
     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."
     username_change_period: "The number of days after registration that accounts can change their username."
 
-    allow_uploaded_avatars: "Allow support for uploaded avatars"
+    allow_uploaded_avatars: "Allow users to upload their custom avatars"
+    allow_animated_avatars: "Allow users to use animated gif for avatars"
 
   notification_types:
     mentioned: "%{display_username} mentioned you in %{link}"
diff --git a/lib/jobs/generate_avatars.rb b/lib/jobs/generate_avatars.rb
index abc4960a0..eac7e739a 100644
--- a/lib/jobs/generate_avatars.rb
+++ b/lib/jobs/generate_avatars.rb
@@ -15,6 +15,10 @@ module Jobs
         Discourse.store.path_for(upload)
       end
 
+      # we'll extract the first frame when it's a gif
+      source = original_path
+      source << "[0]" unless SiteSetting.allow_animated_avatars
+
       [120, 45, 32, 25, 20].each do |s|
         # handle retina too
         [s, s * 2].each do |size|
@@ -22,7 +26,7 @@ module Jobs
           temp_file = Tempfile.new(["discourse-avatar", File.extname(original_path)])
           temp_path = temp_file.path
           # create a centered square thumbnail
-          if ImageSorcery.new(original_path).convert(temp_path, gravity: "center", thumbnail: "#{size}x#{size}^", extent: "#{size}x#{size}", background: "transparent")
+          if ImageSorcery.new(source).convert(temp_path, gravity: "center", thumbnail: "#{size}x#{size}^", extent: "#{size}x#{size}", background: "transparent")
             Discourse.store.store_avatar(temp_file, upload, size)
           end
           # close && remove temp file
diff --git a/test/javascripts/components/utilities_test.js b/test/javascripts/components/utilities_test.js
index a0dae30b8..7981dd465 100644
--- a/test/javascripts/components/utilities_test.js
+++ b/test/javascripts/components/utilities_test.js
@@ -132,3 +132,16 @@ test("avatarImg", function() {
   blank(Discourse.Utilities.avatarImg({avatarTemplate: "", size: 'tiny'}),
         "it doesn't render avatars for invalid avatar template");
 });
+
+module("Discourse.Utilities.cropAvatar with animated avatars", {
+  setup: function() { Discourse.SiteSettings.allow_animated_avatars = true; }
+});
+
+asyncTestDiscourse("cropAvatar", function() {
+  expect(1);
+
+  Discourse.Utilities.cropAvatar("/path/to/avatar.gif", "image/gif").then(function(avatarTemplate) {
+    equal(avatarTemplate, "/path/to/avatar.gif", "returns the url to the gif when animated gif are enabled");
+    start();
+  });
+});