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