Support for invite links (AND-90)

This commit is contained in:
Grishka
2024-01-03 23:51:35 +03:00
parent 5d7c37262e
commit 48f9aabaf7
19 changed files with 559 additions and 58 deletions

View File

@@ -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<CheckInviteLink.Response>{
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;
}
}

View File

@@ -4,22 +4,23 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Token;
public class RegisterAccount extends MastodonAPIRequest<Token>{
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;
}
}
}

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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<CatalogInsta
protected EditText searchEdit;
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
protected String currentSearchQuery;
protected String currentSearchQueryButWithCasePreserved;
protected String loadingInstanceDomain;
protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar;
@@ -91,6 +91,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList();
searchEdit.removeCallbacks(searchDebouncer);
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
@@ -105,6 +106,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList();
loadInstanceInfo(currentSearchQuery, false);
}
@@ -149,6 +151,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
loadInstanceInfo(_domain, isFromRedirect, null);
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
if(TextUtils.isEmpty(_domain))
return;
String domain=normalizeInstanceDomain(_domain);
@@ -173,7 +179,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
try{
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
}catch(URISyntaxException x){
showInstanceInfoLoadError(domain, x);
if(onError!=null)
onError.accept(x);
else
showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -193,10 +202,11 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result);
if(instanceProgressDialog!=null || onError!=null)
proceedWithAuthOrSignup(result);
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
}
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false;
@@ -223,11 +233,14 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
public void onError(ErrorResponse error){
loadingInstanceRequest=null;
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
fetchDomainFromHostMetaAndMaybeRetry(domain, error);
fetchDomainFromHostMetaAndMaybeRetry(domain, error, onError);
return;
}
loadingInstanceDomain=null;
showInstanceInfoLoadError(domain, error);
if(onError!=null)
onError.accept(error);
else
showInstanceInfoLoadError(domain, error);
if(fakeInstance!=null && getActivity()!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -276,7 +289,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
}
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError, Consumer<Object> onError){
String url="https://"+domain+"/.well-known/host-meta";
Request req=new Request.Builder()
.url(url)
@@ -290,7 +303,12 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
Activity a=getActivity();
if(a==null)
return;
a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(e);
else
showInstanceInfoLoadError(domain, e);
});
}
@Override
@@ -302,7 +320,13 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
return;
try(response){
if(!response.isSuccessful()){
a.runOnUiThread(()->showInstanceInfoLoadError(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 BaseRecyclerFragment<CatalogInsta
}
}
}
a.runOnUiThread(()->showInstanceInfoLoadError(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);
});
}
}
});

View File

@@ -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<CatalogInstance> 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<String> 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);
}
}

View File

@@ -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);
}

View File

@@ -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){

View File

@@ -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(){}

View File

@@ -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);
}
}
}