diff --git a/mastodon/build.gradle b/mastodon/build.gradle index c37669da..93999e02 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -9,7 +9,7 @@ android { applicationId "org.joinmastodon.android" minSdk 23 targetSdk 33 - versionCode 81 + versionCode 82 versionName "2.2.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW" diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/CheckInviteLink.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/CheckInviteLink.java new file mode 100644 index 00000000..13620054 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/CheckInviteLink.java @@ -0,0 +1,22 @@ +package org.joinmastodon.android.api.requests.accounts; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.api.RequiredField; +import org.joinmastodon.android.model.BaseModel; + +public class CheckInviteLink extends MastodonAPIRequest{ + public CheckInviteLink(String path){ + super(HttpMethod.GET, path, Response.class); + addHeader("Accept", "application/json"); + } + + @Override + protected String getPathPrefix(){ + return ""; + } + + public static class Response extends BaseModel{ + @RequiredField + public String inviteCode; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/RegisterAccount.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/RegisterAccount.java index 62214523..df7915bb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/RegisterAccount.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/RegisterAccount.java @@ -4,22 +4,23 @@ import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Token; public class RegisterAccount extends MastodonAPIRequest{ - public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){ + public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone, String inviteCode){ super(HttpMethod.POST, "/accounts", Token.class); - setRequestBody(new Body(username, email, password, locale, reason, timezone)); + setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode)); } private static class Body{ - public String username, email, password, locale, reason, timeZone; + public String username, email, password, locale, reason, timeZone, inviteCode; public boolean agreement=true; - public Body(String username, String email, String password, String locale, String reason, String timeZone){ + public Body(String username, String email, String password, String locale, String reason, String timeZone, String inviteCode){ this.username=username; this.email=email; this.password=password; this.locale=locale; this.reason=reason; this.timeZone=timeZone; + this.inviteCode=inviteCode; } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/SplashFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/SplashFragment.java index ae1f7354..d1326c58 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/SplashFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/SplashFragment.java @@ -1,7 +1,12 @@ package org.joinmastodon.android.fragments; +import android.app.ProgressDialog; +import android.content.ClipData; +import android.content.ClipboardManager; import android.graphics.drawable.ColorDrawable; +import android.net.Uri; import android.os.Bundle; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -11,6 +16,8 @@ import android.widget.ProgressBar; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.MastodonErrorResponse; +import org.joinmastodon.android.api.requests.accounts.CheckInviteLink; import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances; import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment; @@ -20,6 +27,7 @@ import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.catalog.CatalogDefaultInstance; import org.joinmastodon.android.ui.InterpolatingMotionEffect; import org.joinmastodon.android.ui.M3AlertDialogBuilder; +import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.ProgressBarButton; import org.joinmastodon.android.ui.views.SizeListenerFrameLayout; @@ -48,6 +56,9 @@ public class SplashFragment extends AppKitFragment{ private ProgressBar defaultServerProgress; private String chosenDefaultServer=DEFAULT_SERVER; private boolean loadingDefaultServer, loadedDefaultServer; + private Uri currentInviteLink; + private ProgressDialog instanceLoadingProgress; + private String inviteCode; @Override public void onCreate(Bundle savedInstanceState){ @@ -110,19 +121,65 @@ public class SplashFragment extends AppKitFragment{ Bundle extras=new Bundle(); boolean isSignup=v.getId()==R.id.btn_get_started; extras.putBoolean("signup", isSignup); + extras.putString("defaultServer", chosenDefaultServer); Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras); } private void onJoinDefaultServerClick(View v){ if(loadingDefaultServer) return; + instanceLoadingProgress=new ProgressDialog(getActivity()); + instanceLoadingProgress.setCancelable(false); + instanceLoadingProgress.setMessage(getString(R.string.loading_instance)); + instanceLoadingProgress.show(); + if(currentInviteLink!=null){ + new CheckInviteLink(currentInviteLink.getPath()) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(CheckInviteLink.Response result){ + inviteCode=result.inviteCode; + proceedWithServerDomain(currentInviteLink.getHost()); + } + + @Override + public void onError(ErrorResponse error){ + if(getActivity()==null) + return; + instanceLoadingProgress.dismiss(); + instanceLoadingProgress=null; + if(error instanceof MastodonErrorResponse mer){ + switch(mer.httpStatus){ + case 401 -> new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.expired_invite_link) + .setMessage(getString(R.string.expired_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer)) + .setPositiveButton(R.string.ok, null) + .show(); + case 404 -> new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.invalid_invite_link) + .setMessage(getString(R.string.invalid_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer)) + .setPositiveButton(R.string.ok, null) + .show(); + default -> error.showToast(getActivity()); + } + } + } + }) + .execNoAuth(currentInviteLink.getHost()); + return; + } + proceedWithServerDomain(chosenDefaultServer); + } + + private void proceedWithServerDomain(String domain){ new GetInstance() .setCallback(new Callback<>(){ @Override public void onSuccess(Instance result){ if(getActivity()==null) return; - if(!result.registrations){ + instanceLoadingProgress.dismiss(); + instanceLoadingProgress=null; + if(!result.registrations && TextUtils.isEmpty(inviteCode)){ new M3AlertDialogBuilder(getActivity()) .setTitle(R.string.error) .setMessage(R.string.instance_signup_closed) @@ -132,6 +189,8 @@ public class SplashFragment extends AppKitFragment{ } Bundle args=new Bundle(); args.putParcelable("instance", Parcels.wrap(result)); + if(inviteCode!=null) + args.putString("inviteCode", inviteCode); Nav.go(getActivity(), InstanceRulesFragment.class, args); } @@ -139,11 +198,12 @@ public class SplashFragment extends AppKitFragment{ public void onError(ErrorResponse error){ if(getActivity()==null) return; + instanceLoadingProgress.dismiss(); + instanceLoadingProgress=null; error.showToast(getActivity()); } }) - .wrapProgress(getActivity(), R.string.loading_instance, true) - .execNoAuth(chosenDefaultServer); + .execNoAuth(domain); } private void onLearnMoreClick(View v){ @@ -198,9 +258,18 @@ public class SplashFragment extends AppKitFragment{ } private void loadAndChooseDefaultServer(){ - loadingDefaultServer=true; - defaultServerButton.setTextVisible(false); - defaultServerProgress.setVisibility(View.VISIBLE); + ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip(); + if(clipData!=null && clipData.getItemCount()>0){ + CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity()); + if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){ + currentInviteLink=Uri.parse(clipText.toString()); + defaultServerButton.setText(getString(R.string.join_server_x_with_invite, currentInviteLink.getHost())); + } + }else{ + loadingDefaultServer=true; + defaultServerButton.setTextVisible(false); + defaultServerProgress.setVisibility(View.VISIBLE); + } new GetCatalogDefaultInstances() .setCallback(new Callback<>(){ @Override @@ -243,7 +312,7 @@ public class SplashFragment extends AppKitFragment{ chosenDefaultServer=domain; loadingDefaultServer=false; loadedDefaultServer=true; - if(defaultServerButton!=null && getActivity()!=null){ + if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){ defaultServerButton.setTextVisible(true); defaultServerProgress.setVisibility(View.GONE); defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer)); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java index 05746a5a..03d0c355 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java @@ -137,6 +137,9 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{ protected void onButtonClick(){ Bundle args=new Bundle(); args.putParcelable("instance", Parcels.wrap(instance)); + if(getArguments().containsKey("inviteCode")){ + args.putString("inviteCode", getArguments().getString("inviteCode")); + } Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this); } 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 af750de7..22790395 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 @@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding; import android.app.Activity; import android.app.ProgressDialog; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.view.KeyEvent; @@ -37,6 +36,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.xml.parsers.DocumentBuilderFactory; @@ -48,7 +48,6 @@ import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.MergeRecyclerAdapter; -import me.grishka.appkit.utils.V; import okhttp3.Call; import okhttp3.Request; import okhttp3.Response; @@ -61,6 +60,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment instancesCache=new HashMap<>(); protected View buttonBar; @@ -91,6 +91,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment onError){ if(TextUtils.isEmpty(_domain)) return; String domain=normalizeInstanceDomain(_domain); @@ -173,7 +179,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment0 && filteredData.get(0)==fakeInstance){ @@ -193,10 +202,11 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment0 && filteredData.get(0)==fakeInstance){ @@ -276,7 +289,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment onError){ String url="https://"+domain+"/.well-known/host-meta"; Request req=new Request.Builder() .url(url) @@ -290,7 +303,12 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragmentshowInstanceInfoLoadError(domain, e)); + a.runOnUiThread(()->{ + if(onError!=null) + onError.accept(e); + else + showInstanceInfoLoadError(domain, e); + }); } @Override @@ -302,7 +320,13 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragmentshowInstanceInfoLoadError(domain, response.code()+" "+response.message())); + a.runOnUiThread(()->{ + String err=response.code()+" "+response.message(); + if(onError!=null) + onError.accept(err); + else + showInstanceInfoLoadError(domain, err); + }); return; } InputSource source=new InputSource(response.body().charStream()); @@ -321,9 +345,19 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragmentshowInstanceInfoLoadError(domain, origError)); + a.runOnUiThread(()->{ + if(onError!=null) + onError.accept(origError); + else + showInstanceInfoLoadError(domain, origError); + }); }catch(Exception x){ - a.runOnUiThread(()->showInstanceInfoLoadError(domain, x)); + a.runOnUiThread(()->{ + if(onError!=null) + onError.accept(x); + else + showInstanceInfoLoadError(domain, x); + }); } } }); 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 7e043708..68decec3 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 @@ -1,8 +1,13 @@ package org.joinmastodon.android.fragments.onboarding; import android.annotation.SuppressLint; +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.net.Uri; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; @@ -12,6 +17,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; import android.widget.HorizontalScrollView; import android.widget.ImageButton; import android.widget.LinearLayout; @@ -19,9 +26,12 @@ 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; @@ -29,6 +39,8 @@ 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.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.FilterChipView; import org.joinmastodon.android.utils.ElevationOnScrollListener; @@ -40,7 +52,9 @@ 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; import androidx.annotation.NonNull; @@ -77,6 +91,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple private CatalogInstance.Region chosenRegion; private CategoryChoice categoryChoice=CategoryChoice.GENERAL; + private String inviteCode, inviteCodeHost; + private AlertDialog currentInviteLinkAlert; + public InstanceCatalogSignupFragment(){ super(R.layout.fragment_onboarding_common, 10); } @@ -317,7 +334,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple focusThing=view.findViewById(R.id.focus_thing); focusThing.requestFocus(); - view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick); + view.findViewById(R.id.btn_use_invite).setOnClickListener(this::onUseInviteClick); nextButton.setEnabled(chosenInstance!=null); } @@ -351,34 +368,191 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple @Override protected void proceedWithAuthOrSignup(Instance instance){ + if(currentInviteLinkAlert!=null){ + currentInviteLinkAlert.dismiss(); + }else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.INVITE_LINK_PATTERN.matcher(currentSearchQueryButWithCasePreserved).find()){ + if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){ + Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved); + new CheckInviteLink(inviteLink.getPath()) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(CheckInviteLink.Response result){ + inviteCodeHost=inviteLink.getHost(); + inviteCode=result.inviteCode; + proceedWithAuthOrSignup(instance); + } + + @Override + public void onError(ErrorResponse error){ + if(getActivity()==null) + return; + if(error instanceof MastodonErrorResponse mer){ + switch(mer.httpStatus){ + case 401 -> new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.expired_invite_link) + .setMessage(getString(R.string.expired_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer"))) + .setPositiveButton(R.string.ok, null) + .show(); + case 404 -> new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.invalid_invite_link) + .setMessage(getString(R.string.invalid_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer"))) + .setPositiveButton(R.string.ok, null) + .show(); + default -> error.showToast(getActivity()); + } + } + } + }) + .wrapProgress(getActivity(), R.string.loading_instance, true) + .execNoAuth(inviteLink.getHost()); + return; + } + } getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0); - if(!instance.registrations){ - new M3AlertDialogBuilder(getActivity()) - .setTitle(R.string.error) - .setMessage(R.string.instance_signup_closed) - .setPositiveButton(R.string.ok, null) - .show(); + if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){ + if(instance.invitesEnabled){ + showInviteLinkAlert(instance.uri); + }else{ + new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.error) + .setMessage(R.string.instance_signup_closed) + .setPositiveButton(R.string.ok, null) + .show(); + } return; } Bundle args=new Bundle(); args.putParcelable("instance", Parcels.wrap(instance)); + if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.uri, inviteCodeHost)) + args.putString("inviteCode", inviteCode); Nav.go(getActivity(), InstanceRulesFragment.class, args); } - private void onPickRandomInstanceClick(View v){ - String lang=Locale.getDefault().getLanguage(); - List instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList()); - if(instances.isEmpty()){ - instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList()); + private void onUseInviteClick(View v){ + showInviteLinkAlert(null); + } + + private void showInviteLinkAlert(String domain){ + AlertDialog alert=new M3AlertDialogBuilder(getActivity()) + .setView(R.layout.alert_invite_link) + .setPositiveButton(R.string.next, null) + .setNegativeButton(R.string.cancel, null) + .create(); + + Button next=alert.getButton(AlertDialog.BUTTON_POSITIVE); + EditText edit=alert.findViewById(R.id.edit); + TextView supportingText=alert.findViewById(R.id.supporting_text); + TextView label=alert.findViewById(R.id.label); + TextView subtitle=alert.findViewById(R.id.subtitle); + ImageButton clear=alert.findViewById(R.id.clear); + clear.setVisibility(View.GONE); + + if(TextUtils.isEmpty(domain)){ + subtitle.setVisibility(View.GONE); + }else{ + subtitle.setText(getString(R.string.need_invite_to_join_server, domain)); } - if(instances.isEmpty()){ - instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList()); + + Consumer errorSetter=err->{ + supportingText.setText(err); + int errorColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error); + supportingText.setTextColor(errorColor); + label.setTextColor(errorColor); + edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field_error); + }; + + next.setOnClickListener(_v->{ + Uri inviteLink=Uri.parse(edit.getText().toString()); + if(TextUtils.isEmpty(inviteLink.getHost()) || TextUtils.isEmpty(inviteLink.getPath())){ + errorSetter.accept(getString(R.string.this_invite_is_invalid)); + return; + } + UiUtils.showProgressForAlertButton(next, true); + new CheckInviteLink(inviteLink.getPath()) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(CheckInviteLink.Response result){ + if(getActivity()==null || !alert.isShowing()) + return; + + String host=inviteLink.getHost(); + inviteCode=result.inviteCode; + inviteCodeHost=host; + + Instance instance=instancesCache.get(normalizeInstanceDomain(host)); + if(instance==null){ + loadInstanceInfo(host, false, err->{ + String errorStr; + if(err instanceof String str){ + errorStr=str; + }else if(err instanceof Throwable x){ + errorStr=x.getMessage(); + }else if(err instanceof MastodonErrorResponse mer){ + errorStr=mer.error; + }else{ + errorStr=getString(R.string.error); + } + errorSetter.accept(errorStr); + UiUtils.showProgressForAlertButton(next, false); + }); + }else{ + proceedWithAuthOrSignup(instance); + } + } + + @Override + public void onError(ErrorResponse error){ + if(getActivity()==null || !alert.isShowing()) + return; + UiUtils.showProgressForAlertButton(next, false); + if(error instanceof MastodonErrorResponse mer){ + errorSetter.accept(switch(mer.httpStatus){ + case 404 -> getString(R.string.this_invite_is_invalid); + case 401 -> getString(R.string.this_invite_has_expired); + default -> mer.error; + }); + } + } + }) + .execNoAuth(inviteLink.getHost()); + }); + next.setEnabled(false); + edit.addTextChangedListener(new SimpleTextWatcher(e->{ + boolean wasEmpty=!next.isEnabled(); + next.setEnabled(e.length()>0); + if(supportingText.length()>0){ + supportingText.setText(""); + int regularColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant); + supportingText.setTextColor(regularColor); + label.setTextColor(regularColor); + edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field); + } + if(wasEmpty!=(e.length()==0)){ + int padEnd; + if(e.length()==0){ + clear.setVisibility(View.GONE); + padEnd=V.dp(16); + }else{ + clear.setVisibility(View.VISIBLE); + padEnd=V.dp(48); + } + edit.setPaddingRelative(edit.getPaddingStart(), edit.getPaddingTop(), padEnd, edit.getPaddingBottom()); + } + })); + clear.setOnClickListener(_v->edit.setText("")); + + ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip(); + if(clipData!=null && clipData.getItemCount()>0){ + CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity()); + if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){ + edit.setText(clipText); + supportingText.setText(R.string.invite_link_pasted); + } } - if(instances.isEmpty()){ - return; - } - chosenInstance=instances.get(new Random().nextInt(instances.size())); - onNextClick(v); + + currentInviteLinkAlert=alert; + alert.setOnDismissListener(dialog->currentInviteLinkAlert=null); + alert.show(); } @Override @@ -387,8 +561,14 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple filteredData.clear(); if(searchQueryMode){ if(!TextUtils.isEmpty(currentSearchQuery)){ + String actualQuery; + if(currentSearchQuery.startsWith("https:")){ + actualQuery=Uri.parse(currentSearchQuery).getHost(); + }else{ + actualQuery=currentSearchQuery; + } for(CatalogInstance instance:data){ - if(instance.domain.contains(currentSearchQuery)){ + if(instance.domain.contains(actualQuery)){ filteredData.add(instance); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceRulesFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceRulesFragment.java index 73739ba9..7ccc8e09 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceRulesFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceRulesFragment.java @@ -91,6 +91,9 @@ public class InstanceRulesFragment extends ToolbarFragment{ protected void onButtonClick(){ Bundle args=new Bundle(); args.putParcelable("instance", Parcels.wrap(instance)); + if(getArguments().containsKey("inviteCode")){ + args.putString("inviteCode", getArguments().getString("inviteCode")); + } Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this); } 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 68dbfeb5..26d30ab6 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 @@ -219,7 +219,9 @@ public class SignupFragment extends ToolbarFragment{ if(!serverSupportedTimezones.contains(timezone)) timezone=null; - new RegisterAccount(username, email, password.getText().toString(), locale, reason.getText().toString(), timezone) + String inviteCode=getArguments().getString("inviteCode"); + + new RegisterAccount(username, email, password.getText().toString(), locale, reason.getText().toString(), timezone, inviteCode) .setCallback(new Callback<>(){ @Override public void onSuccess(Token result){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java index 6f4c2b81..e733ab7e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java @@ -52,6 +52,7 @@ public class HtmlParser{ ")" + ")"; public static final Pattern URL_PATTERN=Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE); + public static final Pattern INVITE_LINK_PATTERN=Pattern.compile("^https://"+Regex.URL_VALID_DOMAIN+"/invite/[a-z\\d]+$", Pattern.CASE_INSENSITIVE); private static Pattern EMOJI_CODE_PATTERN=Pattern.compile(":([\\w]+):"); private HtmlParser(){} 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 b946e64b..0a87fb2d 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 @@ -11,9 +11,11 @@ import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.drawable.Animatable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; +import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -32,6 +34,8 @@ import android.transition.ChangeScroll; import android.transition.Fade; import android.transition.TransitionManager; import android.transition.TransitionSet; +import android.view.Gravity; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -39,7 +43,9 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.webkit.MimeTypeMap; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.PopupMenu; +import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import android.widget.Toolbar; @@ -882,4 +888,31 @@ public class UiUtils{ } return msg; } + + public static void showProgressForAlertButton(Button button, boolean show){ + boolean shown=button.getTag(R.id.button_progress_orig_color)!=null; + if(shown==show) + return; + button.setEnabled(!show); + if(show){ + ColorStateList origColor=button.getTextColors(); + button.setTag(R.id.button_progress_orig_color, origColor); + button.setTextColor(0); + ProgressBar progressBar=(ProgressBar) LayoutInflater.from(button.getContext()).inflate(R.layout.progress_bar, null); + Drawable progress=progressBar.getIndeterminateDrawable().mutate(); + progress.setTint(getThemeColor(button.getContext(), R.attr.colorM3OnSurface) & 0x60ffffff); + if(progress instanceof Animatable a) + a.start(); + LayerDrawable layerList=new LayerDrawable(new Drawable[]{progress}); + layerList.setLayerGravity(0, Gravity.CENTER); + layerList.setLayerSize(0, V.dp(24), V.dp(24)); + layerList.setBounds(0, 0, button.getWidth(), button.getHeight()); + button.getOverlay().add(layerList); + }else{ + button.getOverlay().clear(); + ColorStateList origColor=(ColorStateList) button.getTag(R.id.button_progress_orig_color); + button.setTag(R.id.button_progress_orig_color, null); + button.setTextColor(origColor); + } + } } diff --git a/mastodon/src/main/res/drawable/bg_m3_filled_text_field.xml b/mastodon/src/main/res/drawable/bg_m3_filled_text_field.xml new file mode 100644 index 00000000..a2159e9a --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_m3_filled_text_field.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_m3_filled_text_field_error.xml b/mastodon/src/main/res/drawable/bg_m3_filled_text_field_error.xml new file mode 100644 index 00000000..6c18cd7d --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_m3_filled_text_field_error.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_confirmation_number_24px.xml b/mastodon/src/main/res/drawable/ic_confirmation_number_24px.xml new file mode 100644 index 00000000..d5c4b03f --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_confirmation_number_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/layout/alert_invite_link.xml b/mastodon/src/main/res/layout/alert_invite_link.xml new file mode 100644 index 00000000..a188985b --- /dev/null +++ b/mastodon/src/main/res/layout/alert_invite_link.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/fragment_onboarding_common.xml b/mastodon/src/main/res/layout/fragment_onboarding_common.xml index 4cad26f5..46cd3970 100644 --- a/mastodon/src/main/res/layout/fragment_onboarding_common.xml +++ b/mastodon/src/main/res/layout/fragment_onboarding_common.xml @@ -125,25 +125,15 @@ android:orientation="vertical" android:background="@drawable/bg_onboarding_panel"> - -