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

@@ -9,7 +9,7 @@ android {
applicationId "org.joinmastodon.android" applicationId "org.joinmastodon.android"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 81 versionCode 82
versionName "2.2.4" versionName "2.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 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" 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"

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; import org.joinmastodon.android.model.Token;
public class RegisterAccount extends MastodonAPIRequest<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); 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{ 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 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.username=username;
this.email=email; this.email=email;
this.password=password; this.password=password;
this.locale=locale; this.locale=locale;
this.reason=reason; this.reason=reason;
this.timeZone=timeZone; this.timeZone=timeZone;
this.inviteCode=inviteCode;
} }
} }
} }

View File

@@ -1,7 +1,12 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -11,6 +16,8 @@ import android.widget.ProgressBar;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; 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.catalog.GetCatalogDefaultInstances;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment; 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.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect; import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; 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.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton; import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout; import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
@@ -48,6 +56,9 @@ public class SplashFragment extends AppKitFragment{
private ProgressBar defaultServerProgress; private ProgressBar defaultServerProgress;
private String chosenDefaultServer=DEFAULT_SERVER; private String chosenDefaultServer=DEFAULT_SERVER;
private boolean loadingDefaultServer, loadedDefaultServer; private boolean loadingDefaultServer, loadedDefaultServer;
private Uri currentInviteLink;
private ProgressDialog instanceLoadingProgress;
private String inviteCode;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@@ -110,19 +121,65 @@ public class SplashFragment extends AppKitFragment{
Bundle extras=new Bundle(); Bundle extras=new Bundle();
boolean isSignup=v.getId()==R.id.btn_get_started; boolean isSignup=v.getId()==R.id.btn_get_started;
extras.putBoolean("signup", isSignup); extras.putBoolean("signup", isSignup);
extras.putString("defaultServer", chosenDefaultServer);
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras); Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
} }
private void onJoinDefaultServerClick(View v){ private void onJoinDefaultServerClick(View v){
if(loadingDefaultServer) if(loadingDefaultServer)
return; 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() new GetInstance()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Instance result){ public void onSuccess(Instance result){
if(getActivity()==null) if(getActivity()==null)
return; return;
if(!result.registrations){ instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed) .setMessage(R.string.instance_signup_closed)
@@ -132,6 +189,8 @@ public class SplashFragment extends AppKitFragment{
} }
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(result)); args.putParcelable("instance", Parcels.wrap(result));
if(inviteCode!=null)
args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args); Nav.go(getActivity(), InstanceRulesFragment.class, args);
} }
@@ -139,11 +198,12 @@ public class SplashFragment extends AppKitFragment{
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
if(getActivity()==null) if(getActivity()==null)
return; return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
error.showToast(getActivity()); error.showToast(getActivity());
} }
}) })
.wrapProgress(getActivity(), R.string.loading_instance, true) .execNoAuth(domain);
.execNoAuth(chosenDefaultServer);
} }
private void onLearnMoreClick(View v){ private void onLearnMoreClick(View v){
@@ -198,9 +258,18 @@ public class SplashFragment extends AppKitFragment{
} }
private void loadAndChooseDefaultServer(){ private void loadAndChooseDefaultServer(){
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; loadingDefaultServer=true;
defaultServerButton.setTextVisible(false); defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE); defaultServerProgress.setVisibility(View.VISIBLE);
}
new GetCatalogDefaultInstances() new GetCatalogDefaultInstances()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
@@ -243,7 +312,7 @@ public class SplashFragment extends AppKitFragment{
chosenDefaultServer=domain; chosenDefaultServer=domain;
loadingDefaultServer=false; loadingDefaultServer=false;
loadedDefaultServer=true; loadedDefaultServer=true;
if(defaultServerButton!=null && getActivity()!=null){ if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
defaultServerButton.setTextVisible(true); defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE); defaultServerProgress.setVisibility(View.GONE);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer)); defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));

