From c60bc253e549b1659028e44ffa4e781e61c32359 Mon Sep 17 00:00:00 2001 From: Grishka Date: Thu, 31 Mar 2022 17:49:54 +0300 Subject: [PATCH] Search now actually searches, yay --- .../android/api/CacheController.java | 127 +++++--- .../api/requests/search/GetSearchResults.java | 4 +- .../fragments/BaseStatusListFragment.java | 16 +- .../android/fragments/HomeFragment.java | 2 + .../android/fragments/ScrollableToTop.java | 26 ++ .../discover/DiscoverAccountsFragment.java | 8 +- .../fragments/discover/DiscoverFragment.java | 133 +++++++- .../discover/DiscoverNewsFragment.java | 8 +- .../fragments/discover/SearchFragment.java | 291 ++++++++++++++++++ .../discover/TrendingHashtagsFragment.java | 8 +- .../android/model/SearchResult.java | 64 ++++ .../ui/ComposeAutocompleteViewController.java | 38 +-- .../AccountStatusDisplayItem.java | 92 ++++++ .../HashtagStatusDisplayItem.java | 48 +++ .../ui/displayitems/StatusDisplayItem.java | 10 +- .../android/ui/utils/CustomEmojiHelper.java | 2 +- .../HideableSingleViewRecyclerAdapter.java | 32 ++ .../android/ui/utils/UiUtils.java | 38 +++ .../android/ui/views/HashtagChartView.java | 2 +- .../main/res/drawable/edit_text_border.xml | 4 +- .../ic_fluent_dismiss_circle_24_filled.xml | 3 + .../main/res/layout/display_item_account.xml | 42 +++ .../src/main/res/layout/fragment_discover.xml | 85 +++-- .../src/main/res/layout/fragment_search.xml | 43 +++ .../layout/item_recent_searches_header.xml | 29 ++ mastodon/src/main/res/values/strings.xml | 3 + mastodon/src/main/res/values/styles.xml | 10 +- 27 files changed, 1046 insertions(+), 122 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/model/SearchResult.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountStatusDisplayItem.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HashtagStatusDisplayItem.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/utils/HideableSingleViewRecyclerAdapter.java create mode 100644 mastodon/src/main/res/drawable/ic_fluent_dismiss_circle_24_filled.xml create mode 100644 mastodon/src/main/res/layout/display_item_account.xml create mode 100644 mastodon/src/main/res/layout/fragment_search.xml create mode 100644 mastodon/src/main/res/layout/item_recent_searches_header.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java index 7a4a7f88..aa7cf511 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java @@ -14,13 +14,17 @@ import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.api.requests.notifications.GetNotifications; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.Status; import java.io.IOException; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import java.util.function.Consumer; import androidx.annotation.Nullable; import me.grishka.appkit.api.Callback; @@ -29,7 +33,7 @@ import me.grishka.appkit.utils.WorkerThread; public class CacheController{ private static final String TAG="CacheController"; - private static final int DB_VERSION=1; + private static final int DB_VERSION=2; private static final WorkerThread databaseThread=new WorkerThread("databaseThread"); private static final Handler uiHandler=new Handler(Looper.getMainLooper()); @@ -91,24 +95,16 @@ public class CacheController{ } private void putHomeTimeline(List posts, boolean clear){ - cancelDelayedClose(); - databaseThread.postRunnable(()->{ - try{ - SQLiteDatabase db=getOrOpenDatabase(); - if(clear) - db.delete("home_timeline", null, null); - ContentValues values=new ContentValues(2); - for(Status s:posts){ - values.put("id", s.id); - values.put("json", MastodonAPIController.gson.toJson(s)); - db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE); - } - }catch(SQLiteException x){ - Log.w(TAG, x); - }finally{ - closeDelayed(); + runOnDbThread((db)->{ + if(clear) + db.delete("home_timeline", null, null); + ContentValues values=new ContentValues(2); + for(Status s:posts){ + values.put("id", s.id); + values.put("json", MastodonAPIController.gson.toJson(s)); + db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE); } - }, 0); + }); } public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback> callback){ @@ -157,26 +153,46 @@ public class CacheController{ } private void putNotifications(List notifications, boolean onlyMentions, boolean clear){ - cancelDelayedClose(); - databaseThread.postRunnable(()->{ - try{ - SQLiteDatabase db=getOrOpenDatabase(); - String table=onlyMentions ? "notifications_mentions" : "notifications_all"; - if(clear) - db.delete(table, null, null); - ContentValues values=new ContentValues(3); - for(Notification n:notifications){ - values.put("id", n.id); - values.put("json", MastodonAPIController.gson.toJson(n)); - values.put("type", n.type.ordinal()); - db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); - } - }catch(SQLiteException x){ - Log.w(TAG, x); - }finally{ - closeDelayed(); + runOnDbThread((db)->{ + String table=onlyMentions ? "notifications_mentions" : "notifications_all"; + if(clear) + db.delete(table, null, null); + ContentValues values=new ContentValues(3); + for(Notification n:notifications){ + values.put("id", n.id); + values.put("json", MastodonAPIController.gson.toJson(n)); + values.put("type", n.type.ordinal()); + db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); } - }, 0); + }); + } + + public void getRecentSearches(Consumer> callback){ + runOnDbThread((db)->{ + try(Cursor cursor=db.query("recent_searches", new String[]{"json"}, null, null, null, null, "time DESC")){ + List results=new ArrayList<>(); + while(cursor.moveToNext()){ + SearchResult result=MastodonAPIController.gson.fromJson(cursor.getString(0), SearchResult.class); + result.postprocess(); + results.add(result); + } + uiHandler.post(()->callback.accept(results)); + } + }); + } + + public void putRecentSearch(SearchResult result){ + runOnDbThread((db)->{ + ContentValues values=new ContentValues(4); + values.put("id", result.getID()); + values.put("json", MastodonAPIController.gson.toJson(result)); + values.put("time", (int)(System.currentTimeMillis()/1000)); + db.insertWithOnConflict("recent_searches", null, values, SQLiteDatabase.CONFLICT_REPLACE); + }); + } + + public void clearRecentSearches(){ + runOnDbThread((db)->db.delete("recent_searches", null, null)); } private void closeDelayed(){ @@ -204,6 +220,26 @@ public class CacheController{ return db.getWritableDatabase(); } + private void runOnDbThread(DatabaseRunnable r){ + runOnDbThread(r, null); + } + + private void runOnDbThread(DatabaseRunnable r, Consumer onError){ + cancelDelayedClose(); + databaseThread.postRunnable(()->{ + try{ + SQLiteDatabase db=getOrOpenDatabase(); + r.run(db); + }catch(SQLiteException|IOException x){ + Log.w(TAG, x); + if(onError!=null) + onError.accept(x); + }finally{ + closeDelayed(); + } + }, 0); + } + private class DatabaseHelper extends SQLiteOpenHelper{ public DatabaseHelper(){ @@ -232,11 +268,28 @@ public class CacheController{ `flags` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL )"""); + createRecentSearchesTable(db); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ + if(oldVersion==1){ + createRecentSearchesTable(db); + } + } + private void createRecentSearchesTable(SQLiteDatabase db){ + db.execSQL(""" + CREATE TABLE `recent_searches` ( + `id` VARCHAR(50) NOT NULL PRIMARY KEY, + `json` TEXT NOT NULL, + `time` INTEGER NOT NULL + )"""); } } + + @FunctionalInterface + private interface DatabaseRunnable{ + void run(SQLiteDatabase db) throws IOException; + } } 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 edec0727..a3574598 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 @@ -4,11 +4,13 @@ import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.SearchResults; public class GetSearchResults extends MastodonAPIRequest{ - public GetSearchResults(String query, Type type){ + public GetSearchResults(String query, Type type, boolean resolve){ super(HttpMethod.GET, "/search", SearchResults.class); addQueryParameter("q", query); if(type!=null) addQueryParameter("type", type.name().toLowerCase()); + if(resolve) + addQueryParameter("resolve", "true"); } @Override 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 ac844726..c1be0027 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -11,7 +11,6 @@ import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.widget.Toolbar; import org.joinmastodon.android.R; @@ -586,20 +585,7 @@ public abstract class BaseStatusListFragment exten @Override public void scrollToTop(){ - if(list.getChildCount()>0 && list.getChildAdapterPosition(list.getChildAt(0))>10){ - list.scrollToPosition(0); - list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ - @Override - public boolean onPreDraw(){ - list.getViewTreeObserver().removeOnPreDrawListener(this); - list.scrollBy(0, V.dp(300)); - list.smoothScrollToPosition(0); - return true; - } - }); - }else{ - list.smoothScrollToPosition(0); - } + smoothScrollRecyclerViewToTop(list); } protected int getListWidthForMediaLayout(){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index 4551d054..67b290f3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -183,6 +183,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene public boolean onBackPressed(){ if(currentTab==R.id.tab_profile) return profileFragment.onBackPressed(); + if(currentTab==R.id.tab_search) + return searchFragment.onBackPressed(); return false; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ScrollableToTop.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ScrollableToTop.java index f5e753f9..705c5d9f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ScrollableToTop.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ScrollableToTop.java @@ -1,5 +1,31 @@ package org.joinmastodon.android.fragments; +import android.view.ViewTreeObserver; + +import androidx.recyclerview.widget.RecyclerView; +import me.grishka.appkit.utils.V; + public interface ScrollableToTop{ void scrollToTop(); + + /** + * Utility method to scroll a RecyclerView to top in a way that doesn't suck + * @param list + */ + default void smoothScrollRecyclerViewToTop(RecyclerView list){ + if(list.getChildCount()>0 && list.getChildAdapterPosition(list.getChildAt(0))>10){ + list.scrollToPosition(0); + list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ + @Override + public boolean onPreDraw(){ + list.getViewTreeObserver().removeOnPreDrawListener(this); + list.scrollBy(0, V.dp(300)); + list.smoothScrollToPosition(0); + return true; + } + }); + }else{ + list.smoothScrollToPosition(0); + } + } } 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 296f5257..9eff4f0a 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 @@ -16,6 +16,7 @@ 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.model.FollowSuggestion; import org.joinmastodon.android.model.Relationship; @@ -47,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class DiscoverAccountsFragment extends BaseRecyclerFragment{ +public class DiscoverAccountsFragment extends BaseRecyclerFragment implements ScrollableToTop{ private String accountID; private Map relationships=Collections.emptyMap(); private GetAccountRelationships relationshipsRequest; @@ -120,6 +121,11 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment implements ImageLoaderRecyclerAdapter{ public AccountsAdapter(){ 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 654984f8..4d834ace 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,11 +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.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; @@ -22,21 +30,29 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.widget.ViewPager2; 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{ +public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{ 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 DiscoverPostsFragment postsFragment; private TrendingHashtagsFragment hashtagsFragment; private DiscoverNewsFragment newsFragment; private DiscoverAccountsFragment accountsFragment; + private SearchFragment searchFragment; private String accountID; + private Runnable searchDebouncer=this::onSearchChangedDebounced; @Override public void onCreate(Bundle savedInstanceState){ @@ -129,12 +145,68 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{ }); tabLayoutMediator.attach(); + 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); + searchBack.setEnabled(searchActive); + searchBack.setOnClickListener(v->exitSearch()); + if(searchActive){ + searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular); + pager.setVisibility(View.GONE); + tabLayout.setVisibility(View.GONE); + searchView.setVisibility(View.VISIBLE); + } + searchClear.setOnClickListener(v->{ + searchEdit.setText(""); + searchEdit.removeCallbacks(searchDebouncer); + onSearchChangedDebounced(); + }); + return view; } @Override public void scrollToTop(){ - + if(!searchActive){ + ((ScrollableToTop)getFragmentForPage(pager.getCurrentItem())).scrollToTop(); + }else{ + searchFragment.scrollToTop(); + } } public void loadData(){ @@ -142,6 +214,35 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{ postsFragment.loadData(); } + private void onSearchEditFocusChanged(View v, boolean hasFocus){ + if(!searchActive && hasFocus){ + 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.setEnabled(true); + } + } + + private void exitSearch(){ + 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); + searchBack.setEnabled(false); + getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0); + } + + @Override + protected void onHidden(){ + super.onHidden(); + getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0); + } + private Fragment getFragmentForPage(int page){ return switch(page){ case 0 -> postsFragment; @@ -152,6 +253,34 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{ }; } + @Override + public boolean onBackPressed(){ + if(searchActive){ + exitSearch(); + return true; + } + 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); + } + private class DiscoverPagerAdapter extends RecyclerView.Adapter{ @NonNull @Override 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 974aebdd..494d007e 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 @@ -10,6 +10,7 @@ import android.widget.TextView; 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.ui.DividerItemDecoration; import org.joinmastodon.android.ui.OutlineProviders; @@ -32,7 +33,7 @@ import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class DiscoverNewsFragment extends BaseRecyclerFragment{ +public class DiscoverNewsFragment extends BaseRecyclerFragment implements ScrollableToTop{ private String accountID; private List imageRequests=Collections.emptyList(); @@ -72,6 +73,11 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment{ list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 0, 0)); } + @Override + public void scrollToTop(){ + smoothScrollRecyclerViewToTop(list); + } + private class LinksAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter{ public LinksAdapter(){ super(imgLoader); 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 new file mode 100644 index 00000000..18bfa8b7 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java @@ -0,0 +1,291 @@ +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.ViewTreeObserver; +import android.view.inputmethod.InputMethodManager; + +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.BaseStatusListFragment; +import org.joinmastodon.android.fragments.ProfileFragment; +import org.joinmastodon.android.fragments.StatusListFragment; +import org.joinmastodon.android.fragments.ThreadFragment; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Hashtag; +import org.joinmastodon.android.model.SearchResult; +import org.joinmastodon.android.model.SearchResults; +import org.joinmastodon.android.model.Status; +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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +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.SimpleCallback; +import me.grishka.appkit.utils.MergeRecyclerAdapter; +import me.grishka.appkit.utils.V; + +public class SearchFragment extends BaseStatusListFragment{ + private String currentQuery; + 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); + } + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) + setRetainInstance(true); + loadData(); + } + + @Override + public void onAttach(Activity activity){ + super.onAttach(activity); + imm=activity.getSystemService(InputMethodManager.class); + } + + @Override + protected List buildDisplayItems(SearchResult s){ + return switch(s.type){ + case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)); + case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)); + case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true); + }; + } + + @Override + protected void addAccountToKnown(SearchResult s){ + Account acc=switch(s.type){ + case ACCOUNT -> s.account; + case STATUS -> s.status.account; + case HASHTAG -> null; + }; + if(acc!=null && !knownAccounts.containsKey(acc.id)) + knownAccounts.put(acc.id, acc); + } + + @Override + public void onItemClick(String id){ + SearchResult res=getResultByID(id); + switch(res.type){ + case ACCOUNT -> { + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putParcelable("profileAccount", Parcels.wrap(res.account)); + Nav.go(getActivity(), ProfileFragment.class, args); + } + case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name); + case STATUS -> { + Status status=res.status.getContentStatus(); + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putParcelable("status", Parcels.wrap(status)); + if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)) + args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId))); + Nav.go(getActivity(), ThreadFragment.class, args); + } + } + if(res.type!=SearchResult.Type.STATUS) + AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res); + } + + @Override + protected void doLoadData(int offset, int count){ + if(isInRecentMode()){ + AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{ + unfilteredResults=sr; + prevDisplayItems=new ArrayList<>(displayItems); + onDataLoaded(sr, false); + }); + }else{ + progressVisibilityListener.onProgressVisibilityChanged(true); + currentRequest=new GetSearchResults(currentQuery, null, true) + .setCallback(new SimpleCallback<>(this){ + @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); + } + }) + .exec(accountID); + } + } + + @Override + public void updateList(){ + if(prevDisplayItems==null){ + super.updateList(); + return; + } + imgLoader.deactivate(); + 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.activate(); + prevDisplayItems=null; + } + + @Override + protected void onDataLoaded(List d, boolean more){ + super.onDataLoaded(d, more); + 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){ + + } + }); + } + + @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)) + return; + if(currentRequest!=null){ + currentRequest.cancel(); + currentRequest=null; + } + currentQuery=q; + refreshing=true; + doLoadData(0, 0); + } + + 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; + onDataLoaded(filterSearchResults(unfilteredResults), false); + } + + private List filterSearchResults(List input){ + if(currentFilter.size()==SearchResult.Type.values().length) + return input; + return input.stream().filter(sr->currentFilter.contains(sr.type)).collect(Collectors.toList()); + } + + protected SearchResult getResultByID(String id){ + for(SearchResult s:data){ + if(s.id.equals(id)){ + return s; + } + } + 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(); + if(imm.isActive()){ + imm.hideSoftInputFromWindow(getActivity().getWindow().getDecorView().getWindowToken(), 0); + } + } + + @FunctionalInterface + public interface ProgressVisibilityListener{ + void onProgressVisibilityChanged(boolean visible); + } +} 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 f358a44d..453ee5a5 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 @@ -9,6 +9,7 @@ import android.widget.TextView; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags; +import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.utils.UiUtils; @@ -24,7 +25,7 @@ import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class TrendingHashtagsFragment extends BaseRecyclerFragment{ +public class TrendingHashtagsFragment extends BaseRecyclerFragment implements ScrollableToTop{ private String accountID; public TrendingHashtagsFragment(){ @@ -60,6 +61,11 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment{ list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, .5f, 16, 16)); } + @Override + public void scrollToTop(){ + smoothScrollRecyclerViewToTop(list); + } + private class HashtagsAdapter extends RecyclerView.Adapter{ @NonNull @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/SearchResult.java b/mastodon/src/main/java/org/joinmastodon/android/model/SearchResult.java new file mode 100644 index 00000000..70cfb943 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/SearchResult.java @@ -0,0 +1,64 @@ +package org.joinmastodon.android.model; + +import org.joinmastodon.android.api.ObjectValidationException; +import org.joinmastodon.android.api.RequiredField; + +public class SearchResult extends BaseModel implements DisplayItemsParent{ + public Account account; + public Hashtag hashtag; + public Status status; + @RequiredField + public Type type; + + public transient String id; + + public SearchResult(){} + + public SearchResult(Account acc){ + account=acc; + type=Type.ACCOUNT; + generateID(); + } + + public SearchResult(Hashtag tag){ + hashtag=tag; + type=Type.HASHTAG; + generateID(); + } + + public SearchResult(Status status){ + this.status=status; + type=Type.STATUS; + generateID(); + } + + public String getID(){ + return id; + } + + @Override + public void postprocess() throws ObjectValidationException{ + super.postprocess(); + if(account!=null) + account.postprocess(); + if(hashtag!=null) + hashtag.postprocess(); + if(status!=null) + status.postprocess(); + generateID(); + } + + private void generateID(){ + id=switch(type){ + case ACCOUNT -> "acc_"+account.id; + case HASHTAG -> "tag_"+hashtag.name.hashCode(); + case STATUS -> "post_"+status.id; + }; + } + + public enum Type{ + ACCOUNT, + HASHTAG, + STATUS + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/ComposeAutocompleteViewController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/ComposeAutocompleteViewController.java index 90fadde6..368672bc 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/ComposeAutocompleteViewController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/ComposeAutocompleteViewController.java @@ -17,7 +17,6 @@ import org.joinmastodon.android.api.requests.search.GetSearchResults; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Emoji; -import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.SearchResults; import org.joinmastodon.android.ui.drawables.ComposeAutocompleteBackgroundDrawable; @@ -27,13 +26,10 @@ import org.joinmastodon.android.ui.utils.UiUtils; import java.util.Collections; import java.util.List; -import java.util.function.BiPredicate; import java.util.function.Consumer; -import java.util.function.Predicate; import java.util.stream.Collectors; import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.api.APIRequest; @@ -165,7 +161,7 @@ public class ComposeAutocompleteViewController{ .filter(e->e.visibleInPicker && e.shortcode.startsWith(_text)) .map(WrappedEmoji::new) .collect(Collectors.toList()); - updateList(oldList, emojis, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode)); + UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode)); } } @@ -181,39 +177,15 @@ public class ComposeAutocompleteViewController{ return contentView; } - private void updateList(List oldList, List newList, RecyclerView.Adapter adapter, BiPredicate areItemsSame){ - DiffUtil.calculateDiff(new DiffUtil.Callback(){ - @Override - public int getOldListSize(){ - return oldList.size(); - } - - @Override - public int getNewListSize(){ - return newList.size(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ - return areItemsSame.test(oldList.get(oldItemPosition), newList.get(newItemPosition)); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ - return true; - } - }).dispatchUpdatesTo(adapter); - } - private void doSearchUsers(){ - currentRequest=new GetSearchResults(lastText, GetSearchResults.Type.ACCOUNTS) + currentRequest=new GetSearchResults(lastText, GetSearchResults.Type.ACCOUNTS, false) .setCallback(new Callback<>(){ @Override public void onSuccess(SearchResults result){ currentRequest=null; List oldList=users; users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList()); - updateList(oldList, users, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id)); + UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id)); if(listIsHidden){ listIsHidden=false; V.setVisibilityAnimated(list, View.VISIBLE); @@ -230,14 +202,14 @@ public class ComposeAutocompleteViewController{ } private void doSearchHashtags(){ - currentRequest=new GetSearchResults(lastText, GetSearchResults.Type.HASHTAGS) + currentRequest=new GetSearchResults(lastText, GetSearchResults.Type.HASHTAGS, false) .setCallback(new Callback<>(){ @Override public void onSuccess(SearchResults result){ currentRequest=null; List oldList=hashtags; hashtags=result.hashtags; - updateList(oldList, hashtags, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name)); + UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name)); if(listIsHidden){ listIsHidden=false; V.setVisibilityAnimated(list, View.VISIBLE); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountStatusDisplayItem.java new file mode 100644 index 00000000..39c46498 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountStatusDisplayItem.java @@ -0,0 +1,92 @@ +package org.joinmastodon.android.ui.displayitems; + +import android.content.Context; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.ui.OutlineProviders; +import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.CustomEmojiHelper; + +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.V; + +public class AccountStatusDisplayItem extends StatusDisplayItem{ + public final Account account; + private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); + private CharSequence parsedName; + public ImageLoaderRequest avaRequest; + + public AccountStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account){ + super(parentID, parentFragment); + this.account=account; + parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); + emojiHelper.setText(parsedName); + if(!TextUtils.isEmpty(account.avatar)) + avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50)); + } + + @Override + public Type getType(){ + return Type.ACCOUNT; + } + + @Override + public int getImageCount(){ + return 1+emojiHelper.getImageCount(); + } + + @Override + public ImageLoaderRequest getImageRequest(int index){ + if(index==0) + return avaRequest; + return emojiHelper.getImageRequest(index-1); + } + + public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ + private final TextView name, username; + private final ImageView photo; + + public Holder(Context context, ViewGroup parent){ + super(context, R.layout.display_item_account, parent); + name=findViewById(R.id.name); + username=findViewById(R.id.username); + photo=findViewById(R.id.photo); + + photo.setOutlineProvider(OutlineProviders.roundedRect(12)); + photo.setClipToOutline(true); + } + + @Override + public void onBind(AccountStatusDisplayItem item){ + name.setText(item.parsedName); + username.setText("@"+item.account.acct); + } + + @Override + public void setImage(int index, Drawable image){ + if(image instanceof Animatable && !((Animatable) image).isRunning()) + ((Animatable) image).start(); + if(index==0){ + photo.setImageDrawable(image); + }else{ + item.emojiHelper.setImageDrawable(index-1, image); + name.invalidate(); + } + } + + @Override + public void clearImage(int index){ + setImage(index, null); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HashtagStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HashtagStatusDisplayItem.java new file mode 100644 index 00000000..cc2f3d2b --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HashtagStatusDisplayItem.java @@ -0,0 +1,48 @@ +package org.joinmastodon.android.ui.displayitems; + +import android.content.Context; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.model.Hashtag; +import org.joinmastodon.android.ui.views.HashtagChartView; + +public class HashtagStatusDisplayItem extends StatusDisplayItem{ + public final Hashtag tag; + + public HashtagStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Hashtag tag){ + super(parentID, parentFragment); + this.tag=tag; + } + + @Override + public Type getType(){ + return Type.HASHTAG; + } + + public static class Holder extends StatusDisplayItem.Holder{ + private final TextView title, subtitle; + private final HashtagChartView chart; + + public Holder(Context context, ViewGroup parent){ + super(context, R.layout.item_trending_hashtag, parent); + title=findViewById(R.id.title); + subtitle=findViewById(R.id.subtitle); + chart=findViewById(R.id.chart); + } + + @Override + public void onBind(HashtagStatusDisplayItem _item){ + Hashtag item=_item.tag; + title.setText('#'+item.name); + int numPeople=item.history.get(0).accounts; + if(item.history.size()>1) + numPeople+=item.history.get(1).accounts; + subtitle.setText(_item.parentFragment.getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople)); + chart.setData(item.history); + + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 0b3b70e9..effe7916 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -30,6 +30,7 @@ public abstract class StatusDisplayItem{ public final String parentID; public final BaseStatusListFragment parentFragment; public boolean inset; + public int index; public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){ this.parentID=parentID; @@ -60,6 +61,8 @@ public abstract class StatusDisplayItem{ case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent); case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent); case ACCOUNT_CARD -> new AccountCardStatusDisplayItem.Holder(activity, parent); + case ACCOUNT -> new AccountStatusDisplayItem.Holder(activity, parent); + case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent); }; } @@ -110,8 +113,11 @@ public abstract class StatusDisplayItem{ if(addFooter){ items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID)); } - for(StatusDisplayItem item:items) + int i=1; + for(StatusDisplayItem item:items){ item.inset=inset; + item.index=i++; + } return items; } @@ -135,6 +141,8 @@ public abstract class StatusDisplayItem{ CARD, FOOTER, ACCOUNT_CARD, + ACCOUNT, + HASHTAG } public static abstract class Holder extends BindableViewHolder implements UsableRecyclerView.Clickable{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/CustomEmojiHelper.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/CustomEmojiHelper.java index c150114e..6e0a6b00 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/CustomEmojiHelper.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/CustomEmojiHelper.java @@ -34,7 +34,7 @@ public class CustomEmojiHelper{ } public ImageLoaderRequest getImageRequest(int image){ - return requests.get(image); + return image void updateList(List oldList, List newList, RecyclerView list, RecyclerView.Adapter adapter, BiPredicate areItemsSame){ + // Save topmost item position and offset because for some reason RecyclerView would scroll the list to weird places when you insert items at the top + int topItem, topItemOffset; + if(list.getChildCount()==0){ + topItem=topItemOffset=0; + }else{ + View child=list.getChildAt(0); + topItem=list.getChildAdapterPosition(child); + topItemOffset=child.getTop(); + } + DiffUtil.calculateDiff(new DiffUtil.Callback(){ + @Override + public int getOldListSize(){ + return oldList.size(); + } + + @Override + public int getNewListSize(){ + return newList.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ + return areItemsSame.test(oldList.get(oldItemPosition), newList.get(newItemPosition)); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ + return true; + } + }).dispatchUpdatesTo(adapter); + list.scrollToPosition(topItem); + list.scrollBy(0, topItemOffset); + } } 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 19caf052..48038a58 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 @@ -38,7 +38,7 @@ public class HashtagChartView extends View{ } public void setData(List data){ - int max=0; + int max=1; // avoid dividing by zero for(History h:data){ max=Math.max(h.accounts, max); } diff --git a/mastodon/src/main/res/drawable/edit_text_border.xml b/mastodon/src/main/res/drawable/edit_text_border.xml index 9d16da94..d423ca51 100644 --- a/mastodon/src/main/res/drawable/edit_text_border.xml +++ b/mastodon/src/main/res/drawable/edit_text_border.xml @@ -2,8 +2,8 @@ - - + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_dismiss_circle_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_dismiss_circle_24_filled.xml new file mode 100644 index 00000000..421cd902 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_dismiss_circle_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/layout/display_item_account.xml b/mastodon/src/main/res/layout/display_item_account.xml new file mode 100644 index 00000000..7d6eec4b --- /dev/null +++ b/mastodon/src/main/res/layout/display_item_account.xml @@ -0,0 +1,42 @@ + + + + + + + + + + \ 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 2015ed4a..59816514 100644 --- a/mastodon/src/main/res/layout/fragment_discover.xml +++ b/mastodon/src/main/res/layout/fragment_discover.xml @@ -15,29 +15,65 @@ android:paddingBottom="16dp" android:background="?android:statusBarColor"> - + android:layout_height="wrap_content" + android:background="@drawable/bg_search_field" + android:outlineProvider="background" + android:clipToOutline="true"> - + + + + + + + + + @@ -59,4 +95,11 @@ android:layout_height="0dp" android:layout_weight="1"/> + + \ 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 new file mode 100644 index 00000000..81e491e3 --- /dev/null +++ b/mastodon/src/main/res/layout/fragment_search.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/item_recent_searches_header.xml b/mastodon/src/main/res/layout/item_recent_searches_header.xml new file mode 100644 index 00000000..f696a7b8 --- /dev/null +++ b/mastodon/src/main/res/layout/item_recent_searches_header.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index db4955c2..3a72efb1 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -218,4 +218,7 @@ Public Followers only Only people I mention + All + People + Recent searches \ 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 ba64cef3..95414c81 100644 --- a/mastodon/src/main/res/values/styles.xml +++ b/mastodon/src/main/res/values/styles.xml @@ -75,15 +75,15 @@ @color/primary_700 @color/gray_600 @color/primary_600 + @color/primary_800 + @color/gray_400 - @color/gray_200 - @color/gray_600 - @color/gray_400 - @color/primary_100 + @color/gray_700 + @color/gray_300 @drawable/bg_button_primary_light_on_dark - @drawable/bg_edittext_light + @drawable/bg_edittext_dark false false