diff --git a/gradle.properties b/gradle.properties index 52f5917c..3c6cdff7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 \ No newline at end of file +android.enableJetifier=false \ No newline at end of file diff --git a/mastodon/build.gradle b/mastodon/build.gradle index b25d6fb8..ce9f1c7d 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -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" diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index 7a9eb6e5..430a295f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -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/*"}); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index 23fbec2f..5b8072c0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -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; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java index cc642d9a..f7d4ac8d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java @@ -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; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java index dd86fdae..129e95fb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java @@ -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); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java index f4793b8c..5f0404ac 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java @@ -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 diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java index 93d8264d..e7594a6e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java @@ -92,7 +92,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment{ public InstancesAdapter(){ super(imgLoader); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java new file mode 100644 index 00000000..f1ca5c4f --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java @@ -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{ + private String accountID; + private Map 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 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 result){ + relationshipsRequest=null; + relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity())); + if(list==null) + return; + for(int i=0;i=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 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 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 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 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); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingProfileSetupFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingProfileSetupFragment.java new file mode 100644 index 00000000..a5f1e83c --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingProfileSetupFragment.java @@ -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 fieldTitles=savedInstanceState.getStringArrayList("fieldTitles"); + ArrayList fieldValues=savedInstanceState.getStringArrayList("fieldValues"); + for(int i=0;i{ + 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 fields=new ArrayList<>(); + for(int i=0;i(){ + @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 fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>(); + for(int i=0;i 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 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; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/ParsedAccount.java b/mastodon/src/main/java/org/joinmastodon/android/model/ParsedAccount.java new file mode 100644 index 00000000..751b2c0e --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/ParsedAccount.java @@ -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)); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Relationship.java b/mastodon/src/main/java/org/joinmastodon/android/model/Relationship.java index 837178fe..e5f6ec13 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Relationship.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Relationship.java @@ -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{"+ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java index fd07755e..47db4222 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java @@ -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 } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index fc2987f6..bfe12091 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -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 progressCallback, Consumer 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 View.OnClickListener(){ + private long lastClickTime; + + @Override + public void onClick(View v){ + if(SystemClock.uptimeMillis()-lastClickTime>500L){ + lastClickTime=SystemClock.uptimeMillis(); + l.onClick(v); + } + } + }; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java index bb92feee..e5fb7292 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java @@ -20,6 +20,7 @@ import android.text.Editable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; +import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.EditText; @@ -47,6 +48,7 @@ public class FloatingHintEditTextLayout extends FrameLayout{ private RectF tmpRect=new RectF(); private ColorStateList labelColors, origHintColors; private boolean errorState; + private TextView errorView; public FloatingHintEditTextLayout(Context context){ this(context, null); @@ -95,12 +97,22 @@ public class FloatingHintEditTextLayout extends FrameLayout{ label.setAlpha(0f); edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged)); + + errorView=new LinkedTextView(getContext()); + errorView.setTextAppearance(R.style.m3_body_small); + errorView.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant)); + errorView.setLinkTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary)); + errorView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + errorView.setPadding(V.dp(16), V.dp(4), V.dp(16), 0); + errorView.setVisibility(View.GONE); + addView(errorView); } private void onTextChanged(Editable text){ if(errorState){ + errorView.setVisibility(View.GONE); errorState=false; - setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field)); + setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field, getContext().getTheme())); refreshDrawableState(); } boolean newHintVisible=text.length()==0; @@ -211,12 +223,34 @@ public class FloatingHintEditTextLayout extends FrameLayout{ label.setTextColor(color.getColorForState(getDrawableState(), 0xff00ff00)); } - public void setErrorState(){ + public void setErrorState(CharSequence error){ if(errorState) return; errorState=true; setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field_error, getContext().getTheme())); label.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Error)); + errorView.setVisibility(VISIBLE); + errorView.setText(error); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + if(errorView.getVisibility()!=GONE){ + int width=MeasureSpec.getSize(widthMeasureSpec)-getPaddingLeft()-getPaddingRight(); + LayoutParams editLP=(LayoutParams) edit.getLayoutParams(); + width-=editLP.leftMargin+editLP.rightMargin; + errorView.measure(width | MeasureSpec.EXACTLY, MeasureSpec.UNSPECIFIED); + LayoutParams lp=(LayoutParams) errorView.getLayoutParams(); + lp.width=width; + lp.height=errorView.getMeasuredHeight(); + lp.gravity=Gravity.LEFT | Gravity.BOTTOM; + lp.leftMargin=editLP.leftMargin; + editLP.bottomMargin=errorView.getMeasuredHeight(); + }else{ + LayoutParams editLP=(LayoutParams) edit.getLayoutParams(); + editLP.bottomMargin=0; + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); } private class PaddedForegroundDrawable extends Drawable{ @@ -313,8 +347,8 @@ public class FloatingHintEditTextLayout extends FrameLayout{ @Override protected void onBoundsChange(@NonNull Rect bounds){ super.onBoundsChange(bounds); - LayoutParams lp=(LayoutParams) edit.getLayoutParams(); - wrapped.setBounds(bounds.left+lp.leftMargin-V.dp(12), bounds.top, bounds.right-lp.rightMargin+V.dp(12), bounds.bottom); + int offset=V.dp(12); + wrapped.setBounds(edit.getLeft()-offset, edit.getTop()-offset, edit.getRight()+offset, edit.getBottom()+offset); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java b/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java index 7fabbafd..2003018c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java +++ b/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java @@ -71,7 +71,7 @@ public class ElevationOnScrollListener extends RecyclerView.OnScrollListener imp @Override public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){ - handleScroll(v.getContext(), scrollY==0); + handleScroll(v.getContext(), scrollY<=0); } private void handleScroll(Context context, boolean newAtTop){ diff --git a/mastodon/src/main/res/color/button_text_m3_tonal.xml b/mastodon/src/main/res/color/button_text_m3_tonal.xml new file mode 100644 index 00000000..cce58367 --- /dev/null +++ b/mastodon/src/main/res/color/button_text_m3_tonal.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_button_m3_tonal.xml b/mastodon/src/main/res/drawable/bg_button_m3_tonal.xml new file mode 100644 index 00000000..d6043fc3 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_button_m3_tonal.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_onboarding_avatar.xml b/mastodon/src/main/res/drawable/bg_onboarding_avatar.xml new file mode 100644 index 00000000..92491880 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_onboarding_avatar.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_add_24px.xml b/mastodon/src/main/res/drawable/ic_add_24px.xml new file mode 100644 index 00000000..b234bf84 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_add_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_add_photo_alternate_48px.xml b/mastodon/src/main/res/drawable/ic_add_photo_alternate_48px.xml new file mode 100644 index 00000000..e7ffd70e --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_add_photo_alternate_48px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_drag_handle_24px.xml b/mastodon/src/main/res/drawable/ic_drag_handle_24px.xml new file mode 100644 index 00000000..7428dd8b --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_drag_handle_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/layout/fragment_onboarding_follow_suggestions.xml b/mastodon/src/main/res/layout/fragment_onboarding_follow_suggestions.xml new file mode 100644 index 00000000..7872fc04 --- /dev/null +++ b/mastodon/src/main/res/layout/fragment_onboarding_follow_suggestions.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + +