From bf44f7ef13e51c7f93ef7bf6b20a37bd6c2a4fc9 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sat, 8 Jun 2024 14:49:39 +0300 Subject: [PATCH] VQA fixes part 1 --- .../onboarding/InstanceCatalogFragment.java | 4 +- .../InstanceCatalogSignupFragment.java | 109 ++++++------------ .../InstanceChooserLoginFragment.java | 18 ++- .../fragments/onboarding/SignupFragment.java | 83 +++++++++---- .../android/ui/utils/UiUtils.java | 16 +++ .../ui/views/FloatingHintEditTextLayout.java | 6 + .../color/m3_on_surface_variant_alpha40.xml | 4 + .../src/main/res/layout/alert_invite_link.xml | 5 +- .../res/layout/fragment_onboarding_signup.xml | 33 +++--- .../res/layout/header_onboarding_login.xml | 5 +- mastodon/src/main/res/values/strings.xml | 9 +- 11 files changed, 168 insertions(+), 124 deletions(-) create mode 100644 mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java index 78365f6f..da6df80f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java @@ -73,8 +73,6 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment onError){ - if(TextUtils.isEmpty(_domain)) + if(TextUtils.isEmpty(_domain) || _domain.indexOf('.')==-1) return; String domain=normalizeInstanceDomain(_domain); Instance cachedInstance=instancesCache.get(domain); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java index 56afdd3c..37cb5b84 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java @@ -5,8 +5,9 @@ import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Bundle; import android.text.Editable; @@ -26,20 +27,17 @@ import android.widget.PopupMenu; import android.widget.RadioButton; import android.widget.RelativeLayout; import android.widget.TextView; -import android.widget.Toast; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.requests.accounts.CheckInviteLink; -import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories; import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances; import org.joinmastodon.android.model.Instance; -import org.joinmastodon.android.model.catalog.CatalogCategory; import org.joinmastodon.android.model.catalog.CatalogInstance; import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.FilterChipView; @@ -49,11 +47,9 @@ import org.parceler.Parcels; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.Random; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -71,9 +67,6 @@ import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; public class InstanceCatalogSignupFragment extends InstanceCatalogFragment implements OnBackPressedListener{ - private MastodonAPIRequest getCategoriesRequest; - private String currentCategory="all"; - private List categories=new ArrayList<>(); private View topBar; private List languages=Collections.emptyList(); @@ -149,58 +142,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple } }) .execNoAuth(""); - getCategoriesRequest=new GetCatalogCategories(null) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(List result){ - getCategoriesRequest=null; - CatalogCategory all=new CatalogCategory(); - all.category="all"; - categories.add(all); - result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add); - updateCategories(); - } - - @Override - public void onError(ErrorResponse error){ - getCategoriesRequest=null; - error.showToast(getActivity()); - CatalogCategory all=new CatalogCategory(); - all.category="all"; - categories.add(all); - updateCategories(); - } - }) - .execNoAuth(""); - } - - private void updateCategories(){ -// categoriesList.removeAllTabs(); -// for(CatalogCategory cat:categories){ -// int titleRes=getTitleForCategory(cat.category); -// TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category); -// ImageView emoji=tab.getCustomView().findViewById(R.id.emoji); -// emoji.setImageResource(getEmojiForCategory(cat.category)); -// categoriesList.addTab(tab); -// } } @Override public void onDestroy(){ super.onDestroy(); - if(getCategoriesRequest!=null) - getCategoriesRequest.cancel(); } @Override protected RecyclerView.Adapter getAdapter(){ - View headerView=new View(getActivity()); - headerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); - - mergeAdapter=new MergeRecyclerAdapter(); - mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView)); - mergeAdapter.addAdapter(adapter=new InstancesAdapter()); - return mergeAdapter; + return adapter=new InstancesAdapter(); } @SuppressLint("ClickableViewAccessibility") @@ -222,7 +173,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple setStatusBarColor(0); topBar=view.findViewById(R.id.top_bar); - list.addOnScrollListener(new ElevationOnScrollListener(null, topBar, buttonBar)); + list.addOnScrollListener(new ElevationOnScrollListener(null, topBar)); + if(buttonBar.getBackground() instanceof LayerDrawable ld){ + ld=(LayerDrawable) ld.mutate(); + buttonBar.setBackground(ld); + Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay); + if(overlay!=null){ + overlay.setAlpha(20); + } + } + buttonBar.setElevation(V.dp(3)); searchEdit=view.findViewById(R.id.search_edit); searchEdit.setOnEditorActionListener(this::onSearchEnterPressed); @@ -572,6 +532,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple filteredData.add(instance); } } + setEmptyText(getString(R.string.no_servers_found, currentSearchQuery)); + }else{ + setEmptyText(""); } }else{ for(CatalogInstance instance:data){ @@ -591,27 +554,29 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple } } } - DiffUtil.calculateDiff(new DiffUtil.Callback(){ - @Override - public int getOldListSize(){ - return prevData.size(); - } + UiUtils.updateRecyclerViewKeepingAbsoluteScrollPosition(list, ()->{ + DiffUtil.calculateDiff(new DiffUtil.Callback(){ + @Override + public int getOldListSize(){ + return prevData.size(); + } - @Override - public int getNewListSize(){ - return filteredData.size(); - } + @Override + public int getNewListSize(){ + return filteredData.size(); + } - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ - return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); - } + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ + return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); + } - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ - return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); - } - }).dispatchUpdatesTo(adapter); + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ + return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); + } + }).dispatchUpdatesTo(adapter); + }); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java index e4ce6401..5f810fd3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java @@ -1,12 +1,11 @@ package org.joinmastodon.android.fragments.onboarding; -import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Rect; -import android.graphics.RectF; import android.os.Build; import android.os.Bundle; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.view.View; import android.view.ViewGroup; @@ -65,7 +64,7 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ protected void updateFilteredList(){ ArrayList prevData=new ArrayList<>(filteredData); filteredData.clear(); - if(currentSearchQuery.length()>0){ + if(!TextUtils.isEmpty(currentSearchQuery)){ boolean foundExactMatch=false; for(CatalogInstance inst:data){ if(inst.normalizedDomain.contains(currentSearchQuery)){ @@ -74,9 +73,16 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ foundExactMatch=true; } } - if(!foundExactMatch) + if(!foundExactMatch && currentSearchQuery.indexOf('.')!=-1) filteredData.add(0, fakeInstance); } + if(filteredData.isEmpty()){ + for(CatalogInstance inst:data){ + if(inst.normalizedDomain.equals("mastodon.social") || inst.normalizedDomain.equals("mastodon.online")){ + filteredData.add(inst); + } + } + } UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals); for(int i=0;i result){ data.clear(); data.addAll(sortInstances(result)); + updateFilteredList(); } @Override @@ -112,6 +119,9 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ Toolbar toolbar=getToolbar(); toolbar.setElevation(0); toolbar.setBackground(null); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ + toolbar.setContentInsetStartWithNavigation(V.dp(80)); + } } @Override 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 26d30ab6..ea008cbf 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 @@ -50,6 +50,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import androidx.annotation.Nullable; @@ -62,6 +63,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout; public class SignupFragment extends ToolbarFragment{ private static final String TAG="SignupFragment"; + private final Pattern emailRegex=Pattern.compile("^[^@]+@[^@]+\\.[^@]{2,}$"); private Instance instance; @@ -97,6 +99,7 @@ public class SignupFragment extends ToolbarFragment{ View view=inflater.inflate(R.layout.fragment_onboarding_signup, container, false); TextView domain=view.findViewById(R.id.domain); + TextView atSign=view.findViewById(R.id.at_sign); displayName=view.findViewById(R.id.display_name); username=view.findViewById(R.id.username); email=view.findViewById(R.id.email); @@ -118,7 +121,7 @@ public class SignupFragment extends ToolbarFragment{ @Override public boolean onPreDraw(){ username.getViewTreeObserver().removeOnPreDrawListener(this); - username.setPadding(username.getPaddingLeft(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom()); + username.setPadding(atSign.getWidth(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom()); return true; } }); @@ -145,6 +148,10 @@ public class SignupFragment extends ToolbarFragment{ reasonExplain.setVisibility(View.GONE); } + password.setOnFocusChangeListener(this::onPasswordFieldFocusChange); + passwordConfirm.setOnFocusChangeListener(this::onPasswordFieldFocusChange); + email.setOnFocusChangeListener(this::onEmailFieldFocusChange); + return view; } @@ -281,34 +288,44 @@ public class SignupFragment extends ToolbarFragment{ .exec(instance.uri, apiToken); } + private CharSequence makeLinkInErrorMessage(String source, LinkSpan.OnLinkClickListener onClick){ + SpannableStringBuilder ssb=new SpannableStringBuilder(); + Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){ + private int spanStart; + @Override + public void head(Node node, int depth){ + if(node instanceof TextNode tn){ + ssb.append(tn.text()); + }else if(node instanceof Element){ + spanStart=ssb.length(); + } + } + + @Override + public void tail(Node node, int depth){ + if(node instanceof Element){ + ssb.setSpan(new LinkSpan("", onClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + }); + return ssb; + } + private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){ return switch(fieldName){ case "email" -> switch(error.error){ case "ERR_BLOCKED" -> { String emailAddr=email.getText().toString(); String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1))); - SpannableStringBuilder ssb=new SpannableStringBuilder(); - Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){ - private int spanStart; - @Override - public void head(Node node, int depth){ - if(node instanceof TextNode tn){ - ssb.append(tn.text()); - }else if(node instanceof Element){ - spanStart=ssb.length(); - } - } - - @Override - public void tail(Node node, int depth){ - if(node instanceof Element){ - ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - }); - yield ssb; + yield makeLinkInErrorMessage(s, this::onGoBackLinkClick); } + case "ERR_INVALID" -> getString(R.string.signup_email_invalid); + case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_email_taken), this::onForgotPasswordLinkClick); + default -> error.description; + }; + case "username" -> switch(error.error){ + case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_username_taken), this::onGoBackLinkClick); default -> error.description; }; default -> error.description; @@ -345,7 +362,9 @@ public class SignupFragment extends ToolbarFragment{ } private void updateButtonState(){ - btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && passwordConfirm.length()>=8 && (!instance.approvalRequired || reason.length()>0)); + btn.setEnabled(username.length()>0 && email.length()>0 && emailRegex.matcher(email.getText()).find() + && password.length()>=8 && passwordConfirm.length()>=8 && password.getText().toString().equals(passwordConfirm.getText().toString()) + && (!instance.approvalRequired || reason.length()>0)); } private void createAppAndGetToken(){ @@ -406,6 +425,24 @@ public class SignupFragment extends ToolbarFragment{ Nav.finish(this); } + private void onForgotPasswordLinkClick(LinkSpan span){ + UiUtils.launchWebBrowser(getActivity(), "https://"+instance.uri+"/auth/password/new"); + } + + private void onPasswordFieldFocusChange(View v, boolean hasFocus){ + if(hasFocus || password.length()==0 || passwordConfirm.length()==0) + return; + if(!password.getText().toString().equals(passwordConfirm.getText().toString())){ + passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match)); + } + } + + private void onEmailFieldFocusChange(View v, boolean hasFocus){ + if(!hasFocus && email.length()>0 && !emailRegex.matcher(email.getText()).find()){ + emailWrap.setErrorState(getString(R.string.signup_email_invalid)); + } + } + private class ErrorClearingListener implements TextWatcher{ public final EditText editText; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index a9c4cb57..ce7e6f44 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -1040,4 +1040,20 @@ public class UiUtils{ button.setTextColor(origColor); } } + + public static void updateRecyclerViewKeepingAbsoluteScrollPosition(RecyclerView rv, Runnable onUpdate){ + int topItem=-1; + int topItemOffset=0; + if(rv.getChildCount()>0){ + View item=rv.getChildAt(0); + topItem=rv.getChildAdapterPosition(item); + topItemOffset=item.getTop(); + } + onUpdate.run(); + int newCount=rv.getAdapter().getItemCount(); + if(newCount>=topItem){ + rv.scrollToPosition(topItem); + rv.scrollBy(0, -topItemOffset); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java index cec3f1af..8d230ce1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java @@ -158,6 +158,9 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie }else{ transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f); } + int labelX=label.getLeft(); + int editX=edit.getLeft()+edit.getPaddingLeft(); + float xOffset=editX-labelX; AnimatorSet anim=new AnimatorSet(); if(hintVisible){ @@ -166,6 +169,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie ObjectAnimator.ofFloat(label, SCALE_X, scale), ObjectAnimator.ofFloat(label, SCALE_Y, scale), ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY), + ObjectAnimator.ofFloat(label, TRANSLATION_X, xOffset), ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 0f) ); edit.setHintTextColor(0); @@ -173,11 +177,13 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie label.setScaleX(scale); label.setScaleY(scale); label.setTranslationY(transY); + label.setTranslationX(xOffset); anim.playTogether( ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY), ObjectAnimator.ofFloat(label, SCALE_X, 1f), ObjectAnimator.ofFloat(label, SCALE_Y, 1f), ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f), + ObjectAnimator.ofFloat(label, TRANSLATION_X, 0f), ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 1f) ); } diff --git a/mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml b/mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml new file mode 100644 index 00000000..f95ac660 --- /dev/null +++ b/mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/alert_invite_link.xml b/mastodon/src/main/res/layout/alert_invite_link.xml index a188985b..8b1f9d43 100644 --- a/mastodon/src/main/res/layout/alert_invite_link.xml +++ b/mastodon/src/main/res/layout/alert_invite_link.xml @@ -39,14 +39,15 @@ android:layout_height="56dp" android:background="@drawable/bg_m3_filled_text_field" android:paddingHorizontal="16dp" - android:textColorHint="?colorM3OnSurfaceVariant" + android:textColorHint="@color/m3_on_surface_variant_alpha40" android:textColor="?colorM3OnSurface" android:gravity="start|bottom" android:paddingBottom="8dp" android:singleLine="true" android:inputType="textUri" android:textAppearance="@style/m3_body_large" - android:hint="example.social/invite/AbC123"/> + android:hint="example.social/invite/AbC123" + tools:ignore="HardcodedText" /> + android:foreground="@drawable/bg_m3_outlined_text_field" + tools:ignore="RtlHardcoded"> + + - - - @@ -38,7 +38,7 @@ android:textColorHint="?colorM3OnSurfaceVariant" android:inputType="textUri" android:importantForAutofill="no" - android:paddingStart="48dp" + android:paddingStart="64dp" android:layout_marginEnd="52dp" android:drawablePadding="16dp" android:textAppearance="@style/m3_body_large" @@ -47,6 +47,7 @@ Email Password Confirm password - Include capital letters, special characters, and numbers to increase your password strength. General Check your inbox @@ -321,9 +320,9 @@ Log in with the server where you created your account. Server URL Any language - Instant sign-up + Instant sign up Manual review - Any sign-up speed + Any sign up speed Europe North America South America @@ -759,4 +758,8 @@ The request timed out. Check your connection and try again? Something went wrong talking with your server. It’s probably not your fault. Try again? It could’ve been deleted, or maybe it never existed at all. + No servers found for “%s” + This username is taken. Try a different one or <a>pick a different server</a>. + That doesn’t look like a valid email address. + Email address is already in use. Did you <a>forget your password</a>? \ No newline at end of file