Moves loading of accounts, notifications, and statuses to use link headers. Also remedies an issue where duplicate calls for the same chunk of items in a list can occur.
This commit is contained in:
parent
3f3ccfca55
commit
3955649b9c
|
@ -21,12 +21,16 @@ import android.support.v7.widget.RecyclerView;
|
|||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||
List<Account> accountList;
|
||||
AccountActionListener accountActionListener;
|
||||
private String topId;
|
||||
private String bottomId;
|
||||
|
||||
AccountAdapter(AccountActionListener accountActionListener) {
|
||||
super();
|
||||
|
@ -39,12 +43,20 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
|||
return accountList.size() + 1;
|
||||
}
|
||||
|
||||
public void update(List<Account> newAccounts) {
|
||||
public void update(@Nullable List<Account> newAccounts, @Nullable String fromId,
|
||||
@Nullable String uptoId) {
|
||||
if (newAccounts == null || newAccounts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
if (uptoId != null) {
|
||||
topId = uptoId;
|
||||
}
|
||||
if (accountList.isEmpty()) {
|
||||
accountList = newAccounts;
|
||||
// This construction removes duplicates.
|
||||
accountList = new ArrayList<>(new HashSet<>(newAccounts));
|
||||
} else {
|
||||
int index = accountList.indexOf(newAccounts.get(newAccounts.size() - 1));
|
||||
for (int i = 0; i < index; i++) {
|
||||
|
@ -60,11 +72,26 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<Account> newAccounts) {
|
||||
public void addItems(List<Account> newAccounts, @Nullable String fromId) {
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
int end = accountList.size();
|
||||
Account last = accountList.get(end - 1);
|
||||
if (last != null && !findAccount(accountList, last.id)) {
|
||||
accountList.addAll(newAccounts);
|
||||
notifyItemRangeInserted(end, newAccounts.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean findAccount(List<Account> accounts, String id) {
|
||||
for (Account account : accounts) {
|
||||
if (account.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Account removeItem(int position) {
|
||||
|
@ -84,10 +111,21 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
|||
notifyItemInserted(position);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Account getItem(int position) {
|
||||
if (position >= 0 && position < accountList.size()) {
|
||||
return accountList.get(position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getBottomId() {
|
||||
return bottomId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTopId() {
|
||||
return topId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
|||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||
|
@ -56,6 +57,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
private NotificationActionListener notificationActionListener;
|
||||
private FooterState footerState = FooterState.END;
|
||||
private boolean mediaPreviewEnabled;
|
||||
private String bottomId;
|
||||
private String topId;
|
||||
|
||||
public NotificationsAdapter(StatusActionListener statusListener,
|
||||
NotificationActionListener notificationActionListener) {
|
||||
|
@ -186,19 +189,28 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
}
|
||||
}
|
||||
|
||||
public @Nullable Notification getItem(int position) {
|
||||
@Nullable
|
||||
public Notification getItem(int position) {
|
||||
if (position >= 0 && position < notifications.size()) {
|
||||
return notifications.get(position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void update(List<Notification> newNotifications) {
|
||||
public void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
|
||||
@Nullable String uptoId) {
|
||||
if (newNotifications == null || newNotifications.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
if (uptoId != null) {
|
||||
topId = uptoId;
|
||||
}
|
||||
if (notifications.isEmpty()) {
|
||||
notifications = newNotifications;
|
||||
// This construction removes duplicates.
|
||||
notifications = new ArrayList<>(new HashSet<>(newNotifications));
|
||||
} else {
|
||||
int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1));
|
||||
for (int i = 0; i < index; i++) {
|
||||
|
@ -214,10 +226,25 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<Notification> new_notifications) {
|
||||
public void addItems(List<Notification> newNotifications, @Nullable String fromId) {
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
int end = notifications.size();
|
||||
notifications.addAll(new_notifications);
|
||||
notifyItemRangeInserted(end, new_notifications.size());
|
||||
Notification last = notifications.get(end - 1);
|
||||
if (last != null && !findNotification(notifications, last.id)) {
|
||||
notifications.addAll(newNotifications);
|
||||
notifyItemRangeInserted(end, newNotifications.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean findNotification(List<Notification> notifications, String id) {
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
|
@ -233,6 +260,16 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getBottomId() {
|
||||
return bottomId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTopId() {
|
||||
return topId;
|
||||
}
|
||||
|
||||
public void setMediaPreviewEnabled(boolean enabled) {
|
||||
mediaPreviewEnabled = enabled;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
|||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||
|
@ -43,6 +44,8 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
private StatusActionListener statusListener;
|
||||
private FooterState footerState = FooterState.END;
|
||||
private boolean mediaPreviewEnabled;
|
||||
private String topId;
|
||||
private String bottomId;
|
||||
|
||||
public TimelineAdapter(StatusActionListener statusListener) {
|
||||
super();
|
||||
|
@ -126,12 +129,20 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
}
|
||||
}
|
||||
|
||||
public void update(List<Status> newStatuses) {
|
||||
public void update(@Nullable List<Status> newStatuses, @Nullable String fromId,
|
||||
@Nullable String uptoId) {
|
||||
if (newStatuses == null || newStatuses.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
if (uptoId != null) {
|
||||
topId = uptoId;
|
||||
}
|
||||
if (statuses.isEmpty()) {
|
||||
statuses = newStatuses;
|
||||
// This construction removes duplicates.
|
||||
statuses = new ArrayList<>(new HashSet<>(newStatuses));
|
||||
} else {
|
||||
int index = statuses.indexOf(newStatuses.get(newStatuses.size() - 1));
|
||||
for (int i = 0; i < index; i++) {
|
||||
|
@ -147,11 +158,26 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<Status> newStatuses) {
|
||||
public void addItems(List<Status> newStatuses, @Nullable String fromId) {
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
int end = statuses.size();
|
||||
Status last = statuses.get(end - 1);
|
||||
if (last != null && !findStatus(statuses, last.id)) {
|
||||
statuses.addAll(newStatuses);
|
||||
notifyItemRangeInserted(end, newStatuses.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean findStatus(List<Status> statuses, String id) {
|
||||
for (Status status : statuses) {
|
||||
if (status.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
statuses.clear();
|
||||
|
@ -177,4 +203,14 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
public void setMediaPreviewEnabled(boolean enabled) {
|
||||
mediaPreviewEnabled = enabled;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getBottomId() {
|
||||
return bottomId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTopId() {
|
||||
return topId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import com.keylesspalace.tusky.entity.Relationship;
|
|||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||
|
||||
|
@ -71,6 +72,10 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
private AccountAdapter adapter;
|
||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
private MastodonApi api;
|
||||
private boolean bottomLoading;
|
||||
private int bottomFetches;
|
||||
private boolean topLoading;
|
||||
private int topFetches;
|
||||
|
||||
public static AccountListFragment newInstance(Type type) {
|
||||
Bundle arguments = new Bundle();
|
||||
|
@ -160,13 +165,7 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
AccountAdapter adapter = (AccountAdapter) view.getAdapter();
|
||||
Account account = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (account != null) {
|
||||
fetchAccounts(account.id, null);
|
||||
} else {
|
||||
fetchAccounts();
|
||||
}
|
||||
AccountListFragment.this.onLoadMore(view);
|
||||
}
|
||||
};
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
|
@ -181,78 +180,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void fetchAccounts(final String fromId, String uptoId) {
|
||||
Callback<List<Account>> cb = new Callback<List<Account>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Account>> call, Response<List<Account>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onFetchAccountsSuccess(response.body(), fromId);
|
||||
} else {
|
||||
onFetchAccountsFailure(new Exception(response.message()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Account>> call, Throwable t) {
|
||||
onFetchAccountsFailure((Exception) t);
|
||||
}
|
||||
};
|
||||
|
||||
Call<List<Account>> listCall;
|
||||
switch (type) {
|
||||
default:
|
||||
case FOLLOWS: {
|
||||
listCall = api.accountFollowing(accountId, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case FOLLOWERS: {
|
||||
listCall = api.accountFollowers(accountId, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case BLOCKS: {
|
||||
listCall = api.blocks(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case MUTES: {
|
||||
listCall = api.mutes(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case FOLLOW_REQUESTS: {
|
||||
listCall = api.followRequests(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
callList.add(listCall);
|
||||
listCall.enqueue(cb);
|
||||
}
|
||||
|
||||
private void fetchAccounts() {
|
||||
fetchAccounts(null, null);
|
||||
}
|
||||
|
||||
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)) {
|
||||
adapter.addItems(accounts);
|
||||
}
|
||||
} else {
|
||||
adapter.update(accounts);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFetchAccountsFailure(Exception exception) {
|
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAccount(String id) {
|
||||
Intent intent = new Intent(getContext(), AccountActivity.class);
|
||||
|
@ -444,4 +371,126 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
layoutManager.scrollToPositionWithOffset(0, 0);
|
||||
scrollListener.reset();
|
||||
}
|
||||
|
||||
private enum FetchEnd {
|
||||
TOP,
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
private Call<List<Account>> getFetchCallByListType(Type type, String fromId, String uptoId) {
|
||||
switch (type) {
|
||||
default:
|
||||
case FOLLOWS: return api.accountFollowing(accountId, fromId, uptoId, null);
|
||||
case FOLLOWERS: return api.accountFollowers(accountId, fromId, uptoId, null);
|
||||
case BLOCKS: return api.blocks(fromId, uptoId, null);
|
||||
case MUTES: return api.mutes(fromId, uptoId, null);
|
||||
case FOLLOW_REQUESTS: return api.followRequests(fromId, uptoId, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchAccounts(String fromId, String uptoId, final FetchEnd fetchEnd) {
|
||||
/* If there is a fetch already ongoing, record however many fetches are requested and
|
||||
* fulfill them after it's complete. */
|
||||
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
||||
topFetches++;
|
||||
return;
|
||||
}
|
||||
if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) {
|
||||
bottomFetches++;
|
||||
return;
|
||||
}
|
||||
Callback<List<Account>> cb = new Callback<List<Account>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Account>> call, Response<List<Account>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchAccountsSuccess(response.body(), linkHeader, fetchEnd);
|
||||
} else {
|
||||
onFetchAccountsFailure(new Exception(response.message()), fetchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Account>> call, Throwable t) {
|
||||
onFetchAccountsFailure((Exception) t, fetchEnd);
|
||||
}
|
||||
};
|
||||
Call<List<Account>> listCall = getFetchCallByListType(type, fromId, uptoId);
|
||||
callList.add(listCall);
|
||||
listCall.enqueue(cb);
|
||||
}
|
||||
|
||||
private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader,
|
||||
FetchEnd fetchEnd) {
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||
switch (fetchEnd) {
|
||||
case TOP: {
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(accounts, null, uptoId);
|
||||
break;
|
||||
}
|
||||
case BOTTOM: {
|
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||
String fromId = null;
|
||||
if (next != null) {
|
||||
fromId = next.uri.getQueryParameter("max_id");
|
||||
}
|
||||
if (adapter.getItemCount() > 1) {
|
||||
adapter.addItems(accounts, fromId);
|
||||
} else {
|
||||
/* If this is the first fetch, also save the id from the "previous" link and
|
||||
* treat this operation as a refresh so the scroll position doesn't get pushed
|
||||
* down to the end. */
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(accounts, fromId, uptoId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
}
|
||||
|
||||
private void onFetchAccountsFailure(Exception exception, FetchEnd fetchEnd) {
|
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
}
|
||||
|
||||
private void onRefresh() {
|
||||
fetchAccounts(null, adapter.getTopId(), FetchEnd.TOP);
|
||||
}
|
||||
|
||||
private void onLoadMore(RecyclerView recyclerView) {
|
||||
AccountAdapter adapter = (AccountAdapter) recyclerView.getAdapter();
|
||||
fetchAccounts(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
||||
}
|
||||
|
||||
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
|
||||
switch (fetchEnd) {
|
||||
case BOTTOM: {
|
||||
bottomLoading = false;
|
||||
if (bottomFetches > 0) {
|
||||
bottomFetches--;
|
||||
Log.d(TAG, "extra fetchos " + bottomFetches);
|
||||
onLoadMore(recyclerView);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOP: {
|
||||
topLoading = false;
|
||||
if (topFetches > 0) {
|
||||
topFetches--;
|
||||
onRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import com.keylesspalace.tusky.entity.Notification;
|
|||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||
|
||||
|
@ -55,15 +56,23 @@ public class NotificationsFragment extends SFragment implements
|
|||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "Notifications"; // logging tag
|
||||
|
||||
private enum FetchEnd {
|
||||
TOP,
|
||||
BOTTOM,
|
||||
}
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private RecyclerView recyclerView;
|
||||
private EndlessOnScrollListener scrollListener;
|
||||
private NotificationsAdapter adapter;
|
||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
private Call<List<Notification>> listCall;
|
||||
private boolean hideFab;
|
||||
private TimelineReceiver timelineReceiver;
|
||||
private boolean topLoading;
|
||||
private int topFetches;
|
||||
private boolean bottomLoading;
|
||||
private int bottomFetches;
|
||||
|
||||
public static NotificationsFragment newInstance() {
|
||||
NotificationsFragment fragment = new NotificationsFragment();
|
||||
|
@ -157,27 +166,13 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
||||
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (notification != null) {
|
||||
sendFetchNotificationsRequest(notification.id, null);
|
||||
} else {
|
||||
sendFetchNotificationsRequest();
|
||||
}
|
||||
NotificationsFragment.this.onLoadMore(view);
|
||||
}
|
||||
};
|
||||
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (listCall != null) {
|
||||
listCall.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
||||
|
@ -189,88 +184,9 @@ public class NotificationsFragment extends SFragment implements
|
|||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void jumpToTop() {
|
||||
layoutManager.scrollToPosition(0);
|
||||
scrollListener.reset();
|
||||
}
|
||||
|
||||
private void sendFetchNotificationsRequest(final String fromId, String uptoId) {
|
||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.LOADING);
|
||||
}
|
||||
|
||||
listCall = mastodonAPI.notifications(fromId, uptoId, null);
|
||||
|
||||
listCall.enqueue(new Callback<List<Notification>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Notification>> call,
|
||||
Response<List<Notification>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onFetchNotificationsSuccess(response.body(), fromId);
|
||||
} else {
|
||||
onFetchNotificationsFailure(new Exception(response.message()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Notification>> call, Throwable t) {
|
||||
onFetchNotificationsFailure((Exception) t);
|
||||
}
|
||||
});
|
||||
callList.add(listCall);
|
||||
}
|
||||
|
||||
private void sendFetchNotificationsRequest() {
|
||||
sendFetchNotificationsRequest(null, null);
|
||||
}
|
||||
|
||||
private static boolean findNotification(List<Notification> notifications, String id) {
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onFetchNotificationsSuccess(List<Notification> notifications, String fromId) {
|
||||
if (fromId != null) {
|
||||
if (notifications.size() > 0 && !findNotification(notifications, fromId)) {
|
||||
adapter.addItems(notifications);
|
||||
|
||||
// Set last update id for pull notifications so that we don't get notified
|
||||
// about things we already loaded here
|
||||
SharedPreferences preferences = getActivity()
|
||||
.getSharedPreferences(getString(R.string.preferences_file_key),
|
||||
Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putString("lastUpdateId", notifications.get(0).id);
|
||||
editor.apply();
|
||||
}
|
||||
} else {
|
||||
adapter.update(notifications);
|
||||
}
|
||||
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.EMPTY);
|
||||
} else if (fromId != null) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
private void onFetchNotificationsFailure(Exception exception) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
Notification notification = adapter.getItem(0);
|
||||
if (notification != null) {
|
||||
sendFetchNotificationsRequest(null, notification.id);
|
||||
} else {
|
||||
sendFetchNotificationsRequest();
|
||||
}
|
||||
sendFetchNotificationsRequest(null, adapter.getTopId(), FetchEnd.TOP);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -334,8 +250,133 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
private void onLoadMore(RecyclerView view) {
|
||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
||||
sendFetchNotificationsRequest(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
||||
}
|
||||
|
||||
private void jumpToTop() {
|
||||
layoutManager.scrollToPosition(0);
|
||||
scrollListener.reset();
|
||||
}
|
||||
|
||||
private void sendFetchNotificationsRequest(String fromId, String uptoId,
|
||||
final FetchEnd fetchEnd) {
|
||||
/* If there is a fetch already ongoing, record however many fetches are requested and
|
||||
* fulfill them after it's complete. */
|
||||
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
||||
topFetches++;
|
||||
return;
|
||||
}
|
||||
if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) {
|
||||
bottomFetches++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.LOADING);
|
||||
}
|
||||
|
||||
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, null);
|
||||
|
||||
call.enqueue(new Callback<List<Notification>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Notification>> call,
|
||||
Response<List<Notification>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd);
|
||||
} else {
|
||||
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Notification>> call, Throwable t) {
|
||||
onFetchNotificationsFailure((Exception) t, fetchEnd);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
|
||||
FetchEnd fetchEnd) {
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||
switch (fetchEnd) {
|
||||
case TOP: {
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(notifications, null, uptoId);
|
||||
break;
|
||||
}
|
||||
case BOTTOM: {
|
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||
String fromId = null;
|
||||
if (next != null) {
|
||||
fromId = next.uri.getQueryParameter("max_id");
|
||||
}
|
||||
if (adapter.getItemCount() > 1) {
|
||||
adapter.addItems(notifications, fromId);
|
||||
} else {
|
||||
/* If this is the first fetch, also save the id from the "previous" link and
|
||||
* treat this operation as a refresh so the scroll position doesn't get pushed
|
||||
* down to the end. */
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(notifications, fromId, uptoId);
|
||||
}
|
||||
/* Set last update id for pull notifications so that we don't get notified
|
||||
* about things we already loaded here */
|
||||
getPrivatePreferences().edit()
|
||||
.putString("lastUpdateId", fromId)
|
||||
.apply();
|
||||
break;
|
||||
}
|
||||
}
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.EMPTY);
|
||||
} else {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
}
|
||||
|
||||
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
|
||||
switch (fetchEnd) {
|
||||
case BOTTOM: {
|
||||
bottomLoading = false;
|
||||
if (bottomFetches > 0) {
|
||||
bottomFetches--;
|
||||
onLoadMore(recyclerView);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOP: {
|
||||
topLoading = false;
|
||||
if (topFetches > 0) {
|
||||
topFetches--;
|
||||
onRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fullyRefresh() {
|
||||
adapter.clear();
|
||||
sendFetchNotificationsRequest(null, null);
|
||||
sendFetchNotificationsRequest(null, null, FetchEnd.TOP);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,10 +57,11 @@ import retrofit2.Response;
|
|||
* overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear
|
||||
* up what needs to be where. */
|
||||
public abstract class SFragment extends BaseFragment {
|
||||
protected static final int COMPOSE_RESULT = 1;
|
||||
|
||||
protected String loggedInAccountId;
|
||||
protected String loggedInUsername;
|
||||
protected MastodonApi mastodonAPI;
|
||||
protected static int COMPOSE_RESULT = 1;
|
||||
protected MastodonApi mastodonApi;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
@ -75,7 +76,13 @@ public abstract class SFragment extends BaseFragment {
|
|||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
BaseActivity activity = (BaseActivity) getActivity();
|
||||
mastodonAPI = activity.mastodonApi;
|
||||
mastodonApi = activity.mastodonApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivity(Intent intent) {
|
||||
super.startActivity(intent);
|
||||
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
|
||||
}
|
||||
|
||||
protected void reply(Status status) {
|
||||
|
@ -122,9 +129,9 @@ public abstract class SFragment extends BaseFragment {
|
|||
|
||||
Call<Status> call;
|
||||
if (reblog) {
|
||||
call = mastodonAPI.reblogStatus(id);
|
||||
call = mastodonApi.reblogStatus(id);
|
||||
} else {
|
||||
call = mastodonAPI.unreblogStatus(id);
|
||||
call = mastodonApi.unreblogStatus(id);
|
||||
}
|
||||
call.enqueue(cb);
|
||||
callList.add(call);
|
||||
|
@ -154,16 +161,16 @@ public abstract class SFragment extends BaseFragment {
|
|||
|
||||
Call<Status> call;
|
||||
if (favourite) {
|
||||
call = mastodonAPI.favouriteStatus(id);
|
||||
call = mastodonApi.favouriteStatus(id);
|
||||
} else {
|
||||
call = mastodonAPI.unfavouriteStatus(id);
|
||||
call = mastodonApi.unfavouriteStatus(id);
|
||||
}
|
||||
call.enqueue(cb);
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void mute(String id) {
|
||||
Call<Relationship> call = mastodonAPI.muteAccount(id);
|
||||
Call<Relationship> call = mastodonApi.muteAccount(id);
|
||||
call.enqueue(new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, Response<Relationship> response) {}
|
||||
|
@ -179,7 +186,7 @@ public abstract class SFragment extends BaseFragment {
|
|||
}
|
||||
|
||||
private void block(String id) {
|
||||
Call<Relationship> call = mastodonAPI.blockAccount(id);
|
||||
Call<Relationship> call = mastodonApi.blockAccount(id);
|
||||
call.enqueue(new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {}
|
||||
|
@ -195,7 +202,7 @@ public abstract class SFragment extends BaseFragment {
|
|||
}
|
||||
|
||||
private void delete(String id) {
|
||||
Call<ResponseBody> call = mastodonAPI.deleteStatus(id);
|
||||
Call<ResponseBody> call = mastodonApi.deleteStatus(id);
|
||||
call.enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {}
|
||||
|
@ -313,12 +320,6 @@ public abstract class SFragment extends BaseFragment {
|
|||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivity(Intent intent) {
|
||||
super.startActivity(intent);
|
||||
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
|
||||
}
|
||||
|
||||
protected void openReportPage(String accountId, String accountUsername, String statusId,
|
||||
Spanned statusContent) {
|
||||
Intent intent = new Intent(getContext(), ReportActivity.class);
|
||||
|
|
|
@ -38,7 +38,9 @@ import com.keylesspalace.tusky.R;
|
|||
import com.keylesspalace.tusky.adapter.TimelineAdapter;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||
|
||||
|
@ -64,6 +66,11 @@ public class TimelineFragment extends SFragment implements
|
|||
FAVOURITES
|
||||
}
|
||||
|
||||
private enum FetchEnd {
|
||||
TOP,
|
||||
BOTTOM,
|
||||
}
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private TimelineAdapter adapter;
|
||||
private Kind kind;
|
||||
|
@ -72,11 +79,14 @@ public class TimelineFragment extends SFragment implements
|
|||
private LinearLayoutManager layoutManager;
|
||||
private EndlessOnScrollListener scrollListener;
|
||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
private SharedPreferences preferences;
|
||||
private boolean filterRemoveReplies;
|
||||
private boolean filterRemoveReblogs;
|
||||
private boolean hideFab;
|
||||
private TimelineReceiver timelineReceiver;
|
||||
private boolean topLoading;
|
||||
private int topFetches;
|
||||
private boolean bottomLoading;
|
||||
private int bottomFetches;
|
||||
|
||||
public static TimelineFragment newInstance(Kind kind) {
|
||||
TimelineFragment fragment = new TimelineFragment();
|
||||
|
@ -198,8 +208,6 @@ public class TimelineFragment extends SFragment implements
|
|||
};
|
||||
}
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -212,20 +220,9 @@ public class TimelineFragment extends SFragment implements
|
|||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setFiltersFromSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
Status status = adapter.getItem(0);
|
||||
if (status != null) {
|
||||
sendFetchTimelineRequest(null, status.id);
|
||||
} else {
|
||||
sendFetchTimelineRequest(null, null);
|
||||
}
|
||||
sendFetchTimelineRequest(null, adapter.getTopId(), FetchEnd.TOP);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -290,22 +287,35 @@ public class TimelineFragment extends SFragment implements
|
|||
fullyRefresh();
|
||||
break;
|
||||
}
|
||||
case "tabFilterHomeReplies": {
|
||||
boolean filter = sharedPreferences.getBoolean("tabFilterHomeReplies", true);
|
||||
boolean oldRemoveReplies = filterRemoveReplies;
|
||||
filterRemoveReplies = kind == Kind.HOME && !filter;
|
||||
if (adapter.getItemCount() > 1 && oldRemoveReplies != filterRemoveReplies) {
|
||||
fullyRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "tabFilterHomeBoosts": {
|
||||
boolean filter = sharedPreferences.getBoolean("tabFilterHomeBoosts", true);
|
||||
boolean oldRemoveReblogs = filterRemoveReblogs;
|
||||
filterRemoveReblogs = kind == Kind.HOME && !filter;
|
||||
if (adapter.getItemCount() > 1 && oldRemoveReblogs != filterRemoveReblogs) {
|
||||
fullyRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onLoadMore(RecyclerView view) {
|
||||
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
|
||||
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (status != null) {
|
||||
sendFetchTimelineRequest(status.id, null);
|
||||
} else {
|
||||
sendFetchTimelineRequest(null, null);
|
||||
}
|
||||
sendFetchTimelineRequest(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
||||
}
|
||||
|
||||
private void fullyRefresh() {
|
||||
adapter.clear();
|
||||
sendFetchTimelineRequest(null, null);
|
||||
sendFetchTimelineRequest(null, null, FetchEnd.TOP);
|
||||
}
|
||||
|
||||
private boolean jumpToTopAllowed() {
|
||||
|
@ -321,7 +331,33 @@ public class TimelineFragment extends SFragment implements
|
|||
scrollListener.reset();
|
||||
}
|
||||
|
||||
private void sendFetchTimelineRequest(@Nullable final String fromId, @Nullable String uptoId) {
|
||||
private Call<List<Status>> getFetchCallByTimelineType(Kind kind, String tagOrId, String fromId,
|
||||
String uptoId) {
|
||||
MastodonApi api = mastodonApi;
|
||||
switch (kind) {
|
||||
default:
|
||||
case HOME: return api.homeTimeline(fromId, uptoId, null);
|
||||
case PUBLIC_FEDERATED: return api.publicTimeline(null, fromId, uptoId, null);
|
||||
case PUBLIC_LOCAL: return api.publicTimeline(true, fromId, uptoId, null);
|
||||
case TAG: return api.hashtagTimeline(tagOrId, null, fromId, uptoId, null);
|
||||
case USER: return api.accountStatuses(tagOrId, fromId, uptoId, null);
|
||||
case FAVOURITES: return api.favourites(fromId, uptoId, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFetchTimelineRequest(@Nullable String fromId, @Nullable String uptoId,
|
||||
final FetchEnd fetchEnd) {
|
||||
/* If there is a fetch already ongoing, record however many fetches are requested and
|
||||
* fulfill them after it's complete. */
|
||||
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
||||
topFetches++;
|
||||
return;
|
||||
}
|
||||
if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) {
|
||||
bottomFetches++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.LOADING);
|
||||
}
|
||||
|
@ -330,99 +366,104 @@ public class TimelineFragment extends SFragment implements
|
|||
@Override
|
||||
public void onResponse(Call<List<Status>> call, Response<List<Status>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onFetchTimelineSuccess(response.body(), fromId);
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd);
|
||||
} else {
|
||||
onFetchTimelineFailure(new Exception(response.message()));
|
||||
onFetchTimelineFailure(new Exception(response.message()), fetchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Status>> call, Throwable t) {
|
||||
onFetchTimelineFailure((Exception) t);
|
||||
onFetchTimelineFailure((Exception) t, fetchEnd);
|
||||
}
|
||||
};
|
||||
|
||||
Call<List<Status>> listCall;
|
||||
switch (kind) {
|
||||
default:
|
||||
case HOME: {
|
||||
listCall = mastodonAPI.homeTimeline(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case PUBLIC_FEDERATED: {
|
||||
listCall = mastodonAPI.publicTimeline(null, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case PUBLIC_LOCAL: {
|
||||
listCall = mastodonAPI.publicTimeline(true, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case TAG: {
|
||||
listCall = mastodonAPI.hashtagTimeline(hashtagOrId, null, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case USER: {
|
||||
listCall = mastodonAPI.accountStatuses(hashtagOrId, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case FAVOURITES: {
|
||||
listCall = mastodonAPI.favourites(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Call<List<Status>> listCall = getFetchCallByTimelineType(kind, hashtagOrId, fromId, uptoId);
|
||||
callList.add(listCall);
|
||||
listCall.enqueue(callback);
|
||||
}
|
||||
|
||||
private static boolean findStatus(List<Status> statuses, String id) {
|
||||
for (Status status : statuses) {
|
||||
if (status.id.equals(id)) {
|
||||
return true;
|
||||
public void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
|
||||
FetchEnd fetchEnd) {
|
||||
filterStatuses(statuses);
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||
switch (fetchEnd) {
|
||||
case TOP: {
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(statuses, null, uptoId);
|
||||
break;
|
||||
}
|
||||
case BOTTOM: {
|
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||
String fromId = null;
|
||||
if (next != null) {
|
||||
fromId = next.uri.getQueryParameter("max_id");
|
||||
}
|
||||
if (adapter.getItemCount() > 1) {
|
||||
adapter.addItems(statuses, fromId);
|
||||
} else {
|
||||
/* If this is the first fetch, also save the id from the "previous" link and
|
||||
* treat this operation as a refresh so the scroll position doesn't get pushed
|
||||
* down to the end. */
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(statuses, fromId, uptoId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
if (statuses.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.EMPTY);
|
||||
} else {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
Log.e(TAG, "Fetch Failure: " + exception.getMessage());
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
}
|
||||
|
||||
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
|
||||
switch (fetchEnd) {
|
||||
case BOTTOM: {
|
||||
bottomLoading = false;
|
||||
if (bottomFetches > 0) {
|
||||
bottomFetches--;
|
||||
onLoadMore(recyclerView);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOP: {
|
||||
topLoading = false;
|
||||
if (topFetches > 0) {
|
||||
topFetches--;
|
||||
onRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void filterStatuses(List<Status> statuses) {
|
||||
Iterator<Status> it = statuses.iterator();
|
||||
while (it.hasNext()) {
|
||||
Status status = it.next();
|
||||
if ((status.inReplyToId != null && filterRemoveReplies) || (status.reblog != null && filterRemoveReblogs)) {
|
||||
if ((status.inReplyToId != null && filterRemoveReplies)
|
||||
|| (status.reblog != null && filterRemoveReblogs)) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void setFiltersFromSettings() {
|
||||
boolean oldRemoveReplies = filterRemoveReplies;
|
||||
boolean oldRemoveReblogs = filterRemoveReblogs;
|
||||
filterRemoveReplies = (kind == Kind.HOME && !preferences.getBoolean("tabFilterHomeReplies", true));
|
||||
filterRemoveReblogs = (kind == Kind.HOME && !preferences.getBoolean("tabFilterHomeBoosts", true));
|
||||
|
||||
if (adapter.getItemCount() > 1 && (oldRemoveReblogs != filterRemoveReblogs || oldRemoveReplies != filterRemoveReplies)) {
|
||||
fullyRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void onFetchTimelineSuccess(List<Status> statuses, String fromId) {
|
||||
filterStatuses(statuses);
|
||||
if (fromId != null) {
|
||||
if (statuses.size() > 0 && !findStatus(statuses, fromId)) {
|
||||
adapter.addItems(statuses);
|
||||
}
|
||||
} else {
|
||||
adapter.update(statuses);
|
||||
}
|
||||
if (statuses.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.EMPTY);
|
||||
} else if(fromId != null) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchTimelineFailure(Exception exception) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
Log.e(TAG, "Fetch Failure: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,10 +35,8 @@ import android.view.ViewGroup;
|
|||
|
||||
|
||||
import com.keylesspalace.tusky.adapter.ThreadAdapter;
|
||||
import com.keylesspalace.tusky.BaseActivity;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.StatusContext;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
|
@ -56,7 +54,6 @@ public class ViewThreadFragment extends SFragment implements
|
|||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private RecyclerView recyclerView;
|
||||
private ThreadAdapter adapter;
|
||||
private MastodonApi mastodonApi;
|
||||
private String thisThreadsStatusId;
|
||||
private TimelineReceiver timelineReceiver;
|
||||
|
||||
|
@ -97,7 +94,6 @@ public class ViewThreadFragment extends SFragment implements
|
|||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
mastodonApi = null;
|
||||
thisThreadsStatusId = null;
|
||||
|
||||
timelineReceiver = new TimelineReceiver(adapter, this);
|
||||
|
@ -117,77 +113,10 @@ public class ViewThreadFragment extends SFragment implements
|
|||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
/* BaseActivity's MastodonApi object isn't guaranteed to be valid until after its onCreate
|
||||
* is run, so all calls that need it can't be done until here. */
|
||||
mastodonApi = ((BaseActivity) getActivity()).mastodonApi;
|
||||
|
||||
thisThreadsStatusId = getArguments().getString("id");
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
private void sendStatusRequest(final String id) {
|
||||
Call<Status> call = mastodonApi.status(id);
|
||||
call.enqueue(new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(Call<Status> call, Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
int position = adapter.setStatus(response.body());
|
||||
recyclerView.scrollToPosition(position);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Status> call, Throwable t) {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void sendThreadRequest(final String id) {
|
||||
Call<StatusContext> call = mastodonApi.statusContext(id);
|
||||
call.enqueue(new Callback<StatusContext>() {
|
||||
@Override
|
||||
public void onResponse(Call<StatusContext> call, Response<StatusContext> response) {
|
||||
if (response.isSuccessful()) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
StatusContext context = response.body();
|
||||
|
||||
adapter.setContext(context.ancestors, context.descendants);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<StatusContext> call, Throwable t) {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void onThreadRequestFailure(final String id) {
|
||||
View view = getView();
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
if (view != null) {
|
||||
Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sendThreadRequest(id);
|
||||
sendStatusRequest(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
Log.e(TAG, "Couldn't display thread fetch error message");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
sendStatusRequest(thisThreadsStatusId);
|
||||
|
@ -238,4 +167,65 @@ public class ViewThreadFragment extends SFragment implements
|
|||
public void onViewAccount(String id) {
|
||||
super.viewAccount(id);
|
||||
}
|
||||
|
||||
private void sendStatusRequest(final String id) {
|
||||
Call<Status> call = mastodonApi.status(id);
|
||||
call.enqueue(new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(Call<Status> call, Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
int position = adapter.setStatus(response.body());
|
||||
recyclerView.scrollToPosition(position);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Status> call, Throwable t) {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void sendThreadRequest(final String id) {
|
||||
Call<StatusContext> call = mastodonApi.statusContext(id);
|
||||
call.enqueue(new Callback<StatusContext>() {
|
||||
@Override
|
||||
public void onResponse(Call<StatusContext> call, Response<StatusContext> response) {
|
||||
if (response.isSuccessful()) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
StatusContext context = response.body();
|
||||
adapter.setContext(context.ancestors, context.descendants);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<StatusContext> call, Throwable t) {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void onThreadRequestFailure(final String id) {
|
||||
View view = getView();
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
if (view != null) {
|
||||
Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sendThreadRequest(id);
|
||||
sendStatusRequest(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
Log.e(TAG, "Couldn't display thread fetch error message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/* Written in 2017 by Andrew Dawson
|
||||
*
|
||||
* To the extent possible under law, the author(s) have dedicated all copyright and related and
|
||||
* neighboring rights to this software to the public domain worldwide. This software is distributed
|
||||
* without any warranty.
|
||||
*
|
||||
* You should have received a copy of the CC0 Public Domain Dedication along with this software.
|
||||
* If not, see <http://creativecommons.org/publicdomain/zero/1.0/>. */
|
||||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class HttpHeaderLink {
|
||||
private static class Parameter {
|
||||
public String name;
|
||||
public String value;
|
||||
}
|
||||
|
||||
private List<Parameter> parameters;
|
||||
public Uri uri;
|
||||
|
||||
private HttpHeaderLink(String uri) {
|
||||
this.uri = Uri.parse(uri);
|
||||
this.parameters = new ArrayList<>();
|
||||
}
|
||||
|
||||
private static int findAny(String s, int fromIndex, char[] set) {
|
||||
for (int i = fromIndex; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
for (char member : set) {
|
||||
if (c == member) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int findEndOfQuotedString(String line, int start) {
|
||||
for (int i = start; i < line.length(); i++) {
|
||||
char c = line.charAt(i);
|
||||
if (c == '\\') {
|
||||
i += 1;
|
||||
} else if (c == '"') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static class ValueResult {
|
||||
String value;
|
||||
int end;
|
||||
|
||||
ValueResult() {
|
||||
end = -1;
|
||||
}
|
||||
|
||||
void setValue(String value) {
|
||||
value = value.trim();
|
||||
if (!value.isEmpty()) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ValueResult parseValue(String line, int start) {
|
||||
ValueResult result = new ValueResult();
|
||||
int foundIndex = findAny(line, start, new char[] {';', ',', '"'});
|
||||
if (foundIndex == -1) {
|
||||
result.setValue(line.substring(start));
|
||||
return result;
|
||||
}
|
||||
char c = line.charAt(foundIndex);
|
||||
if (c == ';' || c == ',') {
|
||||
result.end = foundIndex;
|
||||
result.setValue(line.substring(start, foundIndex));
|
||||
return result;
|
||||
} else {
|
||||
int quoteEnd = findEndOfQuotedString(line, foundIndex + 1);
|
||||
if (quoteEnd == -1) {
|
||||
quoteEnd = line.length();
|
||||
}
|
||||
result.end = quoteEnd;
|
||||
result.setValue(line.substring(foundIndex + 1, quoteEnd));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseParameters(String line, int start, HttpHeaderLink link) {
|
||||
for (int i = start; i < line.length(); i++) {
|
||||
int foundIndex = findAny(line, i, new char[] {'=', ','});
|
||||
if (foundIndex == -1) {
|
||||
return -1;
|
||||
} else if (line.charAt(foundIndex) == ',') {
|
||||
return foundIndex;
|
||||
}
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim();
|
||||
link.parameters.add(parameter);
|
||||
ValueResult result = parseValue(line, foundIndex);
|
||||
parameter.value = result.value;
|
||||
if (result.end == -1) {
|
||||
return -1;
|
||||
} else {
|
||||
i = result.end;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static List<HttpHeaderLink> parse(@Nullable String line) {
|
||||
List<HttpHeaderLink> linkList = new ArrayList<>();
|
||||
if (line != null) {
|
||||
for (int i = 0; i < line.length(); i++) {
|
||||
int uriEnd = line.indexOf('>', i);
|
||||
String uri = line.substring(line.indexOf('<', i) + 1, uriEnd);
|
||||
HttpHeaderLink link = new HttpHeaderLink(uri);
|
||||
linkList.add(link);
|
||||
int parseEnd = parseParameters(line, uriEnd, link);
|
||||
if (parseEnd == -1) {
|
||||
break;
|
||||
} else {
|
||||
i = parseEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
return linkList;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static HttpHeaderLink findByRelationType(List<HttpHeaderLink> links,
|
||||
String relationType) {
|
||||
for (HttpHeaderLink link : links) {
|
||||
for (Parameter parameter : link.parameters) {
|
||||
if (parameter.name.equals("rel") && parameter.value.equals(relationType)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -115,7 +115,7 @@
|
|||
<string name="hint_content_warning">Content warning</string>
|
||||
<string name="hint_display_name">Display name</string>
|
||||
<string name="hint_note">Bio</string>
|
||||
<string name="hint_search">Search accounts and tags…</string>
|
||||
<string name="hint_search">Search…</string>
|
||||
|
||||
<string name="search_no_results">No results</string>
|
||||
|
||||
|
|
Loading…
Reference in New Issue