From 9b6f5e63d33077b34de1585ca41d3004094ac507 Mon Sep 17 00:00:00 2001 From: Vavassor Date: Tue, 21 Feb 2017 17:55:37 -0500 Subject: [PATCH] Viewing your block list is now possible on the main menu. Also, changed how end-of-timeline behaviour is handled on all timelines. It should detect it more reliably now. --- app/src/main/AndroidManifest.xml | 1 + .../keylesspalace/tusky/AccountAdapter.java | 7 ++ .../keylesspalace/tusky/AccountFragment.java | 95 +++++++++++++------ .../keylesspalace/tusky/BlocksActivity.java | 43 +++++++++ .../com/keylesspalace/tusky/MainActivity.java | 11 ++- .../tusky/NotificationsAdapter.java | 7 ++ .../tusky/NotificationsFragment.java | 33 ++++--- .../keylesspalace/tusky/TimelineAdapter.java | 7 ++ .../keylesspalace/tusky/TimelineFragment.java | 30 +++--- app/src/main/res/layout/activity_blocks.xml | 35 +++++++ app/src/main/res/menu/main_toolbar.xml | 17 ++-- app/src/main/res/values/strings.xml | 8 +- 12 files changed, 228 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java create mode 100644 app/src/main/res/layout/activity_blocks.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a929daa3a..7491beee7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,7 @@ + accounts; private AccountActionListener accountActionListener; private FooterActionListener footerActionListener; + private FooterViewHolder.State footerState; public AccountAdapter(AccountActionListener accountActionListener, FooterActionListener footerActionListener) { @@ -42,6 +43,7 @@ public class AccountAdapter extends RecyclerView.Adapter { accounts = new ArrayList<>(); this.accountActionListener = accountActionListener; this.footerActionListener = footerActionListener; + footerState = FooterViewHolder.State.LOADING; } @Override @@ -69,6 +71,7 @@ public class AccountAdapter extends RecyclerView.Adapter { holder.setupActionListener(accountActionListener); } else { FooterViewHolder holder = (FooterViewHolder) viewHolder; + holder.setState(footerState); holder.setupButton(footerActionListener); holder.setRetryMessage(R.string.footer_retry_accounts); holder.setEndOfTimelineMessage(R.string.footer_end_of_accounts); @@ -116,6 +119,10 @@ public class AccountAdapter extends RecyclerView.Adapter { return null; } + public void setFooterState(FooterViewHolder.State state) { + this.footerState = state; + } + private static class AccountViewHolder extends RecyclerView.ViewHolder { private View container; private TextView username; diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java index 05131b8ac..1742188d2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java @@ -23,7 +23,6 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -45,12 +44,12 @@ import java.util.Map; public class AccountFragment extends Fragment implements AccountActionListener, FooterActionListener { - private static final String TAG = "Account"; - private static int EXPECTED_ACCOUNTS_FETCHED = 20; + private static final String TAG = "Account"; // logging tag public enum Type { FOLLOWS, FOLLOWERS, + BLOCKS, } private Type type; @@ -63,6 +62,14 @@ public class AccountFragment extends Fragment implements AccountActionListener, private AccountAdapter adapter; private TabLayout.OnTabSelectedListener onTabSelectedListener; + public static AccountFragment newInstance(Type type) { + Bundle arguments = new Bundle(); + AccountFragment fragment = new AccountFragment(); + arguments.putString("type", type.name()); + fragment.setArguments(arguments); + return fragment; + } + public static AccountFragment newInstance(Type type, String accountId) { Bundle arguments = new Bundle(); AccountFragment fragment = new AccountFragment(); @@ -99,7 +106,8 @@ public class AccountFragment extends Fragment implements AccountActionListener, recyclerView.setLayoutManager(layoutManager); DividerItemDecoration divider = new DividerItemDecoration( context, layoutManager.getOrientation()); - Drawable drawable = ContextCompat.getDrawable(context, R.drawable.status_divider_dark); + Drawable drawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable, + R.drawable.status_divider_dark); divider.setDrawable(drawable); recyclerView.addItemDecoration(divider); scrollListener = new EndlessOnScrollListener(layoutManager) { @@ -118,45 +126,54 @@ public class AccountFragment extends Fragment implements AccountActionListener, adapter = new AccountAdapter(this, this); recyclerView.setAdapter(adapter); - TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout); - onTabSelectedListener = new TabLayout.OnTabSelectedListener() { - @Override - public void onTabSelected(TabLayout.Tab tab) {} + if (jumpToTopAllowed()) { + TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout); + onTabSelectedListener = new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + } - @Override - public void onTabUnselected(TabLayout.Tab tab) {} + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } - @Override - public void onTabReselected(TabLayout.Tab tab) { - jumpToTop(); - } - }; - layout.addOnTabSelectedListener(onTabSelectedListener); + @Override + public void onTabReselected(TabLayout.Tab tab) { + jumpToTop(); + } + }; + layout.addOnTabSelectedListener(onTabSelectedListener); + } return rootView; } @Override public void onDestroyView() { - TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout); - tabLayout.removeOnTabSelectedListener(onTabSelectedListener); + if (jumpToTopAllowed()) { + TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout); + tabLayout.removeOnTabSelectedListener(onTabSelectedListener); + } super.onDestroyView(); } private void fetchAccounts(final String fromId) { - int endpointId; + String endpoint; switch (type) { default: case FOLLOWS: { - endpointId = R.string.endpoint_following; + endpoint = String.format(getString(R.string.endpoint_following), accountId); break; } case FOLLOWERS: { - endpointId = R.string.endpoint_followers; + endpoint = String.format(getString(R.string.endpoint_followers), accountId); + break; + } + case BLOCKS: { + endpoint = getString(R.string.endpoint_blocks); break; } } - String endpoint = String.format(getString(endpointId), accountId); String url = "https://" + domain + endpoint; if (fromId != null) { url += "?max_id=" + fromId; @@ -172,7 +189,7 @@ public class AccountFragment extends Fragment implements AccountActionListener, onFetchAccountsFailure(e); return; } - onFetchAccountsSuccess(accounts, fromId != null); + onFetchAccountsSuccess(accounts, fromId); } }, new Response.ErrorListener() { @@ -195,17 +212,26 @@ public class AccountFragment extends Fragment implements AccountActionListener, fetchAccounts(null); } - private void onFetchAccountsSuccess(List accounts, boolean added) { - if (added) { - adapter.addItems(accounts); + private static boolean findAccount(List accounts, String id) { + for (Account account : accounts) { + if (account.id.equals(id)) { + return true; + } + } + return false; + } + + private void onFetchAccountsSuccess(List accounts, String fromId) { + if (fromId != null) { + if (accounts.size() > 0 && !findAccount(accounts, fromId)) { + setFetchTimelineState(FooterViewHolder.State.LOADING); + adapter.addItems(accounts); + } else { + setFetchTimelineState(FooterViewHolder.State.END_OF_TIMELINE); + } } else { adapter.update(accounts); } - if (accounts.size() >= EXPECTED_ACCOUNTS_FETCHED) { - setFetchTimelineState(FooterViewHolder.State.LOADING); - } else { - setFetchTimelineState(FooterViewHolder.State.END_OF_TIMELINE); - } } private void onFetchAccountsFailure(Exception exception) { @@ -214,6 +240,9 @@ public class AccountFragment extends Fragment implements AccountActionListener, } private void setFetchTimelineState(FooterViewHolder.State state) { + // Set the adapter to set its state when it's bound, if the current Footer is offscreen. + adapter.setFooterState(state); + // Check if it's onscreen, and update it directly if it is. RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1); if (viewHolder != null) { @@ -237,6 +266,10 @@ public class AccountFragment extends Fragment implements AccountActionListener, startActivity(intent); } + private boolean jumpToTopAllowed() { + return type != Type.BLOCKS; + } + private void jumpToTop() { layoutManager.scrollToPositionWithOffset(0, 0); scrollListener.reset(); diff --git a/app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java b/app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java new file mode 100644 index 000000000..99e391931 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java @@ -0,0 +1,43 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky 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.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; + +public class BlocksActivity extends BaseActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_blocks); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setTitle(getString(R.string.title_blocks)); + } + + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + Fragment fragment = AccountFragment.newInstance(AccountFragment.Type.BLOCKS); + fragmentTransaction.add(R.id.fragment_container, fragment); + fragmentTransaction.commit(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 7a2ee6a57..afab447a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -175,22 +175,27 @@ public class MainActivity extends BaseActivity { startActivity(intent); return true; } - case R.id.action_profile: { + case R.id.action_view_profile: { Intent intent = new Intent(this, AccountActivity.class); intent.putExtra("id", loggedInAccountId); startActivity(intent); return true; } - case R.id.action_preferences: { + case R.id.action_view_preferences: { Intent intent = new Intent(this, PreferencesActivity.class); startActivity(intent); return true; } - case R.id.action_favourites: { + case R.id.action_view_favourites: { Intent intent = new Intent(this, FavouritesActivity.class); startActivity(intent); return true; } + case R.id.action_view_blocks: { + Intent intent = new Intent(this, BlocksActivity.class); + startActivity(intent); + return true; + } case R.id.action_logout: { if (notificationServiceEnabled) { alarmManager.cancel(serviceAlarmIntent); diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java index f8dcfa3d2..ca3064333 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java @@ -37,6 +37,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte private List notifications; private StatusActionListener statusListener; private FooterActionListener footerListener; + private FooterViewHolder.State footerState; public NotificationsAdapter(StatusActionListener statusListener, FooterActionListener footerListener) { @@ -44,6 +45,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte notifications = new ArrayList<>(); this.statusListener = statusListener; this.footerListener = footerListener; + footerState = FooterViewHolder.State.LOADING; } @Override @@ -100,6 +102,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte } } else { FooterViewHolder holder = (FooterViewHolder) viewHolder; + holder.setState(footerState); holder.setupButton(footerListener); holder.setRetryMessage(R.string.footer_retry_notifications); holder.setEndOfTimelineMessage(R.string.footer_end_of_notifications); @@ -170,6 +173,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte notifyItemChanged(position); } + public void setFooterState(FooterViewHolder.State state) { + footerState = state; + } + public static class FollowViewHolder extends RecyclerView.ViewHolder { private TextView message; diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java index 552f6e9bd..cd7425821 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java @@ -20,7 +20,6 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.TabLayout; -import android.support.v4.content.ContextCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; @@ -44,7 +43,6 @@ import java.util.Map; public class NotificationsFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener { private static final String TAG = "Notifications"; // logging tag - private static final int EXPECTED_NOTIFICATIONS_FETCHED = 10; private SwipeRefreshLayout swipeRefreshLayout; private RecyclerView recyclerView; @@ -77,7 +75,8 @@ public class NotificationsFragment extends SFragment implements recyclerView.setLayoutManager(layoutManager); DividerItemDecoration divider = new DividerItemDecoration( context, layoutManager.getOrientation()); - Drawable drawable = ContextCompat.getDrawable(context, R.drawable.status_divider_dark); + Drawable drawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable, + R.drawable.status_divider_dark); divider.setDrawable(drawable); recyclerView.addItemDecoration(divider); scrollListener = new EndlessOnScrollListener(layoutManager) { @@ -140,7 +139,7 @@ public class NotificationsFragment extends SFragment implements public void onResponse(JSONArray response) { try { List notifications = Notification.parse(response); - onFetchNotificationsSuccess(notifications, fromId != null); + onFetchNotificationsSuccess(notifications, fromId); } catch (JSONException e) { onFetchNotificationsFailure(e); } @@ -165,17 +164,26 @@ public class NotificationsFragment extends SFragment implements sendFetchNotificationsRequest(null); } - private void onFetchNotificationsSuccess(List notifications, boolean added) { - if (added) { - adapter.addItems(notifications); + private static boolean findNotification(List notifications, String id) { + for (Notification notification : notifications) { + if (notification.getId().equals(id)) { + return true; + } + } + return false; + } + + private void onFetchNotificationsSuccess(List notifications, String fromId) { + if (fromId != null) { + if (notifications.size() > 0 && !findNotification(notifications, fromId)) { + setFetchTimelineState(FooterViewHolder.State.LOADING); + adapter.addItems(notifications); + } else { + setFetchTimelineState(FooterViewHolder.State.END_OF_TIMELINE); + } } else { adapter.update(notifications); } - if (notifications.size() >= EXPECTED_NOTIFICATIONS_FETCHED) { - setFetchTimelineState(FooterViewHolder.State.LOADING); - } else { - setFetchTimelineState(FooterViewHolder.State.END_OF_TIMELINE); - } swipeRefreshLayout.setRefreshing(false); } @@ -186,6 +194,7 @@ public class NotificationsFragment extends SFragment implements } private void setFetchTimelineState(FooterViewHolder.State state) { + adapter.setFooterState(state); RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1); if (viewHolder != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java index d14e9ec64..0fe360de8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java @@ -31,6 +31,7 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem private List statuses; private StatusActionListener statusListener; private FooterActionListener footerListener; + private FooterViewHolder.State footerState; public TimelineAdapter(StatusActionListener statusListener, FooterActionListener footerListener) { @@ -38,6 +39,7 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem statuses = new ArrayList<>(); this.statusListener = statusListener; this.footerListener = footerListener; + footerState = FooterViewHolder.State.LOADING; } @Override @@ -65,6 +67,7 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem holder.setupWithStatus(status, statusListener, position); } else { FooterViewHolder holder = (FooterViewHolder) viewHolder; + holder.setState(footerState); holder.setupButton(footerListener); holder.setRetryMessage(R.string.footer_retry_statuses); holder.setEndOfTimelineMessage(R.string.footer_end_of_statuses); @@ -121,4 +124,8 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem } return null; } + + public void setFooterState(FooterViewHolder.State state) { + footerState = state; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java index 50a62c2d6..0c2d6983c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java @@ -19,7 +19,6 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.design.widget.TabLayout; -import android.support.v4.content.ContextCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; @@ -43,7 +42,6 @@ import java.util.Map; public class TimelineFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener { private static final String TAG = "Timeline"; // logging tag - private static final int EXPECTED_STATUSES_FETCHED = 20; public enum Kind { HOME, @@ -207,7 +205,7 @@ public class TimelineFragment extends SFragment implements onFetchTimelineFailure(e); } if (statuses != null) { - onFetchTimelineSuccess(statuses, fromId != null); + onFetchTimelineSuccess(statuses, fromId); } } }, new Response.ErrorListener() { @@ -230,17 +228,26 @@ public class TimelineFragment extends SFragment implements sendFetchTimelineRequest(null); } - public void onFetchTimelineSuccess(List statuses, boolean added) { - if (added) { - adapter.addItems(statuses); + private static boolean findStatus(List statuses, String id) { + for (Status status : statuses) { + if (status.getId().equals(id)) { + return true; + } + } + return false; + } + + public void onFetchTimelineSuccess(List statuses, String fromId) { + if (fromId != null) { + if (statuses.size() > 0 && !findStatus(statuses, fromId)) { + setFetchTimelineState(FooterViewHolder.State.LOADING); + adapter.addItems(statuses); + } else { + setFetchTimelineState(FooterViewHolder.State.END_OF_TIMELINE); + } } else { adapter.update(statuses); } - if (statuses.size() >= EXPECTED_STATUSES_FETCHED) { - setFetchTimelineState(FooterViewHolder.State.LOADING); - } else { - setFetchTimelineState(FooterViewHolder.State.END_OF_TIMELINE); - } swipeRefreshLayout.setRefreshing(false); } @@ -251,6 +258,7 @@ public class TimelineFragment extends SFragment implements } private void setFetchTimelineState(FooterViewHolder.State state) { + adapter.setFooterState(state); RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1); if (viewHolder != null) { diff --git a/app/src/main/res/layout/activity_blocks.xml b/app/src/main/res/layout/activity_blocks.xml new file mode 100644 index 000000000..aa8a37306 --- /dev/null +++ b/app/src/main/res/layout/activity_blocks.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_toolbar.xml b/app/src/main/res/menu/main_toolbar.xml index 57ee72a22..8ed161a5a 100644 --- a/app/src/main/res/menu/main_toolbar.xml +++ b/app/src/main/res/menu/main_toolbar.xml @@ -10,18 +10,23 @@ app:showAsAction="always" /> + + Follows Followers Favourites + Blocked Users \@%s %s boosted @@ -100,11 +101,12 @@ Cancel Close Back - Profile + Profile + Preferences + Favourites + Blocked Users Open In Web - Preferences Set - Favourites Toot!