Support for invite links (AND-90)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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(){}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user