").append([
+ this.controls.rotateLeft,
+ this.controls.rotateRight
+ ]));
+
+ // preview images in the middle
+ this.controlRow.append("
");
+
+ // main buttons on the right
+ this.controlRow.append($("
").append([
+ this.controls.reset,
+ this.controls.cancel,
+ this.controls.accept
+ ]));
+ }
+
+ /**
+ * Called when the user clicked accept button. Sets file data and triggers file upload.
+ */
+ cropImage() {
+ const canvas = this.cropper.getCroppedCanvas();
+
+ // replace the stored file with the new canvas
+ this.fineUploader.clearStoredFiles();
+ this.fineUploader.addFiles([{
+ canvas: canvas,
+ name: this.fileName,
+ quality: 100,
+ type: this.mimeType
+ }]);
+
+ // reset all controls
+ this.cancel();
+ this.picture.setAttribute("src", canvas.toDataURL(this.mimeType));
+
+ // finally start uploading
+ this.setLoading(true);
+ this.fineUploader.uploadStoredFiles();
+ }
+
+ /**
+ * Is called after the file upload has been completed and the profile photo changed.
+ * @param {number} id - The current file's id.
+ * @param {string} fileName - The current file's name.
+ * @param {object} responseJSON - The server's json response.
+ */
+ onUploadCompleted(id, fileName, responseJSON) {
+ this.setLoading(false);
+ this.fileInput.classList.remove("hidden");
+
+ if (responseJSON.data !== undefined) {
+ /* flash message prompt */
+ this.showMessage("success", Diaspora.I18n.t("photo_uploader.looking_good"));
+
+ this.info.innerText = Diaspora.I18n.t("photo_uploader.completed", {"file": fileName});
+
+ const photoId = responseJSON.data.photo.id;
+ const url = responseJSON.data.photo.unprocessed_image.url;
+ const oldPhoto = $("#photo_id");
+ if (oldPhoto.length === 0) {
+ $("#update_profile_form")
+ .prepend(``);
+ } else {
+ oldPhoto.val(photoId);
+ }
+
+ this.picture.setAttribute("src", url);
+ $(`.avatar[alt="${gon.user.diaspora_id}"]`).attr("src", url);
+ } else {
+ this.cancel();
+ }
+ }
+
+ /**
+ * Toggles loading state by hiding or showing several elements
+ * @param {boolean} loading - True if loading state should be enabled.
+ */
+ setLoading(loading) {
+ if (loading) {
+ this.fileInput.classList.add("hidden");
+ this.picture.classList.add("hidden");
+ this.spinner.classList.remove("hidden");
+ } else {
+ this.picture.classList.remove("hidden");
+ this.spinner.classList.add("hidden");
+ }
+ }
+
+ /**
+ * Destroys the cropper and resets all elements to initial state.
+ */
+ cancel() {
+ this.cropper.destroy();
+ this.picture.onload = null;
+ this.picture.setAttribute("style", "");
+ this.picture.setAttribute("src", this.previousPicture);
+ this.controlRow.remove();
+ this.fileInput.classList.remove("hidden");
+ this.info.innerText = "";
+
+ this.mimeType = null;
+ this.name = null;
+ }
+};
+// @license-end
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index a59460b96..a560aa0c7 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -44,3 +44,4 @@
//= require bootstrap-markdown/bootstrap-markdown
//= require helpers/markdown_editor
//= require jquery.are-you-sure
+//= require cropperjs/dist/cropper.js
diff --git a/app/assets/javascripts/mobile/mobile.js b/app/assets/javascripts/mobile/mobile.js
index 0bb660573..7d69a4a47 100644
--- a/app/assets/javascripts/mobile/mobile.js
+++ b/app/assets/javascripts/mobile/mobile.js
@@ -16,7 +16,6 @@
//= require bootstrap
//= require diaspora
//= require helpers/i18n
-//= require helpers/profile_photo_uploader
//= require helpers/tags_autocomplete
//= require bootstrap-markdown/bootstrap-markdown
//= require helpers/markdown_editor
@@ -32,4 +31,5 @@
//= require mobile/mobile_conversations
//= require mobile/mobile_post_actions
//= require mobile/mobile_drawer
+//= require mobile/mobile_profile_photo_uploader
// @license-end
diff --git a/app/assets/javascripts/helpers/profile_photo_uploader.js b/app/assets/javascripts/mobile/mobile_profile_photo_uploader.js
similarity index 100%
rename from app/assets/javascripts/helpers/profile_photo_uploader.js
rename to app/assets/javascripts/mobile/mobile_profile_photo_uploader.js
diff --git a/app/assets/stylesheets/_application.scss b/app/assets/stylesheets/_application.scss
index 169bf1e1c..34bf5292b 100644
--- a/app/assets/stylesheets/_application.scss
+++ b/app/assets/stylesheets/_application.scss
@@ -42,6 +42,7 @@
// profile and settings pages
@import 'settings';
+@import 'cropperjs/dist/cropper';
// new SPV
@import 'header';
diff --git a/app/assets/stylesheets/getting-started.scss b/app/assets/stylesheets/getting-started.scss
index 12c4931a1..725ad479d 100644
--- a/app/assets/stylesheets/getting-started.scss
+++ b/app/assets/stylesheets/getting-started.scss
@@ -1,9 +1,4 @@
#hello-there {
- #profile_photo_upload .avatar {
- max-height: 200px;
- max-width: 200px;
- }
-
.well .avatar {
min-width: 50px;
}
diff --git a/app/assets/stylesheets/mobile/mobile.scss b/app/assets/stylesheets/mobile/mobile.scss
index 6447df013..60c05cf1e 100644
--- a/app/assets/stylesheets/mobile/mobile.scss
+++ b/app/assets/stylesheets/mobile/mobile.scss
@@ -631,6 +631,12 @@ h1.session {
}
}
}
+
+ .spinner {
+ height: 50px;
+ margin-top: 10px;
+ width: 50px;
+ }
}
#birth-date {
diff --git a/app/assets/stylesheets/settings.scss b/app/assets/stylesheets/settings.scss
index 11cb5b13f..3dc79ce25 100644
--- a/app/assets/stylesheets/settings.scss
+++ b/app/assets/stylesheets/settings.scss
@@ -13,6 +13,7 @@
padding-right: 10px;
}
}
+
// scss-lint:enable SelectorFormat
.enclosed-checkbox label {
@@ -28,9 +29,31 @@
max-width: 200px;
width: auto;
}
+
+ .crop-container {
+ .controls {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 10px;
+ }
+ }
+
+ .preview {
+ border-radius: 4px;
+ height: 50px;
+ overflow: hidden;
+ width: 50px;
+ }
+
+ .spinner {
+ height: 50px;
+ width: 50px;
+ }
}
-.settings-visibility { margin-left: 10px; }
+.settings-visibility {
+ margin-left: 10px;
+}
.page-profiles.action-edit textarea {
max-width: 100%;
diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb
index 8ebc0908c..5674d888c 100644
--- a/app/controllers/photos_controller.rb
+++ b/app/controllers/photos_controller.rb
@@ -5,14 +5,14 @@
# the COPYRIGHT file.
class PhotosController < ApplicationController
- before_action :authenticate_user!, except: %i(show index)
+ before_action :authenticate_user!, except: %i[show index]
respond_to :html, :json
def show
@photo = if user_signed_in?
- current_user.photos_from(Person.find_by_guid(params[:person_id])).where(id: params[:id]).first
- else
- Photo.where(id: params[:id], public: true).first
+ current_user.photos_from(Person.find_by(guid: params[:person_id])).where(id: params[:id]).first
+ else
+ Photo.where(id: params[:id], public: true).first
end
raise ActiveRecord::RecordNotFound unless @photo
@@ -20,7 +20,7 @@ class PhotosController < ApplicationController
def index
@post_type = :photos
- @person = Person.find_by_guid(params[:person_id])
+ @person = Person.find_by(guid: params[:person_id])
authenticate_user! if @person.try(:remote?) && !user_signed_in?
@presenter = PersonPresenter.new(@person, current_user)
@@ -35,10 +35,10 @@ class PhotosController < ApplicationController
render "people/show", layout: "with_header"
end
format.mobile { render "people/show" }
- format.json{ render_for_api :backbone, :json => @posts, :root => :photos }
+ format.json { render_for_api :backbone, json: @posts, root: :photos }
end
else
- flash[:error] = I18n.t 'people.show.does_not_exist'
+ flash[:error] = I18n.t "people.show.does_not_exist"
redirect_to people_path
end
end
@@ -51,21 +51,23 @@ class PhotosController < ApplicationController
def make_profile_photo
author_id = current_user.person_id
- @photo = Photo.where(:id => params[:photo_id], :author_id => author_id).first
+ @photo = Photo.where(id: params[:photo_id], author_id: author_id).first
if @photo
- profile_hash = {:image_url => @photo.url(:thumb_large),
- :image_url_medium => @photo.url(:thumb_medium),
- :image_url_small => @photo.url(:thumb_small)}
+ profile_hash = {image_url: @photo.url(:thumb_large),
+ image_url_medium: @photo.url(:thumb_medium),
+ image_url_small: @photo.url(:thumb_small)}
if current_user.update_profile(profile_hash)
respond_to do |format|
- format.js{ render :json => { :photo_id => @photo.id,
- :image_url => @photo.url(:thumb_large),
- :image_url_medium => @photo.url(:thumb_medium),
- :image_url_small => @photo.url(:thumb_small),
- :author_id => author_id},
- :status => 201}
+ format.js {
+ render json: {photo_id: @photo.id,
+ image_url: @photo.url(:thumb_large),
+ image_url_medium: @photo.url(:thumb_medium),
+ image_url_small: @photo.url(:thumb_small),
+ author_id: author_id},
+ status: 201
+ }
end
else
head :unprocessable_entity
@@ -76,7 +78,7 @@ class PhotosController < ApplicationController
end
def destroy
- photo = current_user.photos.where(:id => params[:id]).first
+ photo = current_user.photos.where(id: params[:id]).first
if photo
current_user.retract(photo)
@@ -84,16 +86,16 @@ class PhotosController < ApplicationController
respond_to do |format|
format.json { head :no_content }
format.html do
- flash[:notice] = I18n.t 'photos.destroy.notice'
- if StatusMessage.find_by_guid(photo.status_message_guid)
- respond_with photo, :location => post_path(photo.status_message)
+ flash[:notice] = I18n.t "photos.destroy.notice"
+ if StatusMessage.find_by(guid: photo.status_message_guid)
+ respond_with photo, location: post_path(photo.status_message)
else
- respond_with photo, :location => person_photos_path(current_user.person)
+ respond_with photo, location: person_photos_path(current_user.person)
end
end
end
else
- respond_with photo, :location => person_photos_path(current_user.person)
+ respond_with photo, location: person_photos_path(current_user.person)
end
end
@@ -105,23 +107,27 @@ class PhotosController < ApplicationController
def file_handler(params)
# For XHR file uploads, request.params[:qqfile] will be the path to the temporary file
- # For regular form uploads (such as those made by Opera), request.params[:qqfile] will be an UploadedFile which can be returned unaltered.
- if not request.params[:qqfile].is_a?(String)
- params[:qqfile]
+ # For regular form uploads (such as those made by Opera), request.params[:qqfile] will be an
+ # UploadedFile which can be returned unaltered.
+ if !request.params[:qqfile].is_a?(String)
+ qqfile = params[:qqfile]
+ # Cropped or manipulated files have their real filename only in qqfilename. Take care of this.
+ qqfile.original_filename = params[:qqfilename] if qqfile.original_filename == "blob"
+ qqfile
else
######################## dealing with local files #############
# get file name
file_name = params[:qqfile]
# get file content type
- att_content_type = (request.content_type.to_s == "") ? "application/octet-stream" : request.content_type.to_s
+ att_content_type = request.content_type.to_s == "" ? "application/octet-stream" : request.content_type.to_s
# create tempora##l file
- file = Tempfile.new(file_name, {:encoding => 'BINARY'})
+ file = Tempfile.new(file_name, encoding: "BINARY")
# put data into this file from raw post request
- file.print request.raw_post.force_encoding('BINARY')
+ file.print request.raw_post.force_encoding("BINARY")
# create several required methods for this temporal file
- Tempfile.send(:define_method, "content_type") {return att_content_type}
- Tempfile.send(:define_method, "original_filename") {return file_name}
+ Tempfile.send(:define_method, "content_type") { return att_content_type }
+ Tempfile.send(:define_method, "original_filename") { return file_name }
file
end
end
@@ -149,36 +155,35 @@ class PhotosController < ApplicationController
end
if photo_params[:set_profile_photo]
- profile_params = {:image_url => @photo.url(:thumb_large),
- :image_url_medium => @photo.url(:thumb_medium),
- :image_url_small => @photo.url(:thumb_small)}
+ profile_params = {image_url: @photo.url(:thumb_large),
+ image_url_medium: @photo.url(:thumb_medium),
+ image_url_small: @photo.url(:thumb_small)}
current_user.update_profile(profile_params)
end
respond_to do |format|
- format.json{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )}
- format.html{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )}
+ format.json { render(layout: false, json: {"success" => true, "data" => @photo}.to_json) }
+ format.html { render(layout: false, json: {"success" => true, "data" => @photo}.to_json) }
end
else
- respond_with @photo, :location => photos_path, :error => message
+ respond_with @photo, location: photos_path, error: message
end
end
def rescuing_photo_errors
- begin
- yield
- rescue TypeError
- message = I18n.t 'photos.create.type_error'
- respond_with @photo, :location => photos_path, :error => message
+ yield
+ rescue TypeError
+ return_photo_error I18n.t "photos.create.type_error"
+ rescue CarrierWave::IntegrityError
+ return_photo_error I18n.t "photos.create.integrity_error"
+ rescue RuntimeError
+ return_photo_error I18n.t "photos.create.runtime_error"
+ end
- rescue CarrierWave::IntegrityError
- message = I18n.t 'photos.create.integrity_error'
- respond_with @photo, :location => photos_path, :error => message
-
- rescue RuntimeError => e
- message = I18n.t 'photos.create.runtime_error'
- respond_with @photo, :location => photos_path, :error => message
- raise e
+ def return_photo_error(message)
+ respond_to do |format|
+ format.json { render(layout: false, json: {"success" => false, "error" => message}.to_json) }
+ format.html { render(layout: false, json: {"success" => false, "error" => message}.to_json) }
end
end
end
diff --git a/app/views/photos/_new_profile_photo.haml b/app/views/photos/_new_profile_photo.haml
index f5cb61659..2b24f2b2e 100644
--- a/app/views/photos/_new_profile_photo.haml
+++ b/app/views/photos/_new_profile_photo.haml
@@ -3,14 +3,15 @@
-# the COPYRIGHT file.
.profile-photo-upload#profile_photo_upload
- = owner_image_tag(:thumb_large)
+ .crop-container
+ = owner_image_tag(:thumb_large)
.small-horizontal-spacer
.clearfix
.text-center
#file-upload.btn.btn-primary
=t('.upload')
- = image_tag('mobile-spinner.gif', :class => 'hidden', :style => "z-index:-1", :id => 'file-upload-spinner')
+ .spinner.hidden#file-upload-spinner
%p
#fileInfo
diff --git a/features/desktop/edits_profile.feature b/features/desktop/edits_profile.feature
index bf68ed972..90e7b06bb 100644
--- a/features/desktop/edits_profile.feature
+++ b/features/desktop/edits_profile.feature
@@ -44,12 +44,31 @@ Feature: editing your profile
And I press "update_profile"
Then I should see "#kamino" within "ul#as-selections-tags"
And I should see "#starwars" within "ul#as-selections-tags"
+ And I should see a ".crop-container" within "#profile_photo_upload"
And the "#profile_public_details" bootstrap-switch should be on
When I attach the file "spec/fixtures/bad_urls.txt" to "qqfile" within "#file-upload"
Then I should see a flash message indicating failure
When I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload"
+ Then I should see a ".cropper-container" within ".crop-container"
+ And I should see a ".controls" within ".crop-container"
+ And I should see a ".preview" within ".controls"
+ And I should see 2 ".btn" within ".buttons-left"
+ And I should see 3 ".btn" within ".buttons-right"
+
+ When I press the 2nd ".btn" within ".buttons-right"
+ Then I should see a ".avatar" within ".crop-container"
+ But I should not see a ".cropper-container" within ".crop-container"
+
+ When I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload"
+ Then I should see a ".cropper-container" within ".crop-container"
+ And I should see a ".controls" within ".crop-container"
+ And I should see a ".preview" within ".controls"
+ And I should see 2 ".btn" within ".buttons-left"
+ And I should see 3 ".btn" within ".buttons-right"
+
+ When I press the 3rd ".btn" within ".buttons-right"
Then I should see "button.png completed"
And I should see a "img" within "#profile_photo_upload"
diff --git a/features/step_definitions/custom_web_steps.rb b/features/step_definitions/custom_web_steps.rb
index 041afe439..c5caf120c 100644
--- a/features/step_definitions/custom_web_steps.rb
+++ b/features/step_definitions/custom_web_steps.rb
@@ -182,6 +182,12 @@ Then /^(?:|I )should see a "([^\"]*)"(?: within "([^\"]*)")?$/ do |selector, sco
end
end
+Then /^I should see (\d+) "([^\"]*)"(?: within "([^\"]*)")?$/ do |count, selector, scope_selector|
+ with_scope(scope_selector) do
+ expect(current_scope).to have_selector(selector, count: count)
+ end
+end
+
Then /^(?:|I )should not see a "([^\"]*)"(?: within "([^\"]*)")?$/ do |selector, scope_selector|
with_scope(scope_selector) do
current_scope.should have_no_css(selector, :visible => true)
diff --git a/features/step_definitions/web_steps.rb b/features/step_definitions/web_steps.rb
index 38f7d0ab4..3cb2a75fb 100644
--- a/features/step_definitions/web_steps.rb
+++ b/features/step_definitions/web_steps.rb
@@ -96,7 +96,8 @@ When /^(?:|I )attach the file "([^"]*)" to (?:hidden )?"([^"]*)"(?: within "([^"
attach_file(field, Rails.root.join(path).to_s)
end
# wait for the image to be ready
- page.assert_selector(".loading", count: 0)
+ page.assert_no_selector(".loading")
+ page.assert_no_selector("#file-upload-spinner")
end
Then /^(?:|I )should see (\".+?\"[\s]*)(?:[\s]+within[\s]* "([^"]*)")?$/ do |vars, selector|