View File

@@ -137,6 +137,9 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
protected void onButtonClick(){ protected void onButtonClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance)); 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); 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.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -37,6 +36,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory; 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.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@@ -61,6 +60,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected EditText searchEdit; protected EditText searchEdit;
protected Runnable searchDebouncer=this::onSearchChangedDebounced; protected Runnable searchDebouncer=this::onSearchChangedDebounced;
protected String currentSearchQuery; protected String currentSearchQuery;
protected String currentSearchQueryButWithCasePreserved;
protected String loadingInstanceDomain; protected String loadingInstanceDomain;
protected HashMap<String, Instance> instancesCache=new HashMap<>(); protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar; protected View buttonBar;
@@ -91,6 +91,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN) if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true; return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim(); currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList(); updateFilteredList();
searchEdit.removeCallbacks(searchDebouncer); searchEdit.removeCallbacks(searchDebouncer);
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery)); Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
@@ -105,6 +106,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected void onSearchChangedDebounced(){ protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim(); currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList(); updateFilteredList();
loadInstanceInfo(currentSearchQuery, false); loadInstanceInfo(currentSearchQuery, false);
} }
@@ -149,6 +151,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
} }
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){ 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)) if(TextUtils.isEmpty(_domain))
return; return;
String domain=normalizeInstanceDomain(_domain); String domain=normalizeInstanceDomain(_domain);
@@ -173,6 +179,9 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
try{ try{
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
}catch(URISyntaxException x){ }catch(URISyntaxException x){
if(onError!=null)
onError.accept(x);
else
showInstanceInfoLoadError(domain, x); showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){ if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error); fakeInstance.description=getString(R.string.error);
@@ -193,10 +202,11 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
loadingInstanceDomain=null; loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result); instancesCache.put(domain, result);
if(instanceProgressDialog!=null || onError!=null)
proceedWithAuthOrSignup(result);
if(instanceProgressDialog!=null){ if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss(); instanceProgressDialog.dismiss();
instanceProgressDialog=null; instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
} }
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){ if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false; boolean found=false;
@@ -223,10 +233,13 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
loadingInstanceRequest=null; loadingInstanceRequest=null;
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){ if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
fetchDomainFromHostMetaAndMaybeRetry(domain, error); fetchDomainFromHostMetaAndMaybeRetry(domain, error, onError);
return; return;
} }
loadingInstanceDomain=null; loadingInstanceDomain=null;
if(onError!=null)
onError.accept(error);
else
showInstanceInfoLoadError(domain, error); showInstanceInfoLoadError(domain, error);
if(fakeInstance!=null && getActivity()!=null){ if(fakeInstance!=null && getActivity()!=null){
fakeInstance.description=getString(R.string.error); fakeInstance.description=getString(R.string.error);
@@ -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"; String url="https://"+domain+"/.well-known/host-meta";
Request req=new Request.Builder() Request req=new Request.Builder()
.url(url) .url(url)
@@ -290,7 +303,12 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
Activity a=getActivity(); Activity a=getActivity();
if(a==null) if(a==null)
return; return;
a.runOnUiThread(()->showInstanceInfoLoadError(domain, e)); a.runOnUiThread(()->{
if(onError!=null)
onError.accept(e);
else
showInstanceInfoLoadError(domain, e);
});
} }
@Override @Override
@@ -302,7 +320,13 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
return; return;
try(response){ try(response){
if(!response.isSuccessful()){ 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; return;
} }
InputSource source=new InputSource(response.body().charStream()); 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){ }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; package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
@@ -12,6 +17,8 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.HorizontalScrollView; import android.widget.HorizontalScrollView;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@@ -19,9 +26,12 @@ import android.widget.PopupMenu;
import android.widget.RadioButton; import android.widget.RadioButton;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest; 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.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances; import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.model.Instance; 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.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; 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.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView; import org.joinmastodon.android.ui.views.FilterChipView;
import org.joinmastodon.android.utils.ElevationOnScrollListener; import org.joinmastodon.android.utils.ElevationOnScrollListener;
@@ -40,7 +52,9 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.Random; import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -77,6 +91,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
private CatalogInstance.Region chosenRegion; private CatalogInstance.Region chosenRegion;
private CategoryChoice categoryChoice=CategoryChoice.GENERAL; private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
private String inviteCode, inviteCodeHost;
private AlertDialog currentInviteLinkAlert;
public InstanceCatalogSignupFragment(){ public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10); 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=view.findViewById(R.id.focus_thing);
focusThing.requestFocus(); 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); nextButton.setEnabled(chosenInstance!=null);
} }
@@ -351,34 +368,191 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
@Override @Override
protected void proceedWithAuthOrSignup(Instance instance){ 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); getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(!instance.registrations){ if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){
if(instance.invitesEnabled){
showInviteLinkAlert(instance.uri);
}else{
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed) .setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show(); .show();
}
return; return;
} }
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance)); 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); Nav.go(getActivity(), InstanceRulesFragment.class, args);
} }
private void onPickRandomInstanceClick(View v){ private void onUseInviteClick(View v){
String lang=Locale.getDefault().getLanguage(); showInviteLinkAlert(null);
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());
} }
if(instances.isEmpty()){
instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList()); 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()){
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; return;
} }
chosenInstance=instances.get(new Random().nextInt(instances.size())); UiUtils.showProgressForAlertButton(next, true);
onNextClick(v); 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);
}
}
currentInviteLinkAlert=alert;
alert.setOnDismissListener(dialog->currentInviteLinkAlert=null);
alert.show();
} }
@Override @Override
@@ -387,8 +561,14 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
filteredData.clear(); filteredData.clear();
if(searchQueryMode){ if(searchQueryMode){
if(!TextUtils.isEmpty(currentSearchQuery)){ if(!TextUtils.isEmpty(currentSearchQuery)){
String actualQuery;
if(currentSearchQuery.startsWith("https:")){
actualQuery=Uri.parse(currentSearchQuery).getHost();
}else{
actualQuery=currentSearchQuery;
}
for(CatalogInstance instance:data){ for(CatalogInstance instance:data){
if(instance.domain.contains(currentSearchQuery)){ if(instance.domain.contains(actualQuery)){
filteredData.add(instance); filteredData.add(instance);
} }
} }

View File

@@ -91,6 +91,9 @@ public class InstanceRulesFragment extends ToolbarFragment{
protected void onButtonClick(){ protected void onButtonClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance)); 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); Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
} }

View File

@@ -219,7 +219,9 @@ public class SignupFragment extends ToolbarFragment{
if(!serverSupportedTimezones.contains(timezone)) if(!serverSupportedTimezones.contains(timezone))
timezone=null; 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<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Token result){ 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 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 static Pattern EMOJI_CODE_PATTERN=Pattern.compile(":([\\w]+):");
private HtmlParser(){} private HtmlParser(){}

View File

@@ -11,9 +11,11 @@ import android.content.res.TypedArray;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@@ -32,6 +34,8 @@ import android.transition.ChangeScroll;
import android.transition.Fade; import android.transition.Fade;
import android.transition.TransitionManager; import android.transition.TransitionManager;
import android.transition.TransitionSet; import android.transition.TransitionSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@@ -39,7 +43,9 @@ import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.Toolbar; import android.widget.Toolbar;
@@ -882,4 +888,31 @@ public class UiUtils{
} }
return msg; 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);
}
}
} }

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<selector>
<item android:state_enabled="true">
<shape>
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp"/>
<solid android:color="?colorM3SurfaceVariant"/>
</shape>
</item>
<item>
<shape android:tint="?colorM3OnSurface">
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp"/>
<solid android:color="#0a000000"/>
</shape>
</item>
</selector>
</item>
<item android:left="-3dp" android:top="-3dp" android:right="-3dp">
<selector>
<item android:state_focused="true">
<shape>
<stroke android:color="?colorM3Primary" android:width="2dp"/>
</shape>
</item>
<item>
<shape>
<stroke android:color="?colorM3OnSurfaceVariant" android:width="1dp"/>
</shape>
</item>
</selector>
</item>
</layer-list>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp" />
<solid android:color="?colorM3SurfaceVariant" />
</shape>
</item>
<item android:left="-3dp" android:right="-3dp" android:top="-3dp">
<shape>
<stroke
android:width="2dp"
android:color="?colorM3Error" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,20Q3.175,20 2.588,19.413Q2,18.825 2,18V14Q2.825,14 3.413,13.412Q4,12.825 4,12Q4,11.175 3.413,10.587Q2.825,10 2,10V6Q2,5.175 2.588,4.588Q3.175,4 4,4H20Q20.825,4 21.413,4.588Q22,5.175 22,6V10Q21.175,10 20.587,10.587Q20,11.175 20,12Q20,12.825 20.587,13.412Q21.175,14 22,14V18Q22,18.825 21.413,19.413Q20.825,20 20,20ZM4,18H20V15.45Q19.075,14.9 18.538,13.988Q18,13.075 18,12Q18,10.925 18.538,10.012Q19.075,9.1 20,8.55V6H4V8.55Q4.925,9.1 5.463,10.012Q6,10.925 6,12Q6,13.075 5.463,13.988Q4.925,14.9 4,15.45ZM12,17Q12.425,17 12.713,16.712Q13,16.425 13,16Q13,15.575 12.713,15.287Q12.425,15 12,15Q11.575,15 11.288,15.287Q11,15.575 11,16Q11,16.425 11.288,16.712Q11.575,17 12,17ZM12,13Q12.425,13 12.713,12.712Q13,12.425 13,12Q13,11.575 12.713,11.287Q12.425,11 12,11Q11.575,11 11.288,11.287Q11,11.575 11,12Q11,12.425 11.288,12.712Q11.575,13 12,13ZM12,9Q12.425,9 12.713,8.712Q13,8.425 13,8Q13,7.575 12.713,7.287Q12.425,7 12,7Q11.575,7 11.288,7.287Q11,7.575 11,8Q11,8.425 11.288,8.712Q11.575,9 12,9ZM12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Z"/>
</vector>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingTop="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="@style/m3_headline_small"
android:textColor="?colorM3OnSurface"
android:drawableTop="@drawable/ic_confirmation_number_24px"
android:drawableTint="?colorM3Secondary"
android:drawablePadding="16dp"
android:text="@string/enter_invite_link"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="@string/need_invite_to_join_server"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="76dp"
android:layout_marginTop="16dp">
<EditText
android:id="@+id/edit"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@drawable/bg_m3_filled_text_field"
android:paddingHorizontal="16dp"
android:textColorHint="?colorM3OnSurfaceVariant"
android:textColor="?colorM3OnSurface"
android:gravity="start|bottom"
android:paddingBottom="8dp"
android:singleLine="true"
android:inputType="textUri"
android:textAppearance="@style/m3_body_large"
android:hint="example.social/invite/AbC123"/>
<TextView
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_gravity="top"
android:paddingEnd="23dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:text="@string/server_url"/>
<TextView
android:id="@+id/supporting_text"
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_marginHorizontal="16dp"
android:layout_gravity="bottom"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:text=""/>
<ImageButton
android:id="@+id/clear"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="4dp"
android:layout_gravity="end|top"
android:src="@drawable/ic_m3_cancel"
android:background="?android:actionBarItemBackground"
android:contentDescription="@string/clear"/>
</FrameLayout>
</LinearLayout>

