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.entity.Account;
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class AccountAdapter extends RecyclerView.Adapter {
|
public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||||
List<Account> accountList;
|
List<Account> accountList;
|
||||||
AccountActionListener accountActionListener;
|
AccountActionListener accountActionListener;
|
||||||
|
private String topId;
|
||||||
|
private String bottomId;
|
||||||
|
|
||||||
AccountAdapter(AccountActionListener accountActionListener) {
|
AccountAdapter(AccountActionListener accountActionListener) {
|
||||||
super();
|
super();
|
||||||
|
@ -39,12 +43,20 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||||
return accountList.size() + 1;
|
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()) {
|
if (newAccounts == null || newAccounts.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (fromId != null) {
|
||||||
|
bottomId = fromId;
|
||||||
|
}
|
||||||
|
if (uptoId != null) {
|
||||||
|
topId = uptoId;
|
||||||
|
}
|
||||||
if (accountList.isEmpty()) {
|
if (accountList.isEmpty()) {
|
||||||
accountList = newAccounts;
|
// This construction removes duplicates.
|
||||||
|
accountList = new ArrayList<>(new HashSet<>(newAccounts));
|
||||||
} else {
|
} else {
|
||||||
int index = accountList.indexOf(newAccounts.get(newAccounts.size() - 1));
|
int index = accountList.indexOf(newAccounts.get(newAccounts.size() - 1));
|
||||||
for (int i = 0; i < index; i++) {
|
for (int i = 0; i < index; i++) {
|
||||||
|
@ -60,11 +72,26 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||||
notifyDataSetChanged();
|
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();
|
int end = accountList.size();
|
||||||
|
Account last = accountList.get(end - 1);
|
||||||
|
if (last != null && !findAccount(accountList, last.id)) {
|
||||||
accountList.addAll(newAccounts);
|
accountList.addAll(newAccounts);
|
||||||
notifyItemRangeInserted(end, newAccounts.size());
|
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
|
@Nullable
|
||||||
public Account removeItem(int position) {
|
public Account removeItem(int position) {
|
||||||
|
@ -84,10 +111,21 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||||
notifyItemInserted(position);
|
notifyItemInserted(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public Account getItem(int position) {
|
public Account getItem(int position) {
|
||||||
if (position >= 0 && position < accountList.size()) {
|
if (position >= 0 && position < accountList.size()) {
|
||||||
return accountList.get(position);
|
return accountList.get(position);
|
||||||
}
|
}
|
||||||
return null;
|
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 com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
public class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||||
|
@ -56,6 +57,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
private NotificationActionListener notificationActionListener;
|
private NotificationActionListener notificationActionListener;
|
||||||
private FooterState footerState = FooterState.END;
|
private FooterState footerState = FooterState.END;
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
|
private String bottomId;
|
||||||
|
private String topId;
|
||||||
|
|
||||||
public NotificationsAdapter(StatusActionListener statusListener,
|
public NotificationsAdapter(StatusActionListener statusListener,
|
||||||
NotificationActionListener notificationActionListener) {
|
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()) {
|
if (position >= 0 && position < notifications.size()) {
|
||||||
return notifications.get(position);
|
return notifications.get(position);
|
||||||
}
|
}
|
||||||
return null;
|
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()) {
|
if (newNotifications == null || newNotifications.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (fromId != null) {
|
||||||
|
bottomId = fromId;
|
||||||
|
}
|
||||||
|
if (uptoId != null) {
|
||||||
|
topId = uptoId;
|
||||||
|
}
|
||||||
if (notifications.isEmpty()) {
|
if (notifications.isEmpty()) {
|
||||||
notifications = newNotifications;
|
// This construction removes duplicates.
|
||||||
|
notifications = new ArrayList<>(new HashSet<>(newNotifications));
|
||||||
} else {
|
} else {
|
||||||
int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1));
|
int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1));
|
||||||
for (int i = 0; i < index; i++) {
|
for (int i = 0; i < index; i++) {
|
||||||
|
@ -214,10 +226,25 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
notifyDataSetChanged();
|
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();
|
int end = notifications.size();
|
||||||
notifications.addAll(new_notifications);
|
Notification last = notifications.get(end - 1);
|
||||||
notifyItemRangeInserted(end, new_notifications.size());
|
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() {
|
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) {
|
public void setMediaPreviewEnabled(boolean enabled) {
|
||||||
mediaPreviewEnabled = enabled;
|
mediaPreviewEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||||
|
@ -43,6 +44,8 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
||||||
private StatusActionListener statusListener;
|
private StatusActionListener statusListener;
|
||||||
private FooterState footerState = FooterState.END;
|
private FooterState footerState = FooterState.END;
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
|
private String topId;
|
||||||
|
private String bottomId;
|
||||||
|
|
||||||
public TimelineAdapter(StatusActionListener statusListener) {
|
public TimelineAdapter(StatusActionListener statusListener) {
|
||||||
super();
|
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()) {
|
if (newStatuses == null || newStatuses.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (fromId != null) {
|
||||||
|
bottomId = fromId;
|
||||||
|
}
|
||||||
|
if (uptoId != null) {
|
||||||
|
topId = uptoId;
|
||||||
|
}
|
||||||
if (statuses.isEmpty()) {
|
if (statuses.isEmpty()) {
|
||||||
statuses = newStatuses;
|
// This construction removes duplicates.
|
||||||
|
statuses = new ArrayList<>(new HashSet<>(newStatuses));
|
||||||
} else {
|
} else {
|
||||||
int index = statuses.indexOf(newStatuses.get(newStatuses.size() - 1));
|
int index = statuses.indexOf(newStatuses.get(newStatuses.size() - 1));
|
||||||
for (int i = 0; i < index; i++) {
|
for (int i = 0; i < index; i++) {
|
||||||
|
@ -147,11 +158,26 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
||||||
notifyDataSetChanged();
|
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();
|
int end = statuses.size();
|
||||||
|
Status last = statuses.get(end - 1);
|
||||||
|
if (last != null && !findStatus(statuses, last.id)) {
|
||||||
statuses.addAll(newStatuses);
|
statuses.addAll(newStatuses);
|
||||||
notifyItemRangeInserted(end, newStatuses.size());
|
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() {
|
public void clear() {
|
||||||
statuses.clear();
|
statuses.clear();
|
||||||
|
@ -177,4 +203,14 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
||||||
public void setMediaPreviewEnabled(boolean enabled) {
|
public void setMediaPreviewEnabled(boolean enabled) {
|
||||||
mediaPreviewEnabled = 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.interfaces.AccountActionListener;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||||
|
|
||||||
|
@ -71,6 +72,10 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
private AccountAdapter adapter;
|
private AccountAdapter adapter;
|
||||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||||
private MastodonApi api;
|
private MastodonApi api;
|
||||||
|
private boolean bottomLoading;
|
||||||
|
private int bottomFetches;
|
||||||
|
private boolean topLoading;
|
||||||
|
private int topFetches;
|
||||||
|
|
||||||
public static AccountListFragment newInstance(Type type) {
|
public static AccountListFragment newInstance(Type type) {
|
||||||
Bundle arguments = new Bundle();
|
Bundle arguments = new Bundle();
|
||||||
|
@ -160,13 +165,7 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||||
AccountAdapter adapter = (AccountAdapter) view.getAdapter();
|
AccountListFragment.this.onLoadMore(view);
|
||||||
Account account = adapter.getItem(adapter.getItemCount() - 2);
|
|
||||||
if (account != null) {
|
|
||||||
fetchAccounts(account.id, null);
|
|
||||||
} else {
|
|
||||||
fetchAccounts();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
recyclerView.addOnScrollListener(scrollListener);
|
recyclerView.addOnScrollListener(scrollListener);
|
||||||
|
@ -181,78 +180,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
super.onDestroyView();
|
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
|
@Override
|
||||||
public void onViewAccount(String id) {
|
public void onViewAccount(String id) {
|
||||||
Intent intent = new Intent(getContext(), AccountActivity.class);
|
Intent intent = new Intent(getContext(), AccountActivity.class);
|
||||||
|
@ -444,4 +371,126 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
layoutManager.scrollToPositionWithOffset(0, 0);
|
layoutManager.scrollToPositionWithOffset(0, 0);
|
||||||
scrollListener.reset();
|
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.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||||
|
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||||
|
|
||||||
|
@ -55,15 +56,23 @@ public class NotificationsFragment extends SFragment implements
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private static final String TAG = "Notifications"; // logging tag
|
private static final String TAG = "Notifications"; // logging tag
|
||||||
|
|
||||||
|
private enum FetchEnd {
|
||||||
|
TOP,
|
||||||
|
BOTTOM,
|
||||||
|
}
|
||||||
|
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private LinearLayoutManager layoutManager;
|
private LinearLayoutManager layoutManager;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private EndlessOnScrollListener scrollListener;
|
private EndlessOnScrollListener scrollListener;
|
||||||
private NotificationsAdapter adapter;
|
private NotificationsAdapter adapter;
|
||||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||||
private Call<List<Notification>> listCall;
|
|
||||||
private boolean hideFab;
|
private boolean hideFab;
|
||||||
private TimelineReceiver timelineReceiver;
|
private TimelineReceiver timelineReceiver;
|
||||||
|
private boolean topLoading;
|
||||||
|
private int topFetches;
|
||||||
|
private boolean bottomLoading;
|
||||||
|
private int bottomFetches;
|
||||||
|
|
||||||
public static NotificationsFragment newInstance() {
|
public static NotificationsFragment newInstance() {
|
||||||
NotificationsFragment fragment = new NotificationsFragment();
|
NotificationsFragment fragment = new NotificationsFragment();
|
||||||
|
@ -157,27 +166,13 @@ public class NotificationsFragment extends SFragment implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
NotificationsFragment.this.onLoadMore(view);
|
||||||
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
|
||||||
if (notification != null) {
|
|
||||||
sendFetchNotificationsRequest(notification.id, null);
|
|
||||||
} else {
|
|
||||||
sendFetchNotificationsRequest();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(scrollListener);
|
recyclerView.addOnScrollListener(scrollListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
if (listCall != null) {
|
|
||||||
listCall.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
||||||
|
@ -189,88 +184,9 @@ public class NotificationsFragment extends SFragment implements
|
||||||
super.onDestroyView();
|
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
|
@Override
|
||||||
public void onRefresh() {
|
public void onRefresh() {
|
||||||
Notification notification = adapter.getItem(0);
|
sendFetchNotificationsRequest(null, adapter.getTopId(), FetchEnd.TOP);
|
||||||
if (notification != null) {
|
|
||||||
sendFetchNotificationsRequest(null, notification.id);
|
|
||||||
} else {
|
|
||||||
sendFetchNotificationsRequest();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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() {
|
private void fullyRefresh() {
|
||||||
adapter.clear();
|
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
|
* overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear
|
||||||
* up what needs to be where. */
|
* up what needs to be where. */
|
||||||
public abstract class SFragment extends BaseFragment {
|
public abstract class SFragment extends BaseFragment {
|
||||||
|
protected static final int COMPOSE_RESULT = 1;
|
||||||
|
|
||||||
protected String loggedInAccountId;
|
protected String loggedInAccountId;
|
||||||
protected String loggedInUsername;
|
protected String loggedInUsername;
|
||||||
protected MastodonApi mastodonAPI;
|
protected MastodonApi mastodonApi;
|
||||||
protected static int COMPOSE_RESULT = 1;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
@ -75,7 +76,13 @@ public abstract class SFragment extends BaseFragment {
|
||||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
BaseActivity activity = (BaseActivity) getActivity();
|
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) {
|
protected void reply(Status status) {
|
||||||
|
@ -122,9 +129,9 @@ public abstract class SFragment extends BaseFragment {
|
||||||
|
|
||||||
Call<Status> call;
|
Call<Status> call;
|
||||||
if (reblog) {
|
if (reblog) {
|
||||||
call = mastodonAPI.reblogStatus(id);
|
call = mastodonApi.reblogStatus(id);
|
||||||
} else {
|
} else {
|
||||||
call = mastodonAPI.unreblogStatus(id);
|
call = mastodonApi.unreblogStatus(id);
|
||||||
}
|
}
|
||||||
call.enqueue(cb);
|
call.enqueue(cb);
|
||||||
callList.add(call);
|
callList.add(call);
|
||||||
|
@ -154,16 +161,16 @@ public abstract class SFragment extends BaseFragment {
|
||||||
|
|
||||||
Call<Status> call;
|
Call<Status> call;
|
||||||
if (favourite) {
|
if (favourite) {
|
||||||
call = mastodonAPI.favouriteStatus(id);
|
call = mastodonApi.favouriteStatus(id);
|
||||||
} else {
|
} else {
|
||||||
call = mastodonAPI.unfavouriteStatus(id);
|
call = mastodonApi.unfavouriteStatus(id);
|
||||||
}
|
}
|
||||||
call.enqueue(cb);
|
call.enqueue(cb);
|
||||||
callList.add(call);
|
callList.add(call);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mute(String id) {
|
private void mute(String id) {
|
||||||
Call<Relationship> call = mastodonAPI.muteAccount(id);
|
Call<Relationship> call = mastodonApi.muteAccount(id);
|
||||||
call.enqueue(new Callback<Relationship>() {
|
call.enqueue(new Callback<Relationship>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<Relationship> call, Response<Relationship> response) {}
|
public void onResponse(Call<Relationship> call, Response<Relationship> response) {}
|
||||||
|
@ -179,7 +186,7 @@ public abstract class SFragment extends BaseFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void block(String id) {
|
private void block(String id) {
|
||||||
Call<Relationship> call = mastodonAPI.blockAccount(id);
|
Call<Relationship> call = mastodonApi.blockAccount(id);
|
||||||
call.enqueue(new Callback<Relationship>() {
|
call.enqueue(new Callback<Relationship>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {}
|
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) {
|
private void delete(String id) {
|
||||||
Call<ResponseBody> call = mastodonAPI.deleteStatus(id);
|
Call<ResponseBody> call = mastodonApi.deleteStatus(id);
|
||||||
call.enqueue(new Callback<ResponseBody>() {
|
call.enqueue(new Callback<ResponseBody>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {}
|
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {}
|
||||||
|
@ -313,12 +320,6 @@ public abstract class SFragment extends BaseFragment {
|
||||||
startActivity(intent);
|
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,
|
protected void openReportPage(String accountId, String accountUsername, String statusId,
|
||||||
Spanned statusContent) {
|
Spanned statusContent) {
|
||||||
Intent intent = new Intent(getContext(), ReportActivity.class);
|
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.adapter.TimelineAdapter;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||||
|
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||||
|
|
||||||
|
@ -64,6 +66,11 @@ public class TimelineFragment extends SFragment implements
|
||||||
FAVOURITES
|
FAVOURITES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum FetchEnd {
|
||||||
|
TOP,
|
||||||
|
BOTTOM,
|
||||||
|
}
|
||||||
|
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private TimelineAdapter adapter;
|
private TimelineAdapter adapter;
|
||||||
private Kind kind;
|
private Kind kind;
|
||||||
|
@ -72,11 +79,14 @@ public class TimelineFragment extends SFragment implements
|
||||||
private LinearLayoutManager layoutManager;
|
private LinearLayoutManager layoutManager;
|
||||||
private EndlessOnScrollListener scrollListener;
|
private EndlessOnScrollListener scrollListener;
|
||||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||||
private SharedPreferences preferences;
|
|
||||||
private boolean filterRemoveReplies;
|
private boolean filterRemoveReplies;
|
||||||
private boolean filterRemoveReblogs;
|
private boolean filterRemoveReblogs;
|
||||||
private boolean hideFab;
|
private boolean hideFab;
|
||||||
private TimelineReceiver timelineReceiver;
|
private TimelineReceiver timelineReceiver;
|
||||||
|
private boolean topLoading;
|
||||||
|
private int topFetches;
|
||||||
|
private boolean bottomLoading;
|
||||||
|
private int bottomFetches;
|
||||||
|
|
||||||
public static TimelineFragment newInstance(Kind kind) {
|
public static TimelineFragment newInstance(Kind kind) {
|
||||||
TimelineFragment fragment = new TimelineFragment();
|
TimelineFragment fragment = new TimelineFragment();
|
||||||
|
@ -198,8 +208,6 @@ public class TimelineFragment extends SFragment implements
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
recyclerView.addOnScrollListener(scrollListener);
|
recyclerView.addOnScrollListener(scrollListener);
|
||||||
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -212,20 +220,9 @@ public class TimelineFragment extends SFragment implements
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
setFiltersFromSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRefresh() {
|
public void onRefresh() {
|
||||||
Status status = adapter.getItem(0);
|
sendFetchTimelineRequest(null, adapter.getTopId(), FetchEnd.TOP);
|
||||||
if (status != null) {
|
|
||||||
sendFetchTimelineRequest(null, status.id);
|
|
||||||
} else {
|
|
||||||
sendFetchTimelineRequest(null, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -290,22 +287,35 @@ public class TimelineFragment extends SFragment implements
|
||||||
fullyRefresh();
|
fullyRefresh();
|
||||||
break;
|
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) {
|
private void onLoadMore(RecyclerView view) {
|
||||||
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
|
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
|
||||||
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
sendFetchTimelineRequest(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
||||||
if (status != null) {
|
|
||||||
sendFetchTimelineRequest(status.id, null);
|
|
||||||
} else {
|
|
||||||
sendFetchTimelineRequest(null, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fullyRefresh() {
|
private void fullyRefresh() {
|
||||||
adapter.clear();
|
adapter.clear();
|
||||||
sendFetchTimelineRequest(null, null);
|
sendFetchTimelineRequest(null, null, FetchEnd.TOP);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean jumpToTopAllowed() {
|
private boolean jumpToTopAllowed() {
|
||||||
|
@ -321,7 +331,33 @@ public class TimelineFragment extends SFragment implements
|
||||||
scrollListener.reset();
|
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) {
|
if (fromId != null || adapter.getItemCount() <= 1) {
|
||||||
adapter.setFooterState(TimelineAdapter.FooterState.LOADING);
|
adapter.setFooterState(TimelineAdapter.FooterState.LOADING);
|
||||||
}
|
}
|
||||||
|
@ -330,99 +366,104 @@ public class TimelineFragment extends SFragment implements
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<List<Status>> call, Response<List<Status>> response) {
|
public void onResponse(Call<List<Status>> call, Response<List<Status>> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
onFetchTimelineSuccess(response.body(), fromId);
|
String linkHeader = response.headers().get("Link");
|
||||||
|
onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd);
|
||||||
} else {
|
} else {
|
||||||
onFetchTimelineFailure(new Exception(response.message()));
|
onFetchTimelineFailure(new Exception(response.message()), fetchEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<List<Status>> call, Throwable t) {
|
public void onFailure(Call<List<Status>> call, Throwable t) {
|
||||||
onFetchTimelineFailure((Exception) t);
|
onFetchTimelineFailure((Exception) t, fetchEnd);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Call<List<Status>> listCall;
|
Call<List<Status>> listCall = getFetchCallByTimelineType(kind, hashtagOrId, fromId, uptoId);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callList.add(listCall);
|
callList.add(listCall);
|
||||||
listCall.enqueue(callback);
|
listCall.enqueue(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean findStatus(List<Status> statuses, String id) {
|
public void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
|
||||||
for (Status status : statuses) {
|
FetchEnd fetchEnd) {
|
||||||
if (status.id.equals(id)) {
|
filterStatuses(statuses);
|
||||||
return true;
|
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) {
|
protected void filterStatuses(List<Status> statuses) {
|
||||||
Iterator<Status> it = statuses.iterator();
|
Iterator<Status> it = statuses.iterator();
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Status status = it.next();
|
Status status = it.next();
|
||||||
if ((status.inReplyToId != null && filterRemoveReplies) || (status.reblog != null && filterRemoveReblogs)) {
|
if ((status.inReplyToId != null && filterRemoveReplies)
|
||||||
|
|| (status.reblog != null && filterRemoveReblogs)) {
|
||||||
it.remove();
|
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.adapter.ThreadAdapter;
|
||||||
import com.keylesspalace.tusky.BaseActivity;
|
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.entity.StatusContext;
|
import com.keylesspalace.tusky.entity.StatusContext;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||||
|
@ -56,7 +54,6 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private ThreadAdapter adapter;
|
private ThreadAdapter adapter;
|
||||||
private MastodonApi mastodonApi;
|
|
||||||
private String thisThreadsStatusId;
|
private String thisThreadsStatusId;
|
||||||
private TimelineReceiver timelineReceiver;
|
private TimelineReceiver timelineReceiver;
|
||||||
|
|
||||||
|
@ -97,7 +94,6 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
mastodonApi = null;
|
|
||||||
thisThreadsStatusId = null;
|
thisThreadsStatusId = null;
|
||||||
|
|
||||||
timelineReceiver = new TimelineReceiver(adapter, this);
|
timelineReceiver = new TimelineReceiver(adapter, this);
|
||||||
|
@ -117,77 +113,10 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
super.onActivityCreated(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");
|
thisThreadsStatusId = getArguments().getString("id");
|
||||||
onRefresh();
|
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
|
@Override
|
||||||
public void onRefresh() {
|
public void onRefresh() {
|
||||||
sendStatusRequest(thisThreadsStatusId);
|
sendStatusRequest(thisThreadsStatusId);
|
||||||
|
@ -238,4 +167,65 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
public void onViewAccount(String id) {
|
public void onViewAccount(String id) {
|
||||||
super.viewAccount(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_content_warning">Content warning</string>
|
||||||
<string name="hint_display_name">Display name</string>
|
<string name="hint_display_name">Display name</string>
|
||||||
<string name="hint_note">Bio</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>
|
<string name="search_no_results">No results</string>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue