diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java index 391918e6..dd3d8e65 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java @@ -8,6 +8,7 @@ import android.util.Log; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonIOException; import com.google.gson.JsonObject; @@ -25,6 +26,8 @@ import java.io.IOException; import java.io.Reader; import java.time.Instant; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -157,10 +160,29 @@ public class MastodonAPIController{ try{ JsonObject error=JsonParser.parseReader(reader).getAsJsonObject(); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error); - req.onError(error.get("error").getAsString(), response.code()); + if(error.has("details")){ + MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code()); + HashMap> details=new HashMap<>(); + JsonObject errorDetails=error.getAsJsonObject("details"); + for(String key:errorDetails.keySet()){ + ArrayList fieldErrors=new ArrayList<>(); + for(JsonElement el:errorDetails.getAsJsonArray(key)){ + JsonObject eobj=el.getAsJsonObject(); + MastodonDetailedErrorResponse.FieldError fe=new MastodonDetailedErrorResponse.FieldError(); + fe.description=eobj.get("description").getAsString(); + fe.error=eobj.get("error").getAsString(); + fieldErrors.add(fe); + } + details.put(key, fieldErrors); + } + err.detailedErrors=details; + req.onError(err); + }else{ + req.onError(error.get("error").getAsString(), response.code()); + } }catch(JsonIOException|JsonSyntaxException x){ req.onError(response.code()+" "+response.message(), response.code()); - }catch(IllegalStateException x){ + }catch(Exception x){ req.onError("Error parsing an API error", response.code()); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java index a16319c9..8f38d138 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java @@ -25,6 +25,7 @@ import androidx.annotation.CallSuper; import androidx.annotation.StringRes; import me.grishka.appkit.api.APIRequest; import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; import okhttp3.Call; import okhttp3.RequestBody; @@ -183,6 +184,10 @@ public abstract class MastodonAPIRequest extends APIRequest{ } } + void onError(ErrorResponse err){ + invokeErrorCallback(err); + } + void onError(String msg, int httpStatus){ invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus)); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonDetailedErrorResponse.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonDetailedErrorResponse.java new file mode 100644 index 00000000..61ac1cdb --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonDetailedErrorResponse.java @@ -0,0 +1,18 @@ +package org.joinmastodon.android.api; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MastodonDetailedErrorResponse extends MastodonErrorResponse{ + public Map> detailedErrors; + + public MastodonDetailedErrorResponse(String error, int httpStatus){ + super(error, httpStatus); + } + + public static class FieldError{ + public String error; + public String description; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java index 1000fe4e..1d6d1f73 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; @@ -17,11 +18,10 @@ import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import org.joinmastodon.android.R; import org.joinmastodon.android.api.MastodonAPIController; -import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.api.MastodonDetailedErrorResponse; import org.joinmastodon.android.api.requests.accounts.RegisterAccount; import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp; import org.joinmastodon.android.api.requests.oauth.GetOauthToken; @@ -39,8 +39,11 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; import androidx.annotation.Nullable; import me.grishka.appkit.Nav; @@ -70,6 +73,7 @@ public class SignupFragment extends AppKitFragment{ private ProgressDialog progressDialog; private Uri avatarUri; private File avatarFile; + private HashSet errorFields=new HashSet<>(); @Override public void onCreate(Bundle savedInstanceState){ @@ -115,6 +119,10 @@ public class SignupFragment extends AppKitFragment{ email.addTextChangedListener(buttonStateUpdater); password.addTextChangedListener(buttonStateUpdater); + username.addTextChangedListener(new ErrorClearingListener(username)); + email.addTextChangedListener(new ErrorClearingListener(email)); + password.addTextChangedListener(new ErrorClearingListener(password)); + avaWrap.setOutlineProvider(OutlineProviders.roundedRect(22)); avaWrap.setClipToOutline(true); avaWrap.setOnClickListener(v->onAvatarClick()); @@ -172,8 +180,12 @@ public class SignupFragment extends AppKitFragment{ } private void actuallySubmit(){ - String username=this.username.getText().toString(); - String email=this.email.getText().toString(); + String username=this.username.getText().toString().trim(); + String email=this.email.getText().toString().trim(); + for(EditText edit:errorFields){ + edit.setError(null); + } + errorFields.clear(); new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), null) .setCallback(new Callback<>(){ @Override @@ -194,7 +206,23 @@ public class SignupFragment extends AppKitFragment{ @Override public void onError(ErrorResponse error){ - error.showToast(getActivity()); + if(error instanceof MastodonDetailedErrorResponse){ + Map> fieldErrors=((MastodonDetailedErrorResponse) error).detailedErrors; + boolean first=true; + for(String fieldName:fieldErrors.keySet()){ + EditText field=getFieldByName(fieldName); + if(field==null) + continue; + field.setError(fieldErrors.get(fieldName).stream().map(err->err.description).collect(Collectors.joining("\n"))); + errorFields.add(field); + if(first){ + first=false; + field.requestFocus(); + } + } + }else{ + error.showToast(getActivity()); + } progressDialog.dismiss(); progressDialog=null; } @@ -202,6 +230,15 @@ public class SignupFragment extends AppKitFragment{ .exec(instance.uri, apiToken); } + private EditText getFieldByName(String name){ + return switch(name){ + case "email" -> email; + case "username" -> username; + case "password" -> password; + default -> null; + }; + } + private void showProgressDialog(){ progressDialog=new ProgressDialog(getActivity()); progressDialog.setMessage(getString(R.string.loading)); @@ -287,4 +324,30 @@ public class SignupFragment extends AppKitFragment{ private void onAvatarClick(){ startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*").addCategory(Intent.CATEGORY_OPENABLE), AVATAR_RESULT); } + + private class ErrorClearingListener implements TextWatcher{ + public final EditText editText; + + private ErrorClearingListener(EditText editText){ + this.editText=editText; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after){ + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count){ + + } + + @Override + public void afterTextChanged(Editable s){ + if(errorFields.contains(editText)){ + errorFields.remove(editText); + editText.setError(null); + } + } + } } diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index 35482dde..b98fd55e 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -176,7 +176,7 @@ Back Mastodon is made of users in different communities. Pick a community based on your interests, region, or a general purpose one. You can still connect with everyone, regardless of community. - Search communities + Search communities or enter URL Some ground rules Take a minute to review the rules set and enforced by %s admins. Let\'s get you set up on %s