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.
This commit is contained in:
Vavassor 2017-02-21 17:55:37 -05:00
parent e59c0534c7
commit 9b6f5e63d3
12 changed files with 228 additions and 66 deletions

View File

@ -38,6 +38,7 @@
<activity android:name=".AccountActivity" />
<activity android:name=".PreferencesActivity" />
<activity android:name=".FavouritesActivity" />
<activity android:name=".BlocksActivity" />
<service
android:name=".PullNotificationService"
android:description="@string/notification_service_description"

View File

@ -35,6 +35,7 @@ public class AccountAdapter extends RecyclerView.Adapter {
private List<Account> 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;

View File

@ -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<Account> accounts, boolean added) {
if (added) {
adapter.addItems(accounts);
private static boolean findAccount(List<Account> accounts, String id) {
for (Account account : accounts) {
if (account.id.equals(id)) {
return true;
}
}
return false;
}
private void onFetchAccountsSuccess(List<Account> 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();

View File

@ -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
* <http://www.gnu.org/licenses/>. */
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();
}
}

View File

@ -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);

View File

@ -37,6 +37,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
private List<Notification> 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;

View File

@ -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<Notification> 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<Notification> notifications, boolean added) {
if (added) {
adapter.addItems(notifications);
private static boolean findNotification(List<Notification> notifications, String id) {
for (Notification notification : notifications) {
if (notification.getId().equals(id)) {
return true;
}
}
return false;
}
private void onFetchNotificationsSuccess(List<Notification> 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) {

View File

@ -31,6 +31,7 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
private List<Status> 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;
}
}

View File

@ -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<Status> statuses, boolean added) {
if (added) {
adapter.addItems(statuses);
private static boolean findStatus(List<Status> statuses, String id) {
for (Status status : statuses) {
if (status.getId().equals(id)) {
return true;
}
}
return false;
}
public void onFetchTimelineSuccess(List<Status> 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) {

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_view_thread"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.BlocksActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:background="?attr/toolbar_background_color" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<FrameLayout
android:id="@+id/overlay_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
</RelativeLayout>

View File

@ -10,18 +10,23 @@
app:showAsAction="always" />
<item
android:id="@+id/action_profile"
android:title="@string/action_profile"
android:id="@+id/action_view_profile"
android:title="@string/action_view_profile"
app:showAsAction="never" />
<item
android:id="@+id/action_preferences"
android:title="@string/action_preferences"
android:id="@+id/action_view_preferences"
android:title="@string/action_view_preferences"
app:showAsAction="never" />
<item
android:id="@+id/action_favourites"
android:title="@string/action_favourites"
android:id="@+id/action_view_favourites"
android:title="@string/action_view_favourites"
app:showAsAction="never" />
<item
android:id="@+id/action_view_blocks"
android:title="@string/action_view_blocks"
app:showAsAction="never" />
<item

View File

@ -65,6 +65,7 @@
<string name="title_follows">Follows</string>
<string name="title_followers">Followers</string>
<string name="title_favourites">Favourites</string>
<string name="title_blocks">Blocked Users</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boosted</string>
@ -100,11 +101,12 @@
<string name="action_cancel">Cancel</string>
<string name="action_close">Close</string>
<string name="action_back">Back</string>
<string name="action_profile">Profile</string>
<string name="action_view_profile">Profile</string>
<string name="action_view_preferences">Preferences</string>
<string name="action_view_favourites">Favourites</string>
<string name="action_view_blocks">Blocked Users</string>
<string name="action_open_in_web">Open In Web</string>
<string name="action_preferences">Preferences</string>
<string name="action_set_time">Set</string>
<string name="action_favourites">Favourites</string>
<string name="confirmation_send">Toot!</string>