Signup flow redesign WIP
This commit is contained in:
parent
bcb3e217cd
commit
b153a64373
|
@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=false
|
|
@ -9,7 +9,7 @@ android {
|
|||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 47
|
||||
versionCode 48
|
||||
versionName "1.1.5"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "nl-rNL", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
|
|
|
@ -795,18 +795,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if Android platform photopicker is available on the device\
|
||||
* @return whether the device supports photopicker intents.
|
||||
*/
|
||||
private boolean isPhotoPickerAvailable() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return true;
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return getExtensionVersion(Build.VERSION_CODES.R) >= 2;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the correct intent for the device version to select media.
|
||||
|
@ -818,24 +806,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
*/
|
||||
private void openFilePicker(){
|
||||
Intent intent;
|
||||
boolean usePhotoPicker = isPhotoPickerAvailable();
|
||||
if (usePhotoPicker) {
|
||||
intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
|
||||
} else {
|
||||
intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
boolean usePhotoPicker=UiUtils.isPhotoPickerAvailable();
|
||||
if(usePhotoPicker){
|
||||
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MAX_ATTACHMENTS-getMediaAttachmentsCount());
|
||||
}else{
|
||||
intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
}
|
||||
if (!usePhotoPicker && instance.configuration != null &&
|
||||
instance.configuration.mediaAttachments != null &&
|
||||
instance.configuration.mediaAttachments.supportedMimeTypes != null &&
|
||||
!instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()) {
|
||||
if(!usePhotoPicker && instance.configuration!=null &&
|
||||
instance.configuration.mediaAttachments!=null &&
|
||||
instance.configuration.mediaAttachments.supportedMimeTypes!=null &&
|
||||
!instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES,
|
||||
instance.configuration.mediaAttachments.supportedMimeTypes.toArray(
|
||||
new String[0]));
|
||||
} else {
|
||||
if (!usePhotoPicker) {
|
||||
}else{
|
||||
if(!usePhotoPicker){
|
||||
// If photo picker is being used these are the default mimetypes.
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.joinmastodon.android.R;
|
|||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
@ -31,6 +32,7 @@ import java.util.ArrayList;
|
|||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
|
@ -235,6 +237,11 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||
new AccountSwitcherSheet(getActivity()).show();
|
||||
return true;
|
||||
}
|
||||
if(tab==R.id.tab_home){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,9 +37,11 @@ import org.joinmastodon.android.R;
|
|||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
|
@ -55,6 +57,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
|
@ -122,6 +125,19 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
||||
|
||||
if(BuildConfig.DEBUG){
|
||||
items.add(new RedHeaderItem("Debug options"));
|
||||
items.add(new TextItem("Test e-mail confirmation flow", ()->{
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("debug", true);
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}));
|
||||
}
|
||||
|
||||
items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
||||
}
|
||||
|
||||
|
@ -346,6 +362,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||
this.text=getString(text);
|
||||
}
|
||||
|
||||
public HeaderItem(String text){
|
||||
this.text=text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 0;
|
||||
|
@ -405,6 +425,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
public TextItem(String text, Runnable onClick){
|
||||
this.text=text;
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 4;
|
||||
|
@ -417,6 +442,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||
super(text);
|
||||
}
|
||||
|
||||
public RedHeaderItem(String text){
|
||||
super(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 5;
|
||||
|
|
|
@ -193,30 +193,24 @@ public class AccountActivationFragment extends ToolbarFragment{
|
|||
mgr.removeAccount(accountID);
|
||||
mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, null);
|
||||
String newID=mgr.getLastActiveAccountID();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", newID);
|
||||
if(session.self.avatar!=null || session.self.displayName!=null){
|
||||
File avaFile=session.self.avatar!=null ? new File(session.self.avatar) : null;
|
||||
new UpdateAccountCredentials(session.self.displayName, "", avaFile, null, Collections.emptyList())
|
||||
accountID=newID;
|
||||
if((session.self.avatar!=null || session.self.displayName!=null) && !getArguments().getBoolean("debug")){
|
||||
new UpdateAccountCredentials(session.self.displayName, "", (File)null, null, Collections.emptyList())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
if(avaFile!=null)
|
||||
avaFile.delete();
|
||||
mgr.updateAccountInfo(newID, result);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
proceed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(avaFile!=null)
|
||||
avaFile.delete();
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
proceed();
|
||||
}
|
||||
})
|
||||
.exec(newID);
|
||||
}else{
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
proceed();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,4 +243,11 @@ public class AccountActivationFragment extends ToolbarFragment{
|
|||
super.onDestroyView();
|
||||
resendBtn.removeCallbacks(resendTimer);
|
||||
}
|
||||
|
||||
private void proceed(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
// Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
Nav.goClearingStack(getActivity(), OnboardingProfileSetupFragment.class, args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,8 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
|||
private ItemsAdapter itemsAdapter;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
|
||||
private static final int SIGNUP_REQUEST=722;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -139,7 +141,16 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
|||
protected void onButtonClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), SignupFragment.class, args);
|
||||
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
super.onFragmentResult(reqCode, success, result);
|
||||
if(reqCode==SIGNUP_REQUEST && !success){
|
||||
setResult(false, null);
|
||||
Nav.finish(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -92,7 +92,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
|||
protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
|
||||
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
|
||||
return true;
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
||||
updateFilteredList();
|
||||
searchEdit.removeCallbacks(searchDebouncer);
|
||||
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
|
||||
|
@ -106,7 +106,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
|||
}
|
||||
|
||||
protected void onSearchChangedDebounced(){
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
||||
updateFilteredList();
|
||||
loadInstanceInfo(currentSearchQuery, false);
|
||||
}
|
||||
|
|
|
@ -527,6 +527,15 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
|||
updateFilteredList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!searchQueryMode){
|
||||
// Prevent search view automatically getting focused when the user returns to this fragment
|
||||
focusThing.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder>{
|
||||
public InstancesAdapter(){
|
||||
super(imgLoader);
|
||||
|
|
|
@ -0,0 +1,350 @@
|
|||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.model.FollowSuggestion;
|
||||
import org.joinmastodon.android.model.ParsedAccount;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<ParsedAccount>{
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
private View buttonBar;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
private int numRunningFollowRequests=0;
|
||||
|
||||
public OnboardingFollowSuggestionsFragment(){
|
||||
super(R.layout.fragment_onboarding_follow_suggestions, 40);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
setTitle(R.string.popular_on_mastodon);
|
||||
accountID=getArguments().getString("account");
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
|
||||
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
|
||||
view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
new GetFollowSuggestions(40)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
onDataLoaded(result.stream().map(fs->new ParsedAccount(fs.account, accountID)).collect(Collectors.toList()), false);
|
||||
loadRelationships();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void loadRelationships(){
|
||||
relationships=Collections.emptyMap();
|
||||
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
|
||||
relationshipsRequest.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Relationship> result){
|
||||
relationshipsRequest=null;
|
||||
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
|
||||
if(list==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof SuggestionViewHolder svh)
|
||||
svh.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
relationshipsRequest=null;
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return new SuggestionsAdapter();
|
||||
}
|
||||
|
||||
private void onFollowAllClick(View v){
|
||||
if(!loaded || relationships.isEmpty())
|
||||
return;
|
||||
if(data.isEmpty()){
|
||||
proceed();
|
||||
return;
|
||||
}
|
||||
ArrayList<String> accountIdsToFollow=new ArrayList<>();
|
||||
for(ParsedAccount acc:data){
|
||||
Relationship rel=relationships.get(acc.account.id);
|
||||
if(rel==null)
|
||||
continue;
|
||||
if(rel.canFollow())
|
||||
accountIdsToFollow.add(acc.account.id);
|
||||
}
|
||||
|
||||
final ProgressDialog progress=new ProgressDialog(getActivity());
|
||||
progress.setIndeterminate(false);
|
||||
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
progress.setMax(accountIdsToFollow.size());
|
||||
progress.setCancelable(false);
|
||||
progress.setMessage(getString(R.string.sending_follows));
|
||||
progress.show();
|
||||
|
||||
for(int i=0;i<Math.min(accountIdsToFollow.size(), 5);i++){ // Send up to 5 requests in parallel
|
||||
followNextAccount(accountIdsToFollow, progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void followNextAccount(ArrayList<String> accountIdsToFollow, ProgressDialog progress){
|
||||
if(accountIdsToFollow.isEmpty()){
|
||||
if(numRunningFollowRequests==0){
|
||||
progress.dismiss();
|
||||
proceed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
numRunningFollowRequests++;
|
||||
String id=accountIdsToFollow.remove(0);
|
||||
new SetAccountFollowed(id, true, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
numRunningFollowRequests--;
|
||||
progress.setProgress(progress.getMax()-accountIdsToFollow.size()-numRunningFollowRequests);
|
||||
followNextAccount(accountIdsToFollow, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
numRunningFollowRequests--;
|
||||
progress.setProgress(progress.getMax()-accountIdsToFollow.size()-numRunningFollowRequests);
|
||||
followNextAccount(accountIdsToFollow, progress);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void proceed(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), HomeFragment.class, args);
|
||||
getActivity().getWindow().getDecorView().postDelayed(()->Nav.finish(this), 500);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canGoBack(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToolbarNavigationClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
}
|
||||
|
||||
private class SuggestionsAdapter extends UsableRecyclerView.Adapter<SuggestionViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public SuggestionsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SuggestionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new SuggestionViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SuggestionViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return data.get(position).emojiHelper.getImageCount()+1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
ParsedAccount account=data.get(position);
|
||||
if(image==0)
|
||||
return account.avatarRequest;
|
||||
return account.emojiHelper.getImageRequest(image-1);
|
||||
}
|
||||
}
|
||||
|
||||
private class SuggestionViewHolder extends BindableViewHolder<ParsedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final TextView name, username, bio;
|
||||
private final ImageView avatar;
|
||||
private final ProgressBarButton actionButton;
|
||||
private final ProgressBar actionProgress;
|
||||
private final View actionWrap;
|
||||
|
||||
private Relationship relationship;
|
||||
|
||||
public SuggestionViewHolder(){
|
||||
super(getActivity(), R.layout.item_user_row_m3, list);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
bio=findViewById(R.id.bio);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
actionButton=findViewById(R.id.action_btn);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
actionWrap=findViewById(R.id.action_btn_wrap);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setClipToOutline(true);
|
||||
actionButton.setOnClickListener(UiUtils.rateLimitedClickListener(this::onActionButtonClick));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ParsedAccount item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText(item.account.getDisplayUsername());
|
||||
if(TextUtils.isEmpty(item.parsedBio)){
|
||||
bio.setVisibility(View.GONE);
|
||||
}else{
|
||||
bio.setVisibility(View.VISIBLE);
|
||||
bio.setText(item.parsedBio);
|
||||
}
|
||||
|
||||
relationship=relationships.get(item.account.id);
|
||||
if(relationship==null){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
}else{
|
||||
actionWrap.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
name.invalidate();
|
||||
bio.invalidate();
|
||||
}
|
||||
if(image instanceof Animatable a && !a.isRunning())
|
||||
a.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
private void onActionButtonClick(View v){
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
|
||||
itemView.setHasTransientState(false);
|
||||
relationships.put(item.account.id, rel);
|
||||
rebind();
|
||||
});
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
actionButton.setTextVisible(!visible);
|
||||
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
actionButton.setClickable(!visible);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class OnboardingProfileSetupFragment extends ToolbarFragment implements ReorderableLinearLayout.OnDragListener{
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private String accountID;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
private ScrollView scroller;
|
||||
private EditText nameEdit, bioEdit;
|
||||
private ImageView avaImage, coverImage;
|
||||
private Button addRow;
|
||||
private ReorderableLinearLayout profileFieldsLayout;
|
||||
private Uri avatarUri, coverUri;
|
||||
|
||||
private static final int AVATAR_RESULT=348;
|
||||
private static final int COVER_RESULT=183;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(R.string.profile_setup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View view=inflater.inflate(R.layout.fragment_onboarding_profile_setup, container, false);
|
||||
|
||||
scroller=view.findViewById(R.id.scroller);
|
||||
nameEdit=view.findViewById(R.id.display_name);
|
||||
bioEdit=view.findViewById(R.id.bio);
|
||||
avaImage=view.findViewById(R.id.avatar);
|
||||
coverImage=view.findViewById(R.id.header);
|
||||
addRow=view.findViewById(R.id.add_row);
|
||||
profileFieldsLayout=view.findViewById(R.id.profile_fields);
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
avaImage.setOutlineProvider(OutlineProviders.roundedRect(24));
|
||||
avaImage.setClipToOutline(true);
|
||||
|
||||
Account account=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||
if(savedInstanceState==null){
|
||||
nameEdit.setText(account.displayName);
|
||||
makeFieldsRow();
|
||||
}else{
|
||||
ArrayList<String> fieldTitles=savedInstanceState.getStringArrayList("fieldTitles");
|
||||
ArrayList<String> fieldValues=savedInstanceState.getStringArrayList("fieldValues");
|
||||
for(int i=0;i<fieldTitles.size();i++){
|
||||
View row=makeFieldsRow();
|
||||
EditText title=row.findViewById(R.id.title);
|
||||
EditText content=row.findViewById(R.id.content);
|
||||
title.setText(fieldTitles.get(i));
|
||||
content.setText(fieldValues.get(i));
|
||||
}
|
||||
if(fieldTitles.size()==4)
|
||||
addRow.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
addRow.setOnClickListener(v->{
|
||||
makeFieldsRow();
|
||||
if(profileFieldsLayout.getChildCount()==4){
|
||||
addRow.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
profileFieldsLayout.setDragListener(this);
|
||||
avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT));
|
||||
coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
scroller.setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
protected void onButtonClick(){
|
||||
ArrayList<AccountField> fields=new ArrayList<>();
|
||||
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
|
||||
View row=profileFieldsLayout.getChildAt(i);
|
||||
EditText title=row.findViewById(R.id.title);
|
||||
EditText content=row.findViewById(R.id.content);
|
||||
AccountField fld=new AccountField();
|
||||
fld.name=title.getText().toString();
|
||||
fld.value=content.getText().toString();
|
||||
fields.add(fld);
|
||||
}
|
||||
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, fields)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, result);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
|
||||
getActivity().getWindow().getDecorView().postDelayed(()->Nav.finish(OnboardingProfileSetupFragment.this), 500);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.saving, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
private View makeFieldsRow(){
|
||||
View view=LayoutInflater.from(getActivity()).inflate(R.layout.onboarding_profile_field, profileFieldsLayout, false);
|
||||
profileFieldsLayout.addView(view);
|
||||
view.findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
|
||||
profileFieldsLayout.startDragging(view);
|
||||
return true;
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwapItems(int oldIndex, int newIndex){}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
ArrayList<String> fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>();
|
||||
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
|
||||
View row=profileFieldsLayout.getChildAt(i);
|
||||
EditText title=row.findViewById(R.id.title);
|
||||
EditText content=row.findViewById(R.id.content);
|
||||
fieldTitles.add(title.getText().toString());
|
||||
fieldValues.add(content.getText().toString());
|
||||
}
|
||||
outState.putStringArrayList("fieldTitles", fieldTitles);
|
||||
outState.putStringArrayList("fieldValues", fieldValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(resultCode!=Activity.RESULT_OK)
|
||||
return;
|
||||
ImageView img;
|
||||
Uri uri=data.getData();
|
||||
int size;
|
||||
if(requestCode==AVATAR_RESULT){
|
||||
img=avaImage;
|
||||
avatarUri=uri;
|
||||
size=V.dp(100);
|
||||
}else{
|
||||
img=coverImage;
|
||||
coverUri=uri;
|
||||
size=V.dp(1000);
|
||||
}
|
||||
img.setForeground(null);
|
||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canGoBack(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToolbarNavigationClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,18 @@
|
|||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -16,11 +20,9 @@ import android.view.ViewTreeObserver;
|
|||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.MastodonDetailedErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.RegisterAccount;
|
||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||
|
@ -31,18 +33,22 @@ import org.joinmastodon.android.model.Account;
|
|||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.ui.text.LinkSpan;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -51,12 +57,10 @@ import me.grishka.appkit.api.APIRequest;
|
|||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class SignupFragment extends ToolbarFragment{
|
||||
private static final int AVATAR_RESULT=198;
|
||||
private static final String TAG="SignupFragment";
|
||||
|
||||
private Instance instance;
|
||||
|
@ -73,6 +77,7 @@ public class SignupFragment extends ToolbarFragment{
|
|||
private boolean submitAfterGettingToken;
|
||||
private ProgressDialog progressDialog;
|
||||
private HashSet<EditText> errorFields=new HashSet<>();
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
|
@ -145,19 +150,22 @@ public class SignupFragment extends ToolbarFragment{
|
|||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.findViewById(R.id.scroller).setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackground(null);
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
private void onButtonClick(){
|
||||
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
|
||||
passwordConfirm.setError(getString(R.string.signup_passwords_dont_match));
|
||||
passwordConfirmWrap.setErrorState();
|
||||
passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match));
|
||||
return;
|
||||
}
|
||||
showProgressDialog();
|
||||
|
@ -212,8 +220,22 @@ public class SignupFragment extends ToolbarFragment{
|
|||
anyFieldsSkipped=true;
|
||||
continue;
|
||||
}
|
||||
field.setError(fieldErrors.get(fieldName).stream().map(err->err.description).collect(Collectors.joining("\n")));
|
||||
getFieldWrapByName(fieldName).setErrorState();
|
||||
List<MastodonDetailedErrorResponse.FieldError> errors=Objects.requireNonNull(fieldErrors.get(fieldName));
|
||||
if(errors.size()==1){
|
||||
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
|
||||
}else{
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
boolean firstErr=true;
|
||||
for(MastodonDetailedErrorResponse.FieldError err:errors){
|
||||
if(firstErr){
|
||||
firstErr=false;
|
||||
}else{
|
||||
ssb.append('\n');
|
||||
}
|
||||
ssb.append(getErrorDescription(err, fieldName));
|
||||
}
|
||||
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
|
||||
}
|
||||
errorFields.add(field);
|
||||
if(first){
|
||||
first=false;
|
||||
|
@ -231,6 +253,40 @@ public class SignupFragment extends ToolbarFragment{
|
|||
.exec(instance.uri, apiToken);
|
||||
}
|
||||
|
||||
private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){
|
||||
return switch(fieldName){
|
||||
case "email" -> switch(error.error){
|
||||
case "ERR_BLOCKED" -> {
|
||||
String emailAddr=email.getText().toString();
|
||||
String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1)));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){
|
||||
private int spanStart;
|
||||
@Override
|
||||
public void head(Node node, int depth){
|
||||
if(node instanceof TextNode tn){
|
||||
ssb.append(tn.text());
|
||||
}else if(node instanceof Element){
|
||||
spanStart=ssb.length();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tail(Node node, int depth){
|
||||
if(node instanceof Element){
|
||||
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
});
|
||||
yield ssb;
|
||||
}
|
||||
default -> error.description;
|
||||
};
|
||||
default -> error.description;
|
||||
};
|
||||
}
|
||||
|
||||
private EditText getFieldByName(String name){
|
||||
return switch(name){
|
||||
case "email" -> email;
|
||||
|
@ -323,6 +379,11 @@ public class SignupFragment extends ToolbarFragment{
|
|||
}
|
||||
}
|
||||
|
||||
private void onGoBackLinkClick(LinkSpan span){
|
||||
setResult(false, null);
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
private class ErrorClearingListener implements TextWatcher{
|
||||
public final EditText editText;
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ParsedAccount{
|
||||
public Account account;
|
||||
public CharSequence parsedName, parsedBio;
|
||||
public CustomEmojiHelper emojiHelper;
|
||||
public ImageLoaderRequest avatarRequest;
|
||||
|
||||
public ParsedAccount(Account account, String accountID){
|
||||
this.account=account;
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
|
||||
ssb.append(parsedBio);
|
||||
emojiHelper.setText(ssb);
|
||||
|
||||
avatarRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(40), V.dp(40));
|
||||
}
|
||||
}
|
|
@ -18,6 +18,10 @@ public class Relationship extends BaseModel{
|
|||
public boolean blockedBy;
|
||||
public String note;
|
||||
|
||||
public boolean canFollow(){
|
||||
return !(following || blocking || blockedBy || domainBlocking);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Relationship{"+
|
||||
|
|
|
@ -35,6 +35,7 @@ public class LinkSpan extends CharacterStyle {
|
|||
case URL -> UiUtils.openURL(context, accountID, link);
|
||||
case MENTION -> UiUtils.openProfileByID(context, accountID, link);
|
||||
case HASHTAG -> UiUtils.openHashtagTimeline(context, accountID, link);
|
||||
case CUSTOM -> listener.onLinkClick(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,6 +58,7 @@ public class LinkSpan extends CharacterStyle {
|
|||
public enum Type{
|
||||
URL,
|
||||
MENTION,
|
||||
HASHTAG
|
||||
HASHTAG,
|
||||
CUSTOM
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,9 @@ import android.os.Build;
|
|||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.os.ext.SdkExtensions;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
@ -443,6 +446,38 @@ public class UiUtils{
|
|||
ta.recycle();
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButtonM3(Relationship relationship, Button button){
|
||||
boolean secondaryStyle;
|
||||
if(relationship.blocking){
|
||||
button.setText(R.string.button_blocked);
|
||||
secondaryStyle=true;
|
||||
}else if(relationship.blockedBy){
|
||||
button.setText(R.string.button_follow);
|
||||
secondaryStyle=false;
|
||||
}else if(relationship.requested){
|
||||
button.setText(R.string.button_follow_pending);
|
||||
secondaryStyle=true;
|
||||
}else if(!relationship.following){
|
||||
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
|
||||
secondaryStyle=false;
|
||||
}else{
|
||||
button.setText(R.string.button_following);
|
||||
secondaryStyle=true;
|
||||
}
|
||||
|
||||
button.setEnabled(!relationship.blockedBy);
|
||||
int styleRes=secondaryStyle ? R.style.Widget_Mastodon_M3_Button_Tonal : R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
if(relationship.blocking)
|
||||
button.setTextColor(button.getResources().getColorStateList(R.color.error_600));
|
||||
else
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer<Boolean> progressCallback, Consumer<Relationship> resultCallback){
|
||||
if(relationship.blocking){
|
||||
confirmToggleBlockUser(activity, accountID, account, true, resultCallback);
|
||||
|
@ -609,4 +644,63 @@ public class UiUtils{
|
|||
int b=Math.round((color1 & 0xFF)*alpha0+(color2 & 0xFF)*alpha);
|
||||
return 0xFF000000 | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if Android platform photopicker is available on the device\
|
||||
*
|
||||
* @return whether the device supports photopicker intents.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public static boolean isPhotoPickerAvailable(){
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||
return true;
|
||||
}else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.R){
|
||||
return SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R)>=2;
|
||||
}else
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
public static Intent getMediaPickerIntent(String[] mimeTypes, int maxCount){
|
||||
Intent intent;
|
||||
if(isPhotoPickerAvailable()){
|
||||
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
if(maxCount>1)
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxCount);
|
||||
}else{
|
||||
intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
}
|
||||
if(mimeTypes.length>1){
|
||||
intent.setType("*/*");
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||
}else if(mimeTypes.length==1){
|
||||
intent.setType(mimeTypes[0]);
|
||||
}else{
|
||||
intent.setType("*/*");
|
||||
}
|
||||
if(maxCount>1)
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a View.OnClickListener to filter multiple clicks in succession.
|
||||
* Useful for buttons that perform some action that changes their state asynchronously.
|
||||
* @param l
|
||||
* @return
|
||||
*/
|
||||
public static View.OnClickListener rateLimitedClickListener(View.OnClickListener l){
|
||||
return new |