From e1db5f15caafba76c552e3201db115db77307ac7 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sat, 24 Jun 2023 22:56:55 +0300 Subject: [PATCH] M3 redesign: search/discover --- .../joinmastodon/android/MainActivity.java | 27 +- .../api/requests/search/GetSearchResults.java | 5 + .../fragments/BaseStatusListFragment.java | 4 + .../android/fragments/ProfileFragment.java | 2 +- .../ComposeAccountSearchFragment.java | 69 +-- .../discover/DiscoverAccountsFragment.java | 274 +-------- .../fragments/discover/DiscoverFragment.java | 123 ++-- .../discover/DiscoverNewsFragment.java | 120 ++-- .../discover/DiscoverPostsFragment.java | 20 +- .../discover/LocalTimelineFragment.java | 20 +- .../fragments/discover/SearchFragment.java | 179 ++---- .../discover/SearchQueryFragment.java | 554 ++++++++++++++++++ .../discover/TrendingHashtagsFragment.java | 8 - .../settings/SettingsDebugFragment.java | 12 +- .../settings/SettingsServerFragment.java | 2 +- .../android/model/SearchResult.java | 1 + .../model/viewmodel/CardViewModel.java | 19 + .../viewmodel/SearchResultViewModel.java | 24 + .../android/ui/SearchViewHelper.java | 129 ++++ .../ui/utils/DiscoverInfoBannerHelper.java | 90 +-- .../ui/viewholders/AccountViewHolder.java | 2 + .../android/ui/views/HashtagChartView.java | 2 +- .../main/res/drawable/bg_rect_12dp_ripple.xml | 9 + .../src/main/res/drawable/ic_feed_24px.xml | 9 + .../main/res/drawable/ic_group_add_24px.xml | 9 + .../src/main/res/drawable/ic_history_24px.xml | 9 + .../src/main/res/drawable/ic_link_24px.xml | 9 + .../src/main/res/drawable/ic_person_24px.xml | 9 + .../src/main/res/drawable/ic_stream_24px.xml | 9 + .../src/main/res/drawable/ic_tag_24px.xml | 9 + .../main/res/drawable/ic_whatshot_24px.xml | 9 + .../{bg_search_field.xml => rect_12dp.xml} | 4 +- .../main/res/layout/discover_info_banner.xml | 37 +- .../src/main/res/layout/fragment_discover.xml | 142 ++--- .../src/main/res/layout/fragment_search.xml | 12 - .../main/res/layout/item_trending_link.xml | 45 +- .../res/layout/item_trending_link_card.xml | 44 ++ mastodon/src/main/res/values/ids.xml | 1 + mastodon/src/main/res/values/strings.xml | 20 +- mastodon/src/main/res/values/styles.xml | 2 + 40 files changed, 1300 insertions(+), 774 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CardViewModel.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/SearchResultViewModel.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/SearchViewHelper.java create mode 100644 mastodon/src/main/res/drawable/bg_rect_12dp_ripple.xml create mode 100644 mastodon/src/main/res/drawable/ic_feed_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_group_add_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_history_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_link_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_person_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_stream_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_tag_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_whatshot_24px.xml rename mastodon/src/main/res/drawable/{bg_search_field.xml => rect_12dp.xml} (59%) create mode 100644 mastodon/src/main/res/layout/item_trending_link_card.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java index 1c7fccbc3..99cfcc77f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java +++ b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java @@ -71,7 +71,7 @@ public class MainActivity extends FragmentStackActivity{ }else if(intent.getBooleanExtra("compose", false)){ showCompose(); }else if(Intent.ACTION_VIEW.equals(intent.getAction())){ - handleURL(intent.getData()); + handleURL(intent.getData(), null); }else{ maybeRequestNotificationsPermission(); } @@ -114,29 +114,34 @@ public class MainActivity extends FragmentStackActivity{ }else if(intent.getBooleanExtra("compose", false)){ showCompose(); }else if(Intent.ACTION_VIEW.equals(intent.getAction())){ - handleURL(intent.getData()); + handleURL(intent.getData(), null); }/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){ GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this); }*/ } - private void handleURL(Uri uri){ + public void handleURL(Uri uri, String accountID){ if(uri==null) return; if(!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme())) return; - if(!uri.getPath().startsWith("/@")) - return; - AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount(); + AccountSession session; + if(accountID==null) + session=AccountSessionManager.getInstance().getLastActiveAccount(); + else + session=AccountSessionManager.get(accountID); if(session==null || !session.activated) return; + openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false); + } - new GetSearchResults(uri.toString(), null, true) + public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){ + new GetSearchResults(q, null, true) .setCallback(new Callback<>(){ @Override public void onSuccess(SearchResults result){ Bundle args=new Bundle(); - args.putString("account", session.getID()); + args.putString("account", accountID); if(result.statuses!=null && !result.statuses.isEmpty()){ args.putParcelable("status", Parcels.wrap(result.statuses.get(0))); Nav.go(MainActivity.this, ThreadFragment.class, args); @@ -144,7 +149,7 @@ public class MainActivity extends FragmentStackActivity{ args.putParcelable("profileAccount", Parcels.wrap(result.accounts.get(0))); Nav.go(MainActivity.this, ProfileFragment.class, args); }else{ - Toast.makeText(MainActivity.this, R.string.link_not_supported, Toast.LENGTH_SHORT).show(); + Toast.makeText(MainActivity.this, fromSearch ? R.string.no_search_results : R.string.link_not_supported, Toast.LENGTH_SHORT).show(); } } @@ -153,8 +158,8 @@ public class MainActivity extends FragmentStackActivity{ error.showToast(MainActivity.this); } }) - .wrapProgress(this, R.string.opening_link, true) - .exec(session.getID()); + .wrapProgress(this, progressText, true) + .exec(accountID); } private void showFragmentForNotification(Notification notification, String accountID){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/search/GetSearchResults.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/search/GetSearchResults.java index a35745988..0407bb702 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/search/GetSearchResults.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/search/GetSearchResults.java @@ -13,6 +13,11 @@ public class GetSearchResults extends MastodonAPIRequest{ addQueryParameter("resolve", "true"); } + public GetSearchResults limit(int limit){ + addQueryParameter("limit", String.valueOf(limit)); + return this; + } + @Override protected String getPathPrefix(){ return "/api/v2"; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 6c229b2c1..42dd3fc36 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -54,6 +54,7 @@ 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.MergeRecyclerAdapter; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; @@ -309,6 +310,9 @@ public abstract class BaseStatusListFragment exten } protected int getMainAdapterOffset(){ + if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){ + return mergeAdapter.getPositionForAdapter(adapter); + } return 0; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index 54da08665..1c804f862 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -242,7 +242,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList sizeWrapper.addView(content); tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary)); - tabbar.setTabTextSize(V.dp(16)); + tabbar.setTabTextSize(V.dp(14)); tabLayoutMediator=new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){ @Override public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/ComposeAccountSearchFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/ComposeAccountSearchFragment.java index aa0fb8536..dd12b11b6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/ComposeAccountSearchFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/ComposeAccountSearchFragment.java @@ -1,22 +1,14 @@ package org.joinmastodon.android.fragments.account_list; -import android.content.res.ColorStateList; import android.os.Bundle; -import android.text.InputType; import android.text.TextUtils; import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.Toolbar; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.search.GetSearchResults; import org.joinmastodon.android.model.SearchResults; import org.joinmastodon.android.model.viewmodel.AccountViewModel; -import org.joinmastodon.android.ui.utils.SimpleTextWatcher; +import org.joinmastodon.android.ui.SearchViewHelper; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.viewholders.AccountViewHolder; import org.parceler.Parcels; @@ -25,23 +17,11 @@ import java.util.stream.Collectors; import me.grishka.appkit.Nav; import me.grishka.appkit.api.SimpleCallback; -import me.grishka.appkit.utils.V; public class ComposeAccountSearchFragment extends BaseAccountListFragment{ - private LinearLayout searchLayout; - private EditText searchEdit; - private ImageButton clearSearchButton; private String currentQuery; - private Runnable debouncer=()->{ - currentQuery=searchEdit.getText().toString(); - if(currentRequest!=null){ - currentRequest.cancel(); - currentRequest=null; - } - if(!TextUtils.isEmpty(currentQuery)) - loadData(); - }; private boolean resultDelivered; + private SearchViewHelper searchViewHelper; @Override public void onCreate(Bundle savedInstanceState){ @@ -53,32 +33,9 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{ @Override public void onViewCreated(View view, Bundle savedInstanceState){ - searchLayout=new LinearLayout(view.getContext()); - searchLayout.setOrientation(LinearLayout.HORIZONTAL); - - searchEdit=new EditText(view.getContext()); - searchEdit.setHint(R.string.search_hint); - searchEdit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER); - searchEdit.setBackground(null); - searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{ - searchEdit.removeCallbacks(debouncer); - searchEdit.postDelayed(debouncer, 300); - })); - searchEdit.setImeActionLabel(null, EditorInfo.IME_ACTION_SEARCH); - searchEdit.setOnEditorActionListener((v, actionId, event)->{ - searchEdit.removeCallbacks(debouncer); - debouncer.run(); - return true; - }); - searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)); - - clearSearchButton=new ImageButton(view.getContext()); - clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24); - clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant))); - clearSearchButton.setBackground(UiUtils.getThemeDrawable(getToolbarContext(), android.R.attr.actionBarItemBackground)); - clearSearchButton.setOnClickListener(v->searchEdit.setText("")); - searchLayout.addView(clearSearchButton, new LinearLayout.LayoutParams(V.dp(56), ViewGroup.LayoutParams.MATCH_PARENT)); - + searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint)); + searchViewHelper.setListeners(this::onQueryChanged, null); + searchViewHelper.addDivider(contentView); super.onViewCreated(view, savedInstanceState); view.setBackgroundResource(R.drawable.bg_m3_surface3); @@ -104,11 +61,7 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{ @Override protected void onUpdateToolbar(){ super.onUpdateToolbar(); - if(searchLayout.getParent()!=null) - ((ViewGroup) searchLayout.getParent()).removeView(searchLayout); - getToolbar().addView(searchLayout, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - getToolbar().setBackgroundResource(R.drawable.bg_m3_surface3); - searchEdit.requestFocus(); + searchViewHelper.install(getToolbar()); } @Override @@ -132,4 +85,14 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{ setResult(true, res); Nav.finish(this, false); } + + private void onQueryChanged(String q){ + currentQuery=q; + if(currentRequest!=null){ + currentRequest.cancel(); + currentRequest=null; + } + if(!TextUtils.isEmpty(currentQuery)) + loadData(); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverAccountsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverAccountsFragment.java index fb95db010..048012928 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverAccountsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverAccountsFragment.java @@ -1,80 +1,39 @@ package org.joinmastodon.android.fragments.discover; -import android.graphics.Rect; -import android.graphics.drawable.Animatable; -import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -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.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ScrollableToTop; -import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment; import org.joinmastodon.android.model.FollowSuggestion; -import org.joinmastodon.android.model.Relationship; -import org.joinmastodon.android.ui.OutlineProviders; -import org.joinmastodon.android.ui.text.HtmlParser; -import org.joinmastodon.android.ui.utils.CustomEmojiHelper; -import org.joinmastodon.android.ui.utils.UiUtils; -import org.joinmastodon.android.ui.views.ProgressBarButton; -import org.parceler.Parcels; +import org.joinmastodon.android.model.viewmodel.AccountViewModel; +import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; -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.imageloader.requests.UrlImageLoaderRequest; -import me.grishka.appkit.utils.BindableViewHolder; -import me.grishka.appkit.utils.V; -import me.grishka.appkit.views.UsableRecyclerView; +import me.grishka.appkit.utils.MergeRecyclerAdapter; -public class DiscoverAccountsFragment extends BaseRecyclerFragment implements ScrollableToTop{ - private String accountID; - private Map relationships=Collections.emptyMap(); - private GetAccountRelationships relationshipsRequest; - - public DiscoverAccountsFragment(){ - super(20); - } +public class DiscoverAccountsFragment extends BaseAccountListFragment implements ScrollableToTop{ + private DiscoverInfoBannerHelper bannerHelper; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); - accountID=getArguments().getString("account"); + bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.ACCOUNTS, accountID); } @Override protected void doLoadData(int offset, int count){ - if(relationshipsRequest!=null){ - relationshipsRequest.cancel(); - relationshipsRequest=null; - } currentRequest=new GetFollowSuggestions(count) .setCallback(new SimpleCallback<>(this){ @Override public void onSuccess(List result){ - onDataLoaded(result.stream().map(fs->new AccountWrapper(fs.account)).collect(Collectors.toList()), false); - loadRelationships(); + List accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()); + onDataLoaded(accounts, false); + bannerHelper.onBannerBecameVisible(); } }) .exec(accountID); @@ -82,219 +41,14 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragmentfs.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 implements ImageLoaderRecyclerAdapter{ - - public AccountsAdapter(){ - super(imgLoader); - } - - @Override - public void onBindViewHolder(AccountViewHolder holder, int position){ - holder.bind(data.get(position)); - super.onBindViewHolder(holder, position); - } - - @NonNull - @Override - public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ - return new AccountViewHolder(); - } - - @Override - public int getItemCount(){ - return data.size(); - } - - @Override - public int getImageCountForItem(int position){ - return 2+data.get(position).emojiHelper.getImageCount(); - } - - @Override - public ImageLoaderRequest getImageRequest(int position, int image){ - AccountWrapper item=data.get(position); - if(image==0) - return item.avaRequest; - else if(image==1) - return item.coverRequest; - else - return item.emojiHelper.getImageRequest(image-2); - } - } - - private class AccountViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{ - private final ImageView cover, avatar; - private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel; - private final ProgressBarButton actionButton; - private final ProgressBar actionProgress; - private final View actionWrap; - - private Relationship relationship; - - public AccountViewHolder(){ - super(getActivity(), R.layout.item_discover_account, list); - cover=findViewById(R.id.cover); - avatar=findViewById(R.id.avatar); - name=findViewById(R.id.name); - username=findViewById(R.id.username); - bio=findViewById(R.id.bio); - followersCount=findViewById(R.id.followers_count); - followersLabel=findViewById(R.id.followers_label); - followingCount=findViewById(R.id.following_count); - followingLabel=findViewById(R.id.following_label); - postsCount=findViewById(R.id.posts_count); - postsLabel=findViewById(R.id.posts_label); - actionButton=findViewById(R.id.action_btn); - actionProgress=findViewById(R.id.action_progress); - actionWrap=findViewById(R.id.action_btn_wrap); - - itemView.setOutlineProvider(OutlineProviders.roundedRect(6)); - itemView.setClipToOutline(true); - avatar.setOutlineProvider(OutlineProviders.roundedRect(12)); - avatar.setClipToOutline(true); - cover.setOutlineProvider(OutlineProviders.roundedRect(3)); - cover.setClipToOutline(true); - actionButton.setOnClickListener(this::onActionButtonClick); - } - - @Override - public void onBind(AccountWrapper item){ - name.setText(item.parsedName); - username.setText('@'+item.account.acct); - bio.setText(item.parsedBio); - followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount)); - followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount)); - postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount)); - followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount))); - followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount))); - postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount))); - relationship=relationships.get(item.account.id); - if(relationship==null){ - actionWrap.setVisibility(View.GONE); - }else{ - actionWrap.setVisibility(View.VISIBLE); - UiUtils.setRelationshipToActionButton(relationship, actionButton); - } - } - - @Override - public void setImage(int index, Drawable image){ - if(index==0){ - avatar.setImageDrawable(image); - }else if(index==1){ - cover.setImageDrawable(image); - }else{ - item.emojiHelper.setImageDrawable(index-2, 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); - } - } - - protected class AccountWrapper{ - public Account account; - public ImageLoaderRequest avaRequest, coverRequest; - public CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); - public CharSequence parsedName, parsedBio; - - public AccountWrapper(Account account){ - this.account=account; - if(!TextUtils.isEmpty(account.avatar)) - avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50)); - if(!TextUtils.isEmpty(account.header)) - coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000); - parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID); - if(account.emojis.isEmpty()){ - parsedName=account.displayName; - }else{ - parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); - emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio)); - } - } - } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java index a559c931d..508aaa835 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java @@ -3,22 +3,19 @@ package org.joinmastodon.android.fragments.discover; import android.app.Fragment; import android.os.Build; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.KeyEvent; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.LinearLayout; -import android.widget.ProgressBar; import android.widget.TextView; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.ScrollableToTop; +import org.joinmastodon.android.model.SearchResult; +import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayoutMediator; @@ -28,22 +25,24 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.widget.ViewPager2; +import me.grishka.appkit.Nav; import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.V; public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{ + private static final int QUERY_RESULT=937; private TabLayout tabLayout; private ViewPager2 pager; private FrameLayout[] tabViews; private TabLayoutMediator tabLayoutMediator; - private EditText searchEdit; private boolean searchActive; private FrameLayout searchView; - private ImageButton searchBack, searchClear; - private ProgressBar searchProgress; + private ImageButton searchBack; + private TextView searchText; + private View tabsDivider; private DiscoverPostsFragment postsFragment; private TrendingHashtagsFragment hashtagsFragment; @@ -53,7 +52,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, private LocalTimelineFragment localTimelineFragment; private String accountID; - private Runnable searchDebouncer=this::onSearchChangedDebounced; + private String currentQuery; @Override public void onCreate(Bundle savedInstanceState){ @@ -88,8 +87,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, tabViews[i]=tabView; } - tabLayout.setTabTextSize(V.dp(16)); - tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); + tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary)); + tabLayout.setTabTextSize(V.dp(14)); pager.setOffscreenPageLimit(4); pager.setAdapter(new DiscoverPagerAdapter()); @@ -146,7 +145,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, case 4 -> R.string.for_you; default -> throw new IllegalStateException("Unexpected value: "+position); }); - tab.view.textView.setAllCaps(true); } }); tabLayoutMediator.attach(); @@ -163,44 +161,17 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, } }); - searchEdit=view.findViewById(R.id.search_edit); - searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged); - searchEdit.setOnEditorActionListener(this::onSearchEnterPressed); - searchEdit.addTextChangedListener(new TextWatcher(){ - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after){ - if(s.length()==0){ - V.setVisibilityAnimated(searchClear, View.VISIBLE); - } - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count){ - searchEdit.removeCallbacks(searchDebouncer); - searchEdit.postDelayed(searchDebouncer, 300); - } - - @Override - public void afterTextChanged(Editable s){ - if(s.length()==0){ - V.setVisibilityAnimated(searchClear, View.INVISIBLE); - } - } - }); - searchView=view.findViewById(R.id.search_fragment); if(searchFragment==null){ searchFragment=new SearchFragment(); Bundle args=new Bundle(); args.putString("account", accountID); searchFragment.setArguments(args); - searchFragment.setProgressVisibilityListener(this::onSearchProgressVisibilityChanged); getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit(); } searchBack=view.findViewById(R.id.search_back); - searchClear=view.findViewById(R.id.search_clear); - searchProgress=view.findViewById(R.id.search_progress); + searchText=view.findViewById(R.id.search_text); searchBack.setEnabled(searchActive); searchBack.setImportantForAccessibility(searchActive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO); searchBack.setOnClickListener(v->exitSearch()); @@ -210,11 +181,19 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, tabLayout.setVisibility(View.GONE); searchView.setVisibility(View.VISIBLE); } - searchClear.setOnClickListener(v->{ - searchEdit.setText(""); - searchEdit.removeCallbacks(searchDebouncer); - onSearchChangedDebounced(); + + View searchWrap=view.findViewById(R.id.search_wrap); + searchWrap.setOutlineProvider(OutlineProviders.roundedRect(28)); + searchWrap.setClipToOutline(true); + searchText.setOnClickListener(v->{ + Bundle args=new Bundle(); + args.putString("account", accountID); + if(!TextUtils.isEmpty(currentQuery)){ + args.putString("query", currentQuery); + } + Nav.goForResult(getActivity(), SearchQueryFragment.class, args, QUERY_RESULT, DiscoverFragment.this); }); + tabsDivider=view.findViewById(R.id.tabs_divider); return view; } @@ -233,35 +212,32 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, postsFragment.loadData(); } - private void onSearchEditFocusChanged(View v, boolean hasFocus){ - if(!searchActive && hasFocus){ + private void enterSearch(){ + if(!searchActive){ searchActive=true; pager.setVisibility(View.GONE); tabLayout.setVisibility(View.GONE); searchView.setVisibility(View.VISIBLE); - searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular); + searchBack.setImageResource(R.drawable.ic_arrow_back); searchBack.setEnabled(true); searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + tabsDivider.setVisibility(View.GONE); } } private void exitSearch(){ + if(!searchActive) + return; searchActive=false; pager.setVisibility(View.VISIBLE); tabLayout.setVisibility(View.VISIBLE); searchView.setVisibility(View.GONE); - searchEdit.clearFocus(); - searchEdit.setText(""); - searchBack.setImageResource(R.drawable.ic_fluent_search_24_regular); + searchText.setText(R.string.search_mastodon); + searchBack.setImageResource(R.drawable.ic_search_24px); searchBack.setEnabled(false); searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0); - } - - @Override - protected void onHidden(){ - super.onHidden(); - getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0); + tabsDivider.setVisibility(View.VISIBLE); + currentQuery=null; } private Fragment getFragmentForPage(int page){ @@ -284,23 +260,20 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, return false; } - private void onSearchChangedDebounced(){ - searchFragment.setQuery(searchEdit.getText().toString()); - } - - private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){ - if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN) - return true; - searchEdit.removeCallbacks(searchDebouncer); - onSearchChangedDebounced(); - getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0); - return true; - } - - private void onSearchProgressVisibilityChanged(boolean visible){ - V.setVisibilityAnimated(searchProgress, visible ? View.VISIBLE : View.INVISIBLE); - if(searchEdit.length()>0) - V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE); + @Override + public void onFragmentResult(int reqCode, boolean success, Bundle result){ + if(reqCode==QUERY_RESULT && success){ + enterSearch(); + currentQuery=result.getString("query"); + SearchResult.Type type; + if(result.containsKey("filter")){ + type=SearchResult.Type.values()[result.getInt("filter")]; + }else{ + type=null; + } + searchFragment.setQuery(currentQuery, type); + searchText.setText(currentQuery); + } } private class DiscoverPagerAdapter extends RecyclerView.Adapter{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java index 9e5f65aeb..7c0917f57 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java @@ -1,5 +1,6 @@ package org.joinmastodon.android.fragments.discover; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.TextUtils; @@ -12,32 +13,43 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.trends.GetTrendingLinks; import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.model.Card; +import org.joinmastodon.android.model.viewmodel.CardViewModel; import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.UiUtils; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; 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.ListImageLoaderAdapter; +import me.grishka.appkit.imageloader.ListImageLoaderWrapper; +import me.grishka.appkit.imageloader.RecyclerViewDelegate; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.MergeRecyclerAdapter; +import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class DiscoverNewsFragment extends BaseRecyclerFragment implements ScrollableToTop{ +public class DiscoverNewsFragment extends BaseRecyclerFragment implements ScrollableToTop{ private String accountID; - private List imageRequests=Collections.emptyList(); - private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS); + private DiscoverInfoBannerHelper bannerHelper; + private MergeRecyclerAdapter mergeAdapter; + private UsableRecyclerView cardsList; + private ArrayList top3=new ArrayList<>(); + private CardLinksAdapter cardsAdapter; public DiscoverNewsFragment(){ super(10); @@ -47,6 +59,7 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment implements public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); accountID=getArguments().getString("account"); + bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS, accountID); } @Override @@ -55,10 +68,14 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment implements .setCallback(new SimpleCallback<>(this){ @Override public void onSuccess(List result){ - imageRequests=result.stream() - .map(card->TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(150), V.dp(150))) - .collect(Collectors.toList()); - onDataLoaded(result, false); + top3.clear(); + top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140)).collect(Collectors.toList())); + cardsAdapter.notifyDataSetChanged(); + + onDataLoaded(result.subList(top3.size(), result.size()).stream() + .map(card->new CardViewModel(card, 56, 56)) + .collect(Collectors.toList()), false); + bannerHelper.onBannerBecameVisible(); } }) .exec(accountID); @@ -66,14 +83,27 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment implements @Override protected RecyclerView.Adapter getAdapter(){ - return new LinksAdapter(); - } + cardsList=new UsableRecyclerView(getActivity()); + cardsList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false)); + ListImageLoaderWrapper cardsImageLoader=new ListImageLoaderWrapper(getActivity(), cardsList, new RecyclerViewDelegate(cardsList), this); + cardsList.setAdapter(cardsAdapter=new CardLinksAdapter(cardsImageLoader, top3)); + cardsList.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(256))); + cardsList.setPadding(V.dp(16), V.dp(8), 0, 0); + cardsList.setClipToPadding(false); + cardsList.addItemDecoration(new RecyclerView.ItemDecoration(){ + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + outRect.right=V.dp(16); + } + }); + cardsList.setSelector(R.drawable.bg_rect_12dp_ripple); + cardsList.setDrawSelectorOnTop(true); - @Override - public void onViewCreated(View view, Bundle savedInstanceState){ - super.onViewCreated(view, savedInstanceState); - list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 0, 0)); - bannerHelper.maybeAddBanner(contentWrap); + mergeAdapter=new MergeRecyclerAdapter(); + bannerHelper.maybeAddBanner(list, mergeAdapter); + mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(cardsList)); + mergeAdapter.addAdapter(new LinksAdapter(imgLoader, data)); + return mergeAdapter; } @Override @@ -81,14 +111,17 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment implements smoothScrollRecyclerViewToTop(list); } - private class LinksAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter{ - public LinksAdapter(){ + private class LinksAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter{ + private final List data; + + public LinksAdapter(ListImageLoaderWrapper imgLoader, List data){ super(imgLoader); + this.data=data; } @NonNull @Override - public LinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ return new LinkViewHolder(); } @@ -98,46 +131,51 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment implements } @Override - public void onBindViewHolder(LinkViewHolder holder, int position){ - holder.bind(data.get(position)); + public void onBindViewHolder(BaseLinkViewHolder holder, int position){ + holder.bind(data.get(position).card); super.onBindViewHolder(holder, position); } @Override public int getImageCountForItem(int position){ - return imageRequests.get(position)==null ? 0 : 1; + return data.get(position).imageRequest==null ? 0 : 1; } @Override public ImageLoaderRequest getImageRequest(int position, int image){ - return imageRequests.get(position); + return data.get(position).imageRequest; } } - private class LinkViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{ - private final TextView name, title, subtitle; - private final ImageView photo; + private class CardLinksAdapter extends LinksAdapter{ + public CardLinksAdapter(ListImageLoaderWrapper imgLoader, List data){ + super(imgLoader, data); + } + + @NonNull + @Override + public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return new LinkCardViewHolder(); + } + } + + private class BaseLinkViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{ + protected final TextView name, title; + protected final ImageView photo; private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); private boolean didClear; - public LinkViewHolder(){ - super(getActivity(), R.layout.item_trending_link, list); + public BaseLinkViewHolder(int layout){ + super(getActivity(), layout, list); name=findViewById(R.id.name); title=findViewById(R.id.title); - subtitle=findViewById(R.id.subtitle); photo=findViewById(R.id.photo); - photo.setOutlineProvider(OutlineProviders.roundedRect(2)); - photo.setClipToOutline(true); } @Override public void onBind(Card item){ name.setText(item.providerName); title.setText(item.title); - int num=item.history.get(0).uses; - if(item.history.size()>1) - num+=item.history.get(1).uses; - subtitle.setText(getResources().getQuantityString(R.plurals.discussed_x_times, num, num)); crossfadeDrawable.setSize(item.width, item.height); crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder); crossfadeDrawable.setCrossfadeAlpha(0f); @@ -164,4 +202,20 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment implements UiUtils.launchWebBrowser(getActivity(), item.url); } } + + private class LinkViewHolder extends BaseLinkViewHolder{ + public LinkViewHolder(){ + super(R.layout.item_trending_link); + photo.setOutlineProvider(OutlineProviders.roundedRect(12)); + photo.setClipToOutline(true); + } + } + + private class LinkCardViewHolder extends BaseLinkViewHolder{ + public LinkCardViewHolder(){ + super(R.layout.item_trending_link_card); + itemView.setOutlineProvider(OutlineProviders.roundedRect(12)); + itemView.setClipToOutline(true); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java index ebf8b7620..8c96bca6a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java @@ -1,7 +1,6 @@ package org.joinmastodon.android.fragments.discover; import android.os.Bundle; -import android.view.View; import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses; import org.joinmastodon.android.fragments.StatusListFragment; @@ -10,10 +9,18 @@ import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import java.util.List; +import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.api.SimpleCallback; +import me.grishka.appkit.utils.MergeRecyclerAdapter; public class DiscoverPostsFragment extends StatusListFragment{ - private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS); + private DiscoverInfoBannerHelper bannerHelper; + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID); + } @Override protected void doLoadData(int offset, int count){ @@ -22,13 +29,16 @@ public class DiscoverPostsFragment extends StatusListFragment{ @Override public void onSuccess(List result){ onDataLoaded(result, !result.isEmpty()); + bannerHelper.onBannerBecameVisible(); } }).exec(accountID); } @Override - public void onViewCreated(View view, Bundle savedInstanceState){ - super.onViewCreated(view, savedInstanceState); - bannerHelper.maybeAddBanner(contentWrap); + protected RecyclerView.Adapter getAdapter(){ + MergeRecyclerAdapter adapter=new MergeRecyclerAdapter(); + bannerHelper.maybeAddBanner(list, adapter); + adapter.addAdapter(super.getAdapter()); + return adapter; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java index a1367c74a..7b197e9fb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java @@ -12,12 +12,21 @@ import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import java.util.List; +import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.api.SimpleCallback; +import me.grishka.appkit.utils.MergeRecyclerAdapter; public class LocalTimelineFragment extends StatusListFragment{ - private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE); + private DiscoverInfoBannerHelper bannerHelper; + private String maxID; + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID); + } + @Override protected void doLoadData(int offset, int count){ currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count) @@ -29,14 +38,17 @@ public class LocalTimelineFragment extends StatusListFragment{ boolean empty=result.isEmpty(); AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC); onDataLoaded(result, !empty); + bannerHelper.onBannerBecameVisible(); } }) .exec(accountID); } @Override - public void onViewCreated(View view, Bundle savedInstanceState){ - super.onViewCreated(view, savedInstanceState); - bannerHelper.maybeAddBanner(contentWrap); + protected RecyclerView.Adapter getAdapter(){ + MergeRecyclerAdapter adapter=new MergeRecyclerAdapter(); + bannerHelper.maybeAddBanner(list, adapter); + adapter.addAdapter(super.getAdapter()); + return adapter; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java index f9027d645..41294e32e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java @@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.discover; import android.app.Activity; import android.os.Build; import android.os.Bundle; -import android.text.TextUtils; import android.view.View; import android.view.inputmethod.InputMethodManager; @@ -22,7 +21,6 @@ import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.tabs.TabLayout; -import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; import org.joinmastodon.android.ui.utils.UiUtils; import org.parceler.Parcels; @@ -33,11 +31,9 @@ import java.util.List; import java.util.Objects; import java.util.stream.Collectors; -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.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.V; public class SearchFragment extends BaseStatusListFragment{ @@ -45,12 +41,8 @@ public class SearchFragment extends BaseStatusListFragment{ private List prevDisplayItems; private EnumSet currentFilter=EnumSet.allOf(SearchResult.Type.class); private List unfilteredResults=Collections.emptyList(); - private HideableSingleViewRecyclerAdapter headerAdapter; - private ProgressVisibilityListener progressVisibilityListener; private InputMethodManager imm; - private TabLayout tabLayout; - public SearchFragment(){ setLayout(R.layout.fragment_search); } @@ -60,6 +52,7 @@ public class SearchFragment extends BaseStatusListFragment{ super.onCreate(savedInstanceState); if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) setRetainInstance(true); + setEmptyText(R.string.no_search_results); loadData(); } @@ -118,51 +111,48 @@ public class SearchFragment extends BaseStatusListFragment{ @Override protected void doLoadData(int offset, int count){ - if(isInRecentMode()){ - AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{ - if(getActivity()==null) - return; - unfilteredResults=sr; - prevDisplayItems=new ArrayList<>(displayItems); - onDataLoaded(sr, false); - }); + GetSearchResults.Type type; + if(currentFilter.size()==1){ + type=switch(currentFilter.iterator().next()){ + case ACCOUNT -> GetSearchResults.Type.ACCOUNTS; + case HASHTAG -> GetSearchResults.Type.HASHTAGS; + case STATUS -> GetSearchResults.Type.STATUSES; + }; }else{ - progressVisibilityListener.onProgressVisibilityChanged(true); - currentRequest=new GetSearchResults(currentQuery, null, true) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(SearchResults result){ - ArrayList results=new ArrayList<>(); - if(result.accounts!=null){ - for(Account acc:result.accounts) - results.add(new SearchResult(acc)); - } - if(result.hashtags!=null){ - for(Hashtag tag:result.hashtags) - results.add(new SearchResult(tag)); - } - if(result.statuses!=null){ - for(Status status:result.statuses) - results.add(new SearchResult(status)); - } - prevDisplayItems=new ArrayList<>(displayItems); - unfilteredResults=results; - onDataLoaded(filterSearchResults(results), false); - } - - @Override - public void onError(ErrorResponse error){ - currentRequest=null; - Activity a=getActivity(); - if(a==null) - return; - error.showToast(a); - if(progressVisibilityListener!=null) - progressVisibilityListener.onProgressVisibilityChanged(false); - } - }) - .exec(accountID); + type=null; } + currentRequest=new GetSearchResults(currentQuery, type, true) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(SearchResults result){ + ArrayList results=new ArrayList<>(); + if(result.accounts!=null){ + for(Account acc:result.accounts) + results.add(new SearchResult(acc)); + } + if(result.hashtags!=null){ + for(Hashtag tag:result.hashtags) + results.add(new SearchResult(tag)); + } + if(result.statuses!=null){ + for(Status status:result.statuses) + results.add(new SearchResult(status)); + } + prevDisplayItems=new ArrayList<>(displayItems); + unfilteredResults=results; + onDataLoaded(filterSearchResults(results), false); + } + + @Override + public void onError(ErrorResponse error){ + currentRequest=null; + Activity a=getActivity(); + if(a==null) + return; + error.showToast(a); + } + }) + .exec(accountID); } @Override @@ -172,86 +162,30 @@ public class SearchFragment extends BaseStatusListFragment{ return; } UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType()); - boolean recent=isInRecentMode(); - if(recent!=headerAdapter.isVisible()) - headerAdapter.setVisible(recent); imgLoader.forceUpdateImages(); prevDisplayItems=null; } - @Override - protected void onDataLoaded(List d, boolean more){ - super.onDataLoaded(d, more); - if(progressVisibilityListener!=null) - progressVisibilityListener.onProgressVisibilityChanged(false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState){ - super.onViewCreated(view, savedInstanceState); - tabLayout=view.findViewById(R.id.tabbar); - tabLayout.setTabTextSize(V.dp(16)); - tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); - tabLayout.addTab(tabLayout.newTab().setText(R.string.search_all)); - tabLayout.addTab(tabLayout.newTab().setText(R.string.search_people)); - tabLayout.addTab(tabLayout.newTab().setText(R.string.hashtags)); - tabLayout.addTab(tabLayout.newTab().setText(R.string.posts)); - for(int i=0;i EnumSet.allOf(SearchResult.Type.class); - case 1 -> EnumSet.of(SearchResult.Type.ACCOUNT); - case 2 -> EnumSet.of(SearchResult.Type.HASHTAG); - case 3 -> EnumSet.of(SearchResult.Type.STATUS); - default -> throw new IllegalStateException("Unexpected value: "+tab.getPosition()); - }); - } - - @Override - public void onTabUnselected(TabLayout.Tab tab){ - - } - - @Override - public void onTabReselected(TabLayout.Tab tab){ - scrollToTop(); - } - }); - } - - @Override - protected RecyclerView.Adapter getAdapter(){ - View header=getActivity().getLayoutInflater().inflate(R.layout.item_recent_searches_header, list, false); - header.findViewById(R.id.clear).setOnClickListener(this::onClearRecentClick); - MergeRecyclerAdapter adapter=new MergeRecyclerAdapter(); - adapter.addAdapter(headerAdapter=new HideableSingleViewRecyclerAdapter(header)); - adapter.addAdapter(super.getAdapter()); - headerAdapter.setVisible(isInRecentMode()); - return adapter; - } - - public void setQuery(String q){ - if(Objects.equals(q, currentQuery) || q.isBlank()) + public void setQuery(String q, SearchResult.Type filter){ + if(q.isBlank()) return; if(currentRequest!=null){ currentRequest.cancel(); currentRequest=null; } currentQuery=q; + if(filter==null) + currentFilter=EnumSet.allOf(SearchResult.Type.class); + else + currentFilter=EnumSet.of(filter); refreshing=true; - doLoadData(0, 0); + loadData(); } private void setFilter(EnumSet filter){ if(filter.equals(currentFilter)) return; currentFilter=filter; - if(isInRecentMode()) - return; // This can be optimized by not rebuilding display items every time filter is changed, but I'm too lazy prevDisplayItems=new ArrayList<>(displayItems); refreshing=true; @@ -273,23 +207,6 @@ public class SearchFragment extends BaseStatusListFragment{ return null; } - private void onClearRecentClick(View v){ - AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches(); - if(isInRecentMode()){ - prevDisplayItems=new ArrayList<>(displayItems); - refreshing=true; - onDataLoaded(unfilteredResults=Collections.emptyList(), false); - } - } - - private boolean isInRecentMode(){ - return TextUtils.isEmpty(currentQuery); - } - - public void setProgressVisibilityListener(ProgressVisibilityListener progressVisibilityListener){ - this.progressVisibilityListener=progressVisibilityListener; - } - @Override public void onScrollStarted(){ super.onScrollStarted(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java new file mode 100644 index 000000000..85c13f36e --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java @@ -0,0 +1,554 @@ +package org.joinmastodon.android.fragments.discover; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.app.Fragment; +import android.graphics.Outline; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.RoundedCorner; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.WindowInsets; +import android.view.animation.AnimationUtils; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toolbar; + +import org.joinmastodon.android.MainActivity; +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.search.GetSearchResults; +import org.joinmastodon.android.api.session.AccountSessionManager; +import org.joinmastodon.android.fragments.MastodonRecyclerFragment; +import org.joinmastodon.android.model.Relationship; +import org.joinmastodon.android.model.SearchResult; +import org.joinmastodon.android.model.SearchResults; +import org.joinmastodon.android.model.viewmodel.ListItem; +import org.joinmastodon.android.model.viewmodel.SearchResultViewModel; +import org.joinmastodon.android.ui.DividerItemDecoration; +import org.joinmastodon.android.ui.SearchViewHelper; +import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter; +import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.viewholders.AccountViewHolder; +import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import me.grishka.appkit.Nav; +import me.grishka.appkit.api.SimpleCallback; +import me.grishka.appkit.fragments.CustomTransitionsFragment; +import me.grishka.appkit.fragments.OnBackPressedListener; +import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.utils.MergeRecyclerAdapter; +import me.grishka.appkit.utils.V; +import me.grishka.appkit.views.UsableRecyclerView; + +public class SearchQueryFragment extends MastodonRecyclerFragment implements CustomTransitionsFragment, OnBackPressedListener{ + private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE); + private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE); + + private MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter(); + private HideableSingleViewRecyclerAdapter recentsHeader; + private ListItem openUrlItem, goToHashtagItem, goToAccountItem, goToStatusSearchItem, goToAccountSearchItem; + private ArrayList> topOptions=new ArrayList<>(); + private GenericListItemsAdapter topOptionsAdapter; + + private String accountID; + private SearchViewHelper searchViewHelper; + private String currentQuery; + private LayerDrawable navigationIcon; + private Drawable searchIcon, backIcon; + + public SearchQueryFragment(){ + super(20); + } + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + accountID=getArguments().getString("account"); + setRefreshEnabled(false); + setEmptyText(""); + + openUrlItem=new ListItem<>(R.string.search_open_url, 0, R.drawable.ic_link_24px, this::onOpenURLClick); + goToHashtagItem=new ListItem<>("", null, R.drawable.ic_tag_24px, this::onGoToHashtagClick); + goToAccountItem=new ListItem<>("", null, R.drawable.ic_person_24px, this::onGoToAccountClick); + goToStatusSearchItem=new ListItem<>("", null, R.drawable.ic_search_24px, this::onGoToStatusSearchClick); + goToAccountSearchItem=new ListItem<>("", null, R.drawable.ic_group_24px, this::onGoToAccountSearchClick); + currentQuery=getArguments().getString("query"); + + dataLoaded(); + doLoadData(0, 0); + } + + @Override + protected void doLoadData(int offset, int count){ + if(isInRecentMode()){ + AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(results->{ + if(getActivity()==null) + return; + + onDataLoaded(results.stream().map(sr->{ + SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true); + if(sr.type==SearchResult.Type.HASHTAG){ + vm.hashtagItem.onClick=()->openHashtag(sr); + } + return vm; + }).collect(Collectors.toList()), false); + recentsHeader.setVisible(!data.isEmpty()); + }); + }else{ + currentRequest=new GetSearchResults(currentQuery, null, false) + .limit(2) + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(SearchResults result){ + onDataLoaded(Stream.of(result.hashtags.stream().map(SearchResult::new), result.accounts.stream().map(SearchResult::new)) + .flatMap(Function.identity()) + .map(sr->{ + SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false); + if(sr.type==SearchResult.Type.HASHTAG){ + vm.hashtagItem.onClick=()->openHashtag(sr); + } + return vm; + }) + .collect(Collectors.toList()), false); + recentsHeader.setVisible(false); + } + }) + .exec(accountID); + } + } + + @Override + protected RecyclerView.Adapter getAdapter(){ + View header=getActivity().getLayoutInflater().inflate(R.layout.display_item_section_header, list, false); + TextView title=header.findViewById(R.id.title); + Button action=header.findViewById(R.id.action_btn); + title.setText(R.string.recent_searches); + action.setText(R.string.clear_all); + action.setOnClickListener(v->onClearRecentClick()); + recentsHeader=new HideableSingleViewRecyclerAdapter(header); + + mergeAdapter.addAdapter(recentsHeader); + mergeAdapter.addAdapter(topOptionsAdapter=new GenericListItemsAdapter<>(topOptions)); + mergeAdapter.addAdapter(new SearchResultsAdapter()); + return mergeAdapter; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_mastodon)); + searchViewHelper.setListeners(this::onQueryChanged, this::onQueryChangedNoDebounce); + searchViewHelper.addDivider(contentView); + searchViewHelper.setEnterCallback(this::onSearchViewEnter); + + navigationIcon=new LayerDrawable(new Drawable[]{ + searchIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_search_24px, getToolbarContext().getTheme()).mutate(), + backIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_arrow_back, getToolbarContext().getTheme()).mutate() + }){ + @Override + public Drawable mutate(){ + return this; + } + }; + + super.onViewCreated(view, savedInstanceState); + + view.setBackgroundResource(R.drawable.bg_m3_surface3); + int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f); + setStatusBarColor(color); + setNavigationBarColor(color); + if(currentQuery!=null){ + searchViewHelper.setQuery(currentQuery); + searchIcon.setAlpha(0); + } + list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->!isInRecentMode() && vh.getAbsoluteAdapterPosition()==topOptions.size()-1)); + } + + @Override + protected void onUpdateToolbar(){ + super.onUpdateToolbar(); + ((ViewGroup.MarginLayoutParams)getToolbar().getLayoutParams()).topMargin=V.dp(8); + searchViewHelper.install(getToolbar()); + } + + @Override + protected boolean wantsElevationOnScrollEffect(){ + return false; + } + + private void onQueryChanged(String q){ + currentQuery=q; + if(currentRequest!=null){ + currentRequest.cancel(); + currentRequest=null; + } + refreshing=true; + doLoadData(0, 0); + } + + private void onQueryChangedNoDebounce(String q){ + updateTopOptions(q); + if(!TextUtils.isEmpty(q)){ + recentsHeader.setVisible(false); + } + data.clear(); + mergeAdapter.notifyDataSetChanged(); + } + + private void updateTopOptions(String q){ + topOptions.clear(); + // https://github.com/mastodon/mastodon/blob/a985d587e13494b78ef2879e4d97f78a2df693db/app/javascript/mastodon/features/compose/components/search.jsx#L233 + String trimmedValue=q.trim(); + if(trimmedValue.length()>0){ + boolean couldBeURL=trimmedValue.startsWith("https://") && !trimmedValue.contains(" "); + if(couldBeURL){ + topOptions.add(openUrlItem); + } + + boolean couldBeHashtag=(trimmedValue.startsWith("#") && trimmedValue.length()>1 && !trimmedValue.contains(" ")) || HASHTAG_REGEX.matcher(trimmedValue).find(); + if(couldBeHashtag){ + String tag=trimmedValue.startsWith("#") ? trimmedValue.substring(1) : trimmedValue; + goToHashtagItem.title=getString(R.string.posts_matching_hashtag, "#"+tag); + topOptions.add(goToHashtagItem); + } + + Matcher usernameMatcher=USERNAME_REGEX.matcher(trimmedValue); + if(usernameMatcher.find()){ + String username="@"+usernameMatcher.group(1); + String atDomain=usernameMatcher.group(2); + if(atDomain==null){ + username+="@"+AccountSessionManager.get(accountID).domain; + } + goToAccountItem.title=getString(R.string.search_go_to_account, username); + topOptions.add(goToAccountItem); + } + + goToStatusSearchItem.title=getString(R.string.posts_matching_string, trimmedValue); + topOptions.add(goToStatusSearchItem); + goToAccountSearchItem.title=getString(R.string.accounts_matching_string, trimmedValue); + topOptions.add(goToAccountSearchItem); + } + topOptionsAdapter.notifyDataSetChanged(); + } + + @Override + public Animator onCreateEnterTransition(View prev, View container){ + return createTransition(prev, container, true); + } + + @Override + public Animator onCreateExitTransition(View prev, View container){ + return createTransition(prev, container, false); + } + + @Override + public boolean wantsCustomNavigationIcon(){ + return true; + } + + @Override + protected Drawable getNavigationIconDrawable(){ + return navigationIcon; + } + + @Override + protected void onShown(){ + super.onShown(); + getActivity().getSystemService(InputMethodManager.class).showSoftInput(getActivity().getCurrentFocus(), 0); + } + + @Override + protected void onHidden(){ + super.onHidden(); + getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getActivity().getWindow().getDecorView().getWindowToken(), 0); + } + + private Animator createTransition(View prev, View container, boolean enter){ + int[] loc={0, 0}; + View searchBtn=prev.findViewById(R.id.search_wrap); + searchBtn.getLocationInWindow(loc); + int btnLeft=loc[0], btnTop=loc[1]; + container.getLocationInWindow(loc); + int offX=btnLeft-loc[0], offY=btnTop-loc[1]; + + float screenRadius; + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){ + WindowInsets insets=container.getRootWindowInsets(); + screenRadius=Math.min( + Math.min(insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT).getRadius(), insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT).getRadius()), + Math.min(insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT).getRadius(), insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT).getRadius()) + ); + }else{ + screenRadius=0; + } + float buttonRadius=V.dp(26); + + Rect buttonBounds=new Rect(offX, offY, offX+searchBtn.getWidth(), offY+searchBtn.getHeight()); + Rect containerBounds=new Rect(0, 0, container.getWidth(), container.getHeight()); + AnimatableOutlineProvider outlineProvider=new AnimatableOutlineProvider(enter ? buttonBounds : containerBounds, enter ? containerBounds : buttonBounds, enter ? buttonRadius : screenRadius); + container.setOutlineProvider(outlineProvider); + container.setClipToOutline(true); + + AnimatorSet set=new AnimatorSet(); + ObjectAnimator boundsAnim; + + Toolbar toolbar=getToolbar(); + float toolbarTX=offX-toolbar.getX(); + float toolbarTY=offY-toolbar.getY()+(searchBtn.getHeight()-toolbar.getHeight())/2f; + ArrayList anims=new ArrayList<>(); + anims.add(boundsAnim=ObjectAnimator.ofFloat(outlineProvider, "boundsFraction", 0f, 1f)); + anims.add(ObjectAnimator.ofFloat(outlineProvider, "radius", enter ? buttonRadius : screenRadius, enter ? screenRadius : buttonRadius)); + anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_X, enter ? toolbarTX : 0, enter ? 0 : toolbarTX)); + anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_Y, enter ? toolbarTY : 0, enter ? 0 : toolbarTY)); + anims.add(ObjectAnimator.ofFloat(searchViewHelper.getSearchLayout(), View.TRANSLATION_X, enter ? V.dp(-4) : 0, enter ? 0 : V.dp(-4))); + anims.add(ObjectAnimator.ofFloat(searchViewHelper.getDivider(), View.ALPHA, enter ? 0 : 1, enter ? 1 : 0)); + View parentContent=prev.findViewById(R.id.discover_content); + View parentContentParent=(View) parentContent.getParent(); + parentContentParent.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface)); + if(enter){ + anims.add(ObjectAnimator.ofFloat(contentWrap, View.TRANSLATION_Y, V.dp(-16), 0)); + }else{ + } + anims.add(ObjectAnimator.ofFloat(contentWrap, View.ALPHA, enter ? 0 : 1, enter ? 1 : 0)); + for(Animator anim:anims){ + anim.setDuration(enter ? 700 : 300); + } + if(TextUtils.isEmpty(currentQuery)){ + anims.add(ObjectAnimator.ofInt(searchIcon, "alpha", enter ? 255 : 0, enter ? 0 : 255).setDuration(200)); + anims.add(ObjectAnimator.ofInt(backIcon, "alpha", enter ? 0 : 255, enter ? 255 : 0).setDuration(200)); + } + ObjectAnimator parentContentFade; + anims.add(parentContentFade=ObjectAnimator.ofFloat(parentContent, View.ALPHA, enter ? 1 : 0, enter ? 0 : 1).setDuration(enter ? 350 : 250)); + if(!enter){ + parentContentFade.setStartDelay(50); + ObjectAnimator parentContentTY; + anims.add(parentContentTY=ObjectAnimator.ofFloat(parentContent, View.TRANSLATION_Y, V.dp(16), 0).setDuration(250)); + parentContentTY.setStartDelay(50); + } + + set.playTogether(anims); + set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_emphasized_decelerate)); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + container.setOutlineProvider(null); + container.setClipToOutline(false); + parentContentParent.setBackground(null); + } + }); + boundsAnim.addUpdateListener(animation->{ + container.invalidateOutline(); + navigationIcon.invalidateSelf(); + }); + return set; + } + + private void openHashtag(SearchResult res){ + UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name); + AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res); + } + + private boolean isInRecentMode(){ + return TextUtils.isEmpty(currentQuery); + } + + private void onSearchViewEnter(){ + deliverResult(currentQuery, null); + } + + private void onOpenURLClick(){ + ((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID); + } + + private void onGoToHashtagClick(){ + String q=searchViewHelper.getQuery(); + if(q.startsWith("#")) + q=q.substring(1); + UiUtils.openHashtagTimeline(getActivity(), accountID, q); + } + + private void onGoToAccountClick(){ + String q=searchViewHelper.getQuery(); + if(!q.startsWith("@")){ + q="@"+q; + } + if(q.lastIndexOf('@')==0){ + q+="@"+AccountSessionManager.get(accountID).domain; + } + ((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true); + } + + private void onGoToStatusSearchClick(){ + deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS); + } + + private void onGoToAccountSearchClick(){ + deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT); + } + + private void onClearRecentClick(){ + AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches(); + if(isInRecentMode()){ + data.clear(); + recentsHeader.setVisible(false); + mergeAdapter.notifyDataSetChanged(); + } + } + + private void deliverResult(String query, SearchResult.Type typeFilter){ + Bundle res=new Bundle(); + res.putString("query", query); + if(typeFilter!=null) + res.putInt("filter", typeFilter.ordinal()); + setResult(true, res); + Nav.finish(this); + } + + @Override + public boolean onBackPressed(){ + String initialQuery=getArguments().getString("query"); + searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery); + currentQuery=initialQuery; + return false; + } + + private static class AnimatableOutlineProvider extends ViewOutlineProvider{ + private float boundsFraction, radius; + private final Rect boundsFrom, boundsTo; + + private AnimatableOutlineProvider(Rect boundsFrom, Rect boundsTo, float radius){ + this.boundsFrom=boundsFrom; + this.boundsTo=boundsTo; + this.radius=radius; + } + + @Override + public void getOutline(View view, Outline outline){ + outline.setRoundRect( + UiUtils.lerp(boundsFrom.left, boundsTo.left, boundsFraction), + UiUtils.lerp(boundsFrom.top, boundsTo.top, boundsFraction), + UiUtils.lerp(boundsFrom.right, boundsTo.right, boundsFraction), + UiUtils.lerp(boundsFrom.bottom, boundsTo.bottom, boundsFraction), + radius + ); + } + + @Keep + public float getBoundsFraction(){ + return boundsFraction; + } + + @Keep + public void setBoundsFraction(float boundsFraction){ + this.boundsFraction=boundsFraction; + } + + @Keep + public float getRadius(){ + return radius; + } + + @Keep + public void setRadius(float radius){ + this.radius=radius; + } + } + + private class SearchResultsAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter{ + public SearchResultsAdapter(){ + super(imgLoader); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + if(viewType==R.id.list_item_account){ + return new CustomAccountViewHolder(SearchQueryFragment.this, parent, null); + }else if(viewType==R.id.list_item_simple){ + return new SimpleListItemViewHolder(parent.getContext(), parent); + } + throw new IllegalArgumentException(); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position){ + if(holder instanceof CustomAccountViewHolder avh){ + avh.bind(data.get(position).account); + avh.searchResult=data.get(position).result; + }else if(holder instanceof SimpleListItemViewHolder ivh){ + ivh.bind(data.get(position).hashtagItem); + } + } + + @Override + public int getItemCount(){ + return data.size(); + } + + @Override + public int getItemViewType(int position){ + return switch(data.get(position).result.type){ + case ACCOUNT -> R.id.list_item_account; + case HASHTAG -> R.id.list_item_simple; + default -> throw new IllegalStateException("Unexpected value: "+data.get(position).result.type); + }; + } + + @Override + public int getImageCountForItem(int position){ + SearchResultViewModel vm=data.get(position); + if(vm.account!=null) + return vm.account.emojiHelper.getImageCount()+1; + return 0; + } + + @Override + public ImageLoaderRequest getImageRequest(int position, int image){ + SearchResultViewModel vm=data.get(position); + if(vm.account!=null){ + if(image==0) + return vm.account.avaRequest; + return vm.account.emojiHelper.getImageRequest(image-1); + } + return null; + } + } + + private class CustomAccountViewHolder extends AccountViewHolder{ + public SearchResult searchResult; + + public CustomAccountViewHolder(Fragment fragment, ViewGroup list, HashMap relationships){ + super(fragment, list, relationships); + } + + @Override + public void onClick(){ + super.onClick(); + AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(searchResult); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/TrendingHashtagsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/TrendingHashtagsFragment.java index 9ded86b9f..0f0b47bfc 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/TrendingHashtagsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/TrendingHashtagsFragment.java @@ -25,7 +25,6 @@ import me.grishka.appkit.views.UsableRecyclerView; public class TrendingHashtagsFragment extends BaseRecyclerFragment implements ScrollableToTop{ private String accountID; - private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS); public TrendingHashtagsFragment(){ super(10); @@ -54,13 +53,6 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment impl return new HashtagsAdapter(); } - @Override - public void onViewCreated(View view, Bundle savedInstanceState){ - super.onViewCreated(view, savedInstanceState); - list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, .5f, 16, 16)); - bannerHelper.maybeAddBanner(contentWrap); - } - @Override public void scrollToTop(){ smoothScrollRecyclerViewToTop(list); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDebugFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDebugFragment.java index 9176908db..0b666705e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDebugFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDebugFragment.java @@ -1,13 +1,17 @@ package org.joinmastodon.android.fragments.settings; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; +import org.joinmastodon.android.MastodonApp; 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.fragments.HomeFragment; import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment; import org.joinmastodon.android.model.viewmodel.ListItem; +import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.updater.GithubSelfUpdater; import java.util.List; @@ -23,7 +27,8 @@ public class SettingsDebugFragment extends BaseSettingsFragment{ onDataLoaded(List.of( new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick), selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick), - resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick) + resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick), + new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick) )); if(!GithubSelfUpdater.needSelfUpdating()){ resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false; @@ -55,6 +60,11 @@ public class SettingsDebugFragment extends BaseSettingsFragment{ restartUI(); } + private void onResetDiscoverBannersClick(){ + DiscoverInfoBannerHelper.reset(); + restartUI(); + } + private void restartUI(){ Bundle args=new Bundle(); args.putString("account", accountID); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerFragment.java index 4ff696aa1..f89144158 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerFragment.java @@ -90,7 +90,7 @@ public class SettingsServerFragment extends AppKitFragment{ tabBar=view.findViewById(R.id.tabbar); tabBar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary)); - tabBar.setTabTextSize(V.dp(16)); + tabBar.setTabTextSize(V.dp(14)); tabLayoutMediator=new TabLayoutMediator(tabBar, pager, (tab, position)->tab.setText(switch(position){ case 0 -> R.string.about_server; case 1 -> R.string.server_rules; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/SearchResult.java b/mastodon/src/main/java/org/joinmastodon/android/model/SearchResult.java index 42f901416..6e4f4a2ed 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/SearchResult.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/SearchResult.java @@ -2,6 +2,7 @@ package org.joinmastodon.android.model; import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.RequiredField; +import org.joinmastodon.android.model.viewmodel.AccountViewModel; public class SearchResult extends BaseModel implements DisplayItemsParent{ public Account account; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CardViewModel.java b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CardViewModel.java new file mode 100644 index 000000000..18286683a --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CardViewModel.java @@ -0,0 +1,19 @@ +package org.joinmastodon.android.model.viewmodel; + +import android.text.TextUtils; + +import org.joinmastodon.android.model.Card; + +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.V; + +public class CardViewModel{ + public final Card card; + public final ImageLoaderRequest imageRequest; + + public CardViewModel(Card card, int width, int height){ + this.card=card; + this.imageRequest=TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(width), V.dp(height)); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/SearchResultViewModel.java b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/SearchResultViewModel.java new file mode 100644 index 000000000..4bb8e469e --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/SearchResultViewModel.java @@ -0,0 +1,24 @@ +package org.joinmastodon.android.model.viewmodel; + +import android.content.Context; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.model.Hashtag; +import org.joinmastodon.android.model.SearchResult; + +public class SearchResultViewModel{ + public SearchResult result; + public AccountViewModel account; + public ListItem hashtagItem; + + public SearchResultViewModel(SearchResult result, String accountID, boolean isRecents){ + this.result=result; + switch(result.type){ + case ACCOUNT -> account=new AccountViewModel(result.account, accountID); + case HASHTAG -> { + hashtagItem=new ListItem<>((isRecents ? "#" : "")+result.hashtag.name, null, isRecents ? R.drawable.ic_history_24px : R.drawable.ic_tag_24px, null, result.hashtag); + hashtagItem.isEnabled=true; + } + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/SearchViewHelper.java b/mastodon/src/main/java/org/joinmastodon/android/ui/SearchViewHelper.java new file mode 100644 index 000000000..3a6a85c42 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/SearchViewHelper.java @@ -0,0 +1,129 @@ +package org.joinmastodon.android.ui; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.text.InputType; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.Toolbar; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.ui.utils.SimpleTextWatcher; +import org.joinmastodon.android.ui.utils.UiUtils; + +import java.util.function.Consumer; + +import me.grishka.appkit.utils.V; + +public class SearchViewHelper{ + private LinearLayout searchLayout; + private EditText searchEdit; + private ImageButton clearSearchButton; + private View divider; + private String currentQuery; + private Consumer listener; + private Runnable debouncer=()->{ + currentQuery=searchEdit.getText().toString(); + if(listener!=null){ + listener.accept(currentQuery); + } + }; + private boolean isEmpty=true; + private Runnable enterCallback; + private Consumer listenerWithoutDebounce; + + public SearchViewHelper(Context context, Context toolbarContext, String hint){ + searchLayout=new LinearLayout(context); + searchLayout.setOrientation(LinearLayout.HORIZONTAL); + + searchEdit=new EditText(context); + searchEdit.setHint(hint); + searchEdit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER); + searchEdit.setBackground(null); + searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{ + searchEdit.removeCallbacks(debouncer); + searchEdit.postDelayed(debouncer, 300); + boolean newIsEmpty=e.length()==0; + if(isEmpty!=newIsEmpty){ + isEmpty=newIsEmpty; + V.setVisibilityAnimated(clearSearchButton, isEmpty ? View.INVISIBLE : View.VISIBLE); + } + if(listenerWithoutDebounce!=null) + listenerWithoutDebounce.accept(e.toString()); + })); + searchEdit.setImeOptions(EditorInfo.IME_ACTION_SEARCH); + searchEdit.setOnEditorActionListener((v, actionId, event)->{ + searchEdit.removeCallbacks(debouncer); + debouncer.run(); + if(enterCallback!=null) + enterCallback.run(); + return true; + }); + searchEdit.setTextAppearance(R.style.m3_body_large); + searchEdit.setHintTextColor(UiUtils.getThemeColor(toolbarContext, R.attr.colorM3OnSurfaceVariant)); + searchEdit.setTextColor(UiUtils.getThemeColor(toolbarContext, R.attr.colorM3OnSurface)); + searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)); + + clearSearchButton=new ImageButton(context); + clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24); + clearSearchButton.setContentDescription(context.getString(R.string.clear)); + clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant))); + clearSearchButton.setBackground(UiUtils.getThemeDrawable(toolbarContext, android.R.attr.actionBarItemBackground)); + clearSearchButton.setOnClickListener(v->{ + searchEdit.setText(""); + searchEdit.removeCallbacks(debouncer); + debouncer.run(); + }); + clearSearchButton.setVisibility(View.INVISIBLE); + searchLayout.addView(clearSearchButton, new LinearLayout.LayoutParams(V.dp(56), ViewGroup.LayoutParams.MATCH_PARENT)); + } + + public void setListeners(Consumer listener, Consumer listenerWithoutDebounce){ + this.listener=listener; + this.listenerWithoutDebounce=listenerWithoutDebounce; + } + + public void install(Toolbar toolbar){ + toolbar.getLayoutParams().height=V.dp(72); + toolbar.setMinimumHeight(V.dp(72)); + if(searchLayout.getParent()!=null) + ((ViewGroup) searchLayout.getParent()).removeView(searchLayout); + toolbar.addView(searchLayout, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + toolbar.setBackgroundResource(R.drawable.bg_m3_surface3); + searchEdit.requestFocus(); + } + + public void addDivider(ViewGroup contentView){ + divider=new View(contentView.getContext()); + divider.setBackgroundColor(UiUtils.getThemeColor(contentView.getContext(), R.attr.colorM3Outline)); + contentView.addView(divider, 1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(1))); + } + + public LinearLayout getSearchLayout(){ + return searchLayout; + } + + public void setEnterCallback(Runnable enterCallback){ + this.enterCallback=enterCallback; + } + + public void setQuery(String q){ + currentQuery=q; + searchEdit.setText(currentQuery); + searchEdit.setSelection(searchEdit.length()); + searchEdit.removeCallbacks(debouncer); + } + + public String getQuery(){ + return currentQuery; + } + + public View getDivider(){ + return divider; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java index 60b222171..eaa83b823 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java @@ -4,61 +4,79 @@ import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.TextView; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.session.AccountSessionManager; -import me.grishka.appkit.utils.CubicBezierInterpolator; +import java.util.EnumSet; + +import androidx.recyclerview.widget.RecyclerView; +import me.grishka.appkit.utils.MergeRecyclerAdapter; +import me.grishka.appkit.utils.SingleViewRecyclerAdapter; public class DiscoverInfoBannerHelper{ private View banner; private final BannerType type; + private final String accountID; + private static EnumSet bannerTypesToShow=EnumSet.noneOf(BannerType.class); - public DiscoverInfoBannerHelper(BannerType type){ - this.type=type; - } - - private SharedPreferences getPrefs(){ - return MastodonApp.context.getSharedPreferences("onboarding", Context.MODE_PRIVATE); - } - - public void maybeAddBanner(FrameLayout view){ - if(!getPrefs().getBoolean("bannerHidden_"+type, false)){ - ((Activity)view.getContext()).getLayoutInflater().inflate(R.layout.discover_info_banner, view); - banner=view.findViewById(R.id.discover_info_banner); - view.findViewById(R.id.banner_dismiss).setOnClickListener(this::onDismissClick); - TextView text=view.findViewById(R.id.banner_text); - text.setText(switch(type){ - case TRENDING_POSTS -> R.string.trending_posts_info_banner; - case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner; - case TRENDING_LINKS -> R.string.trending_links_info_banner; - case LOCAL_TIMELINE -> R.string.local_timeline_info_banner; - }); + static{ + for(BannerType t:BannerType.values()){ + if(!getPrefs().getBoolean("bannerHidden_"+t, false)) + bannerTypesToShow.add(t); } } - private void onDismissClick(View v){ - if(banner==null) - return; - View _banner=banner; - banner.animate() - .alpha(0) - .setDuration(200) - .setInterpolator(CubicBezierInterpolator.DEFAULT) - .withEndAction(()->((ViewGroup)_banner.getParent()).removeView(_banner)) - .start(); + public DiscoverInfoBannerHelper(BannerType type, String accountID){ + this.type=type; + this.accountID=accountID; + } + + private static SharedPreferences getPrefs(){ + return MastodonApp.context.getSharedPreferences("onboarding", Context.MODE_PRIVATE); + } + + public void maybeAddBanner(RecyclerView list, MergeRecyclerAdapter adapter){ + if(bannerTypesToShow.contains(type)){ + banner=((Activity)list.getContext()).getLayoutInflater().inflate(R.layout.discover_info_banner, list, false); + TextView text=banner.findViewById(R.id.banner_text); + text.setText(switch(type){ + case TRENDING_POSTS -> list.getResources().getString(R.string.trending_posts_info_banner); + case TRENDING_LINKS -> list.getResources().getString(R.string.trending_links_info_banner); + case LOCAL_TIMELINE -> list.getResources().getString(R.string.local_timeline_info_banner, AccountSessionManager.get(accountID).domain); + case ACCOUNTS -> list.getResources().getString(R.string.recommended_accounts_info_banner); + }); + ImageView icon=banner.findViewById(R.id.icon); + icon.setImageResource(switch(type){ + case TRENDING_POSTS -> R.drawable.ic_whatshot_24px; + case TRENDING_LINKS -> R.drawable.ic_feed_24px; + case LOCAL_TIMELINE -> R.drawable.ic_stream_24px; + case ACCOUNTS -> R.drawable.ic_group_add_24px; + }); + adapter.addAdapter(new SingleViewRecyclerAdapter(banner)); + } + } + + public void onBannerBecameVisible(){ getPrefs().edit().putBoolean("bannerHidden_"+type, true).apply(); - banner=null; + // bannerTypesToShow is not updated here on purpose so the banner keeps showing until the app is relaunched + } + + public static void reset(){ + SharedPreferences prefs=getPrefs(); + SharedPreferences.Editor e=prefs.edit(); + prefs.getAll().keySet().stream().filter(k->k.startsWith("bannerHidden_")).forEach(e::remove); + e.apply(); + bannerTypesToShow=EnumSet.allOf(BannerType.class); } public enum BannerType{ TRENDING_POSTS, - TRENDING_HASHTAGS, TRENDING_LINKS, LOCAL_TIMELINE, -// ACCOUNTS + ACCOUNTS } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java index 6dca58cee..cff88a684 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java @@ -162,6 +162,8 @@ public class AccountViewHolder extends BindableViewHolder impl @Override public boolean onLongClick(float x, float y){ + if(relationships==null) + return false; Relationship relationship=relationships.get(item.account.id); if(relationship==null) return false; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/HashtagChartView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/HashtagChartView.java index 3087ee63f..335db41e3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/HashtagChartView.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/HashtagChartView.java @@ -32,7 +32,7 @@ public class HashtagChartView extends View implements CustomViewHelper{ public HashtagChartView(Context context, AttributeSet attrs, int defStyle){ super(context, attrs, defStyle); - paint.setStrokeWidth(dp(1.71f)); + paint.setStrokeWidth(dp(1)); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStrokeJoin(Paint.Join.ROUND); } diff --git a/mastodon/src/main/res/drawable/bg_rect_12dp_ripple.xml b/mastodon/src/main/res/drawable/bg_rect_12dp_ripple.xml new file mode 100644 index 000000000..f62baf8d3 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_rect_12dp_ripple.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_feed_24px.xml b/mastodon/src/main/res/drawable/ic_feed_24px.xml new file mode 100644 index 000000000..e1610f757 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_feed_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_group_add_24px.xml b/mastodon/src/main/res/drawable/ic_group_add_24px.xml new file mode 100644 index 000000000..532b60ced --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_group_add_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_history_24px.xml b/mastodon/src/main/res/drawable/ic_history_24px.xml new file mode 100644 index 000000000..3afbd448c --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_history_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_link_24px.xml b/mastodon/src/main/res/drawable/ic_link_24px.xml new file mode 100644 index 000000000..af8304e40 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_link_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_person_24px.xml b/mastodon/src/main/res/drawable/ic_person_24px.xml new file mode 100644 index 000000000..87f358efb --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_person_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_stream_24px.xml b/mastodon/src/main/res/drawable/ic_stream_24px.xml new file mode 100644 index 000000000..6a519c07d --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_stream_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_tag_24px.xml b/mastodon/src/main/res/drawable/ic_tag_24px.xml new file mode 100644 index 000000000..f4f0e39a0 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_tag_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_whatshot_24px.xml b/mastodon/src/main/res/drawable/ic_whatshot_24px.xml new file mode 100644 index 000000000..5886a00bd --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_whatshot_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/bg_search_field.xml b/mastodon/src/main/res/drawable/rect_12dp.xml similarity index 59% rename from mastodon/src/main/res/drawable/bg_search_field.xml rename to mastodon/src/main/res/drawable/rect_12dp.xml index 1b9ae5eb8..b7e237576 100644 --- a/mastodon/src/main/res/drawable/bg_search_field.xml +++ b/mastodon/src/main/res/drawable/rect_12dp.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/discover_info_banner.xml b/mastodon/src/main/res/layout/discover_info_banner.xml index 9f9939937..d0d8249b5 100644 --- a/mastodon/src/main/res/layout/discover_info_banner.xml +++ b/mastodon/src/main/res/layout/discover_info_banner.xml @@ -5,30 +5,27 @@ android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="top" - android:elevation="1dp" - android:outlineProvider="background" - android:background="?colorWindowBackground"> + android:layout_margin="16dp" + android:padding="16dp" + android:background="@drawable/rect_12dp" + android:backgroundTint="?colorM3SurfaceVariant"> + + - - \ No newline at end of file diff --git a/mastodon/src/main/res/layout/fragment_discover.xml b/mastodon/src/main/res/layout/fragment_discover.xml index 7ea500d7d..90dd462d3 100644 --- a/mastodon/src/main/res/layout/fragment_discover.xml +++ b/mastodon/src/main/res/layout/fragment_discover.xml @@ -9,106 +9,74 @@ - - - - - + - - + android:layout_height="56dp" + android:layout_margin="16dp" + android:orientation="horizontal" + android:background="@drawable/bg_m3_surface3"> + android:background="@drawable/bg_round_ripple" + android:tint="?colorM3OnSurfaceVariant" + android:src="@drawable/ic_search_24px"/> - - - - - + + - + android:layout_height="match_parent" + android:orientation="vertical"> + - + - + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/fragment_search.xml b/mastodon/src/main/res/layout/fragment_search.xml index 81e491e3f..31b6e19fc 100644 --- a/mastodon/src/main/res/layout/fragment_search.xml +++ b/mastodon/src/main/res/layout/fragment_search.xml @@ -8,18 +8,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:background="?android:windowBackground"> - - + android:paddingVertical="12dp" + android:paddingStart="16dp" + android:paddingEnd="24dp"> - - \ No newline at end of file diff --git a/mastodon/src/main/res/layout/item_trending_link_card.xml b/mastodon/src/main/res/layout/item_trending_link_card.xml new file mode 100644 index 000000000..c829b19c5 --- /dev/null +++ b/mastodon/src/main/res/layout/item_trending_link_card.xml @@ -0,0 +1,44 @@ + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/ids.xml b/mastodon/src/main/res/values/ids.xml index 1000a0b50..dc2ec96d2 100644 --- a/mastodon/src/main/res/values/ids.xml +++ b/mastodon/src/main/res/values/ids.xml @@ -22,6 +22,7 @@ + diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index cf32bb3e2..f1b333b22 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -224,7 +224,7 @@ Only people mentioned All People - Recent searches + Recents Step %1$d of %2$d Skip New followers @@ -307,11 +307,12 @@ File saved Downloading… There’s no app to handle this action - Community - These are the posts gaining traction in your corner of Mastodon. - These are the hashtags gaining traction in your corner of Mastodon. - These are the news stories being shared the most in your corner of Mastodon. - These are the most recent posts by the people who use the same Mastodon server as you. + Local + These are the posts gaining traction across Mastodon. + These are the news stories getting talked about on Mastodon. + + These are all the posts from all users in your server (%s). + You might like these accounts based on others you follow. Dismiss See new posts Load missing posts @@ -633,4 +634,11 @@ Downloading (%d%%) Matches filter “%s” + Search Mastodon + Clear all + Open URL in Mastodon + Posts with “%s” + Go to %s + Posts with “%s” + People with “%s” \ No newline at end of file diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml index ca0096527..da8827d2f 100644 --- a/mastodon/src/main/res/values/styles.xml +++ b/mastodon/src/main/res/values/styles.xml @@ -440,6 +440,7 @@ 16dp ?android:textColorPrimary 5dp + 24dp