From 289e2fbbe14d78ed930b48a45049ab58d68dd7ce Mon Sep 17 00:00:00 2001 From: Vavassor Date: Mon, 19 Jun 2017 04:18:39 -0400 Subject: [PATCH] Adds a prototype search page. The previous search bar is not yet removed. --- app/src/main/AndroidManifest.xml | 6 + .../com/keylesspalace/tusky/MainActivity.java | 18 +- .../keylesspalace/tusky/SearchActivity.java | 195 ++++++++++++++++++ .../tusky/adapter/AccountViewHolder.java | 51 +++++ .../tusky/adapter/FollowAdapter.java | 44 ---- .../tusky/adapter/SearchResultsAdapter.java | 115 +++++++++++ .../tusky/entity/SearchResults.java | 22 ++ .../tusky/network/MastodonAPI.java | 4 + app/src/main/res/layout/activity_search.xml | 50 +++++ app/src/main/res/layout/item_hashtag.xml | 7 + app/src/main/res/menu/search_toolbar.xml | 10 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/searchable.xml | 5 + 13 files changed, 480 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/SearchActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/SearchResultsAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/SearchResults.java create mode 100644 app/src/main/res/layout/activity_search.xml create mode 100644 app/src/main/res/layout/item_hashtag.xml create mode 100644 app/src/main/res/menu/search_toolbar.xml create mode 100644 app/src/main/res/xml/searchable.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 93bd79602..f64a62ed0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -82,6 +82,12 @@ + + + + + + diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index f6f274ebb..c43b85e39 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -293,10 +293,11 @@ public class MainActivity extends BaseActivity { new PrimaryDrawerItem().withIdentifier(1).withName(getString(R.string.action_view_favourites)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_star), new PrimaryDrawerItem().withIdentifier(2).withName(getString(R.string.action_view_mutes)).withSelectable(false).withIcon(muteDrawable), new PrimaryDrawerItem().withIdentifier(3).withName(getString(R.string.action_view_blocks)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block), + new PrimaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_search)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_search), new DividerDrawerItem(), - new SecondaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_view_preferences)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings), - new SecondaryDrawerItem().withIdentifier(5).withName(getString(R.string.about_title_activity)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_info), - new SecondaryDrawerItem().withIdentifier(6).withName(getString(R.string.action_logout)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app) + new SecondaryDrawerItem().withIdentifier(5).withName(getString(R.string.action_view_preferences)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings), + new SecondaryDrawerItem().withIdentifier(6).withName(getString(R.string.about_title_activity)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_info), + new SecondaryDrawerItem().withIdentifier(7).withName(getString(R.string.action_logout)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app) ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override @@ -319,14 +320,17 @@ public class MainActivity extends BaseActivity { intent.putExtra("type", AccountListActivity.Type.BLOCKS); startActivity(intent); } else if (drawerItemIdentifier == 4) { - Intent intent = new Intent(MainActivity.this, PreferencesActivity.class); + Intent intent = new Intent(MainActivity.this, SearchActivity.class); startActivity(intent); } else if (drawerItemIdentifier == 5) { - Intent intent = new Intent(MainActivity.this, AboutActivity.class); + Intent intent = new Intent(MainActivity.this, PreferencesActivity.class); startActivity(intent); } else if (drawerItemIdentifier == 6) { - logout(); + Intent intent = new Intent(MainActivity.this, AboutActivity.class); + startActivity(intent); } else if (drawerItemIdentifier == 7) { + logout(); + } else if (drawerItemIdentifier == 8) { Intent intent = new Intent(MainActivity.this, AccountListActivity.class); intent.putExtra("type", AccountListActivity.Type.FOLLOW_REQUESTS); startActivity(intent); @@ -512,7 +516,7 @@ public class MainActivity extends BaseActivity { // Show follow requests in the menu, if this is a locked account. if (me.locked) { PrimaryDrawerItem followRequestsItem = new PrimaryDrawerItem() - .withIdentifier(7) + .withIdentifier(8) .withName(R.string.action_view_follow_requests) .withSelectable(false) .withIcon(GoogleMaterial.Icon.gmd_person_add); diff --git a/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java b/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java new file mode 100644 index 000000000..9b30c9575 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java @@ -0,0 +1,195 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky; + +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.keylesspalace.tusky.adapter.SearchResultsAdapter; +import com.keylesspalace.tusky.entity.SearchResults; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class SearchActivity extends BaseActivity implements SearchView.OnQueryTextListener { + private static final String TAG = "SearchActivity"; // logging tag + + @BindView(R.id.progress_bar) ProgressBar progressBar; + @BindView(R.id.message_no_results) TextView messageNoResults; + private SearchResultsAdapter adapter; + private String currentQuery; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_search); + ButterKnife.bind(this); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setDisplayHomeAsUpEnabled(true); + bar.setDisplayShowHomeEnabled(true); + } + + RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + adapter = new SearchResultsAdapter(); + recyclerView.setAdapter(adapter); + + handleIntent(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + getMenuInflater().inflate(R.menu.search_toolbar, menu); + SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView(); + setupSearchView(searchView); + + if (currentQuery != null) { + searchView.setQuery(currentQuery, false); + } + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + onBackPressed(); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onQueryTextChange(String newText) { + return false; + } + + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + private void handleIntent(Intent intent) { + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + currentQuery = intent.getStringExtra(SearchManager.QUERY); + search(currentQuery); + } + } + + private void setupSearchView(SearchView searchView) { + searchView.setIconifiedByDefault(false); + + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + if (searchManager != null) { + List searchables = searchManager.getSearchablesInGlobalSearch(); + SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName()); + for (SearchableInfo info : searchables) { + if (info.getSuggestAuthority() != null + && info.getSuggestAuthority().startsWith("applications")) { + searchableInfo = info; + } + } + searchView.setSearchableInfo(searchableInfo); + } + + searchView.setOnQueryTextListener(this); + searchView.setFocusable(false); + searchView.setFocusableInTouchMode(false); + } + + private void search(String query) { + clearResults(); + Callback callback = new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + SearchResults results = response.body(); + if (results.accounts != null || results.hashtags != null) { + adapter.updateSearchResults(results); + hideFeedback(); + } else { + displayNoResults(); + } + } else { + onSearchFailure(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + onSearchFailure(); + } + }; + mastodonAPI.search(query, false) + .enqueue(callback); + } + + private void onSearchFailure() { + displayNoResults(); + Log.e(TAG, "Search request failed."); + } + + private void clearResults() { + adapter.updateSearchResults(null); + progressBar.setVisibility(View.VISIBLE); + messageNoResults.setVisibility(View.GONE); + } + + private void displayNoResults() { + progressBar.setVisibility(View.GONE); + messageNoResults.setVisibility(View.VISIBLE); + } + + private void hideFeedback() { + progressBar.setVisibility(View.GONE); + messageNoResults.setVisibility(View.GONE); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java new file mode 100644 index 000000000..c169841d1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java @@ -0,0 +1,51 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.TextView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.interfaces.AccountActionListener; +import com.pkmmte.view.CircularImageView; +import com.squareup.picasso.Picasso; + +class AccountViewHolder extends RecyclerView.ViewHolder { + private View container; + private TextView username; + private TextView displayName; + private CircularImageView avatar; + private String id; + + AccountViewHolder(View itemView) { + super(itemView); + container = itemView.findViewById(R.id.account_container); + username = (TextView) itemView.findViewById(R.id.account_username); + displayName = (TextView) itemView.findViewById(R.id.account_display_name); + avatar = (CircularImageView) itemView.findViewById(R.id.account_avatar); + } + + void setupWithAccount(Account account) { + id = account.id; + String format = username.getContext().getString(R.string.status_username_format); + String formattedUsername = String.format(format, account.username); + username.setText(formattedUsername); + displayName.setText(account.getDisplayName()); + Context context = avatar.getContext(); + Picasso.with(context) + .load(account.avatar) + .placeholder(R.drawable.avatar_default) + .error(R.drawable.avatar_error) + .into(avatar); + } + + void setupActionListener(final AccountActionListener listener) { + container.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewAccount(id); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java index 7e9b825fa..7df84c6cc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java @@ -15,18 +15,13 @@ package com.keylesspalace.tusky.adapter; -import android.content.Context; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.interfaces.AccountActionListener; -import com.pkmmte.view.CircularImageView; -import com.squareup.picasso.Picasso; /** Both for follows and following lists. */ public class FollowAdapter extends AccountAdapter { @@ -71,43 +66,4 @@ public class FollowAdapter extends AccountAdapter { return VIEW_TYPE_ACCOUNT; } } - - private static class AccountViewHolder extends RecyclerView.ViewHolder { - private View container; - private TextView username; - private TextView displayName; - private CircularImageView avatar; - private String id; - - AccountViewHolder(View itemView) { - super(itemView); - container = itemView.findViewById(R.id.account_container); - username = (TextView) itemView.findViewById(R.id.account_username); - displayName = (TextView) itemView.findViewById(R.id.account_display_name); - avatar = (CircularImageView) itemView.findViewById(R.id.account_avatar); - } - - void setupWithAccount(Account account) { - id = account.id; - String format = username.getContext().getString(R.string.status_username_format); - String formattedUsername = String.format(format, account.username); - username.setText(formattedUsername); - displayName.setText(account.getDisplayName()); - Context context = avatar.getContext(); - Picasso.with(context) - .load(account.avatar) - .placeholder(R.drawable.avatar_default) - .error(R.drawable.avatar_error) - .into(avatar); - } - - void setupActionListener(final AccountActionListener listener) { - container.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onViewAccount(id); - } - }); - } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/SearchResultsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/SearchResultsAdapter.java new file mode 100644 index 000000000..6020944b1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/SearchResultsAdapter.java @@ -0,0 +1,115 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.adapter; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.entity.SearchResults; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SearchResultsAdapter extends RecyclerView.Adapter { + private static final int VIEW_TYPE_ACCOUNT = 0; + private static final int VIEW_TYPE_HASHTAG = 1; + + private List accountList; + private List hashtagList; + + public SearchResultsAdapter() { + super(); + accountList = new ArrayList<>(); + hashtagList = new ArrayList<>(); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + default: + case VIEW_TYPE_ACCOUNT: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_account, parent, false); + return new AccountViewHolder(view); + } + case VIEW_TYPE_HASHTAG: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_hashtag, parent, false); + return new HashtagViewHolder(view); + } + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + if (position < accountList.size()) { + AccountViewHolder holder = (AccountViewHolder) viewHolder; + holder.setupWithAccount(accountList.get(position)); + } else { + HashtagViewHolder holder = (HashtagViewHolder) viewHolder; + int index = position - accountList.size(); + holder.setHashtag(hashtagList.get(index)); + } + } + + @Override + public int getItemCount() { + return accountList.size() + hashtagList.size(); + } + + @Override + public int getItemViewType(int position) { + if (position >= accountList.size()) { + return VIEW_TYPE_HASHTAG; + } else { + return VIEW_TYPE_ACCOUNT; + } + } + + public void updateSearchResults(SearchResults results) { + if (results != null) { + if (results.accounts != null) { + accountList.addAll(Arrays.asList(results.accounts)); + } + if (results.hashtags != null) { + hashtagList.addAll(Arrays.asList(results.hashtags)); + } + } else { + accountList.clear(); + hashtagList.clear(); + } + notifyDataSetChanged(); + } + + private static class HashtagViewHolder extends RecyclerView.ViewHolder { + private TextView hashtag; + + HashtagViewHolder(View itemView) { + super(itemView); + hashtag = (TextView) itemView.findViewById(R.id.hashtag); + } + + void setHashtag(String tag) { + hashtag.setText(String.format("#%s", tag)); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResults.java b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResults.java new file mode 100644 index 000000000..9ac5827b5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResults.java @@ -0,0 +1,22 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity; + +public class SearchResults { + public Account[] accounts; + public Status[] statuses; + public String[] hashtags; +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonAPI.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonAPI.java index aad04523c..72c716088 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonAPI.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonAPI.java @@ -22,6 +22,7 @@ import com.keylesspalace.tusky.entity.Media; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Profile; import com.keylesspalace.tusky.entity.Relationship; +import com.keylesspalace.tusky.entity.SearchResults; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.StatusContext; @@ -191,6 +192,9 @@ public interface MastodonAPI { @POST("api/v1/reports") Call report(@Field("account_id") String accountId, @Field("status_ids[]") List statusIds, @Field("comment") String comment); + @GET("api/v1/search") + Call search(@Query("q") String q, @Query("resolve") Boolean resolve); + @FormUrlEncoded @POST("api/v1/apps") Call authenticateApp( diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml new file mode 100644 index 000000000..81ae2b0ba --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_hashtag.xml b/app/src/main/res/layout/item_hashtag.xml new file mode 100644 index 000000000..128b55df6 --- /dev/null +++ b/app/src/main/res/layout/item_hashtag.xml @@ -0,0 +1,7 @@ + + diff --git a/app/src/main/res/menu/search_toolbar.xml b/app/src/main/res/menu/search_toolbar.xml new file mode 100644 index 000000000..172a12083 --- /dev/null +++ b/app/src/main/res/menu/search_toolbar.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68dc27683..cba9f640e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,6 +100,7 @@ Undo Accept Reject + Search Share toot URL to… Share toot to… @@ -116,6 +117,9 @@ Content warning Display name Bio + Search accounts and tags… + + No results Avatar Header diff --git a/app/src/main/res/xml/searchable.xml b/app/src/main/res/xml/searchable.xml new file mode 100644 index 000000000..c3fbef714 --- /dev/null +++ b/app/src/main/res/xml/searchable.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file