View File

@@ -125,25 +125,15 @@
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/bg_onboarding_panel"> android:background="@drawable/bg_onboarding_panel">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/signup_random_server_explain"/>
<Button <Button
android:id="@+id/btn_random_instance" android:id="@+id/btn_use_invite"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="16dp" android:layout_marginLeft="16dp"
android:layout_marginRight="16dp" android:layout_marginRight="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
style="@style/Widget.Mastodon.M3.Button.Text" style="@style/Widget.Mastodon.M3.Button.Text"
android:text="@string/pick_server_for_me"/> android:text="@string/use_invite_link"/>
<Button <Button
android:id="@+id/btn_next" android:id="@+id/btn_next"

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ProgressBar>

View File

@@ -30,4 +30,6 @@
<item name="server_about" type="id"/> <item name="server_about" type="id"/>
<item name="server_rules" type="id"/> <item name="server_rules" type="id"/>
<item name="button_progress_orig_color" type="id"/>
</resources> </resources>

View File

@@ -332,7 +332,6 @@
<string name="login_title">Welcome Back</string> <string name="login_title">Welcome Back</string>
<string name="login_subtitle">Log in with the server where you created your account.</string> <string name="login_subtitle">Log in with the server where you created your account.</string>
<string name="server_url">Server URL</string> <string name="server_url">Server URL</string>
<string name="signup_random_server_explain">Well pick a server based on your language if you continue without making a selection.</string>
<string name="server_filter_any_language">Any Language</string> <string name="server_filter_any_language">Any Language</string>
<string name="server_filter_instant_signup">Instant Sign-up</string> <string name="server_filter_instant_signup">Instant Sign-up</string>
<string name="server_filter_manual_review">Manual Review</string> <string name="server_filter_manual_review">Manual Review</string>
@@ -346,7 +345,6 @@
<string name="not_accepting_new_members">Not accepting new members</string> <string name="not_accepting_new_members">Not accepting new members</string>
<string name="category_special_interests">Special Interests</string> <string name="category_special_interests">Special Interests</string>
<string name="signup_passwords_dont_match">Passwords dont match</string> <string name="signup_passwords_dont_match">Passwords dont match</string>
<string name="pick_server_for_me">Pick for me</string>
<string name="profile_add_row">Add row</string> <string name="profile_add_row">Add row</string>
<string name="profile_setup">Profile setup</string> <string name="profile_setup">Profile setup</string>
<string name="profile_setup_subtitle">You can always complete this later in the Profile tab.</string> <string name="profile_setup_subtitle">You can always complete this later in the Profile tab.</string>
@@ -670,4 +668,15 @@
<string name="button_reblogged">Boosted</string> <string name="button_reblogged">Boosted</string>
<string name="button_favorited">Favorited</string> <string name="button_favorited">Favorited</string>
<string name="bookmarked">Bookmarked</string> <string name="bookmarked">Bookmarked</string>
<string name="join_server_x_with_invite">Join %s with invite</string>
<string name="expired_invite_link">Expired invite link</string>
<string name="expired_clipboard_invite_link_alert">The invite link for %1$s in your clipboard has expired and cannot be used to sign up.\n\nYou can request a new link from an existing user, sign up through %2$s, or pick another server to sign up through.</string>
<string name="invalid_invite_link">Invalid invite link</string>
<string name="invalid_clipboard_invite_link_alert">The invite link for %1$s in your clipboard is not valid and cannot be used to sign up.\n\nYou can request a new link from an existing user, sign up through %2$s, or pick another server to sign up through.</string>
<string name="use_invite_link">Use invite link</string>
<string name="enter_invite_link">Enter invite link</string>
<string name="this_invite_is_invalid">This invite link is not valid.</string>
<string name="this_invite_has_expired">This invite link has expired.</string>
<string name="invite_link_pasted">Link pasted from your clipboard.</string>
<string name="need_invite_to_join_server">To join %s, youll need an invite link from an existing user.</string>
</resources> </resources>