Merge pull request #435 from Vavassor/timeline-improvement
"load more" Placeholder
This commit is contained in:
commit
7ae771db4c
21 changed files with 999 additions and 414 deletions
|
@ -47,6 +47,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
|
||||
private static final int VIEW_TYPE_FOLLOW = 3;
|
||||
private static final int VIEW_TYPE_PLACEHOLDER = 4;
|
||||
|
||||
private List<NotificationViewData> notifications;
|
||||
private StatusActionListener statusListener;
|
||||
|
@ -88,6 +89,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
.inflate(R.layout.item_follow, parent, false);
|
||||
return new FollowViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_PLACEHOLDER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_status_placeholder, parent, false);
|
||||
return new PlaceholderViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,11 +101,19 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position < notifications.size()) {
|
||||
NotificationViewData notification = notifications.get(position);
|
||||
Notification.Type type = notification.getType();
|
||||
if (notification instanceof NotificationViewData.Placeholder) {
|
||||
NotificationViewData.Placeholder placeholder = ((NotificationViewData.Placeholder) notification);
|
||||
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
|
||||
holder.setup(!placeholder.isLoading(), statusListener);
|
||||
return;
|
||||
}
|
||||
NotificationViewData.Concrete concreteNotificaton =
|
||||
(NotificationViewData.Concrete) notification;
|
||||
Notification.Type type = concreteNotificaton.getType();
|
||||
switch (type) {
|
||||
case MENTION: {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
StatusViewData status = notification.getStatusViewData();
|
||||
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
|
||||
holder.setupWithStatus(status,
|
||||
statusListener, mediaPreviewEnabled);
|
||||
break;
|
||||
|
@ -107,18 +121,18 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
case FAVOURITE:
|
||||
case REBLOG: {
|
||||
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
||||
holder.setMessage(type, notification.getAccount().getDisplayName(),
|
||||
notification.getStatusViewData());
|
||||
holder.setupButtons(notificationActionListener, notification.getAccount().id);
|
||||
holder.setAvatars(notification.getStatusViewData().getAvatar(),
|
||||
notification.getAccount().avatar);
|
||||
holder.setMessage(type, concreteNotificaton.getAccount().getDisplayName(),
|
||||
concreteNotificaton.getStatusViewData());
|
||||
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().id);
|
||||
holder.setAvatars(concreteNotificaton.getStatusViewData().getAvatar(),
|
||||
concreteNotificaton.getAccount().avatar);
|
||||
break;
|
||||
}
|
||||
case FOLLOW: {
|
||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
||||
holder.setMessage(notification.getAccount().getDisplayName(),
|
||||
notification.getAccount().username, notification.getAccount().avatar);
|
||||
holder.setupButtons(notificationActionListener, notification.getAccount().id);
|
||||
holder.setMessage(concreteNotificaton.getAccount().getDisplayName(),
|
||||
concreteNotificaton.getAccount().username, concreteNotificaton.getAccount().avatar);
|
||||
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -139,7 +153,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
NotificationViewData notification = notifications.get(position);
|
||||
switch (notification.getType()) {
|
||||
if (notification instanceof NotificationViewData.Concrete) {
|
||||
NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification);
|
||||
switch (concrete.getType()) {
|
||||
default:
|
||||
case MENTION: {
|
||||
return VIEW_TYPE_MENTION;
|
||||
|
@ -152,6 +168,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
return VIEW_TYPE_FOLLOW;
|
||||
}
|
||||
}
|
||||
} else if (notification instanceof NotificationViewData.Placeholder) {
|
||||
return VIEW_TYPE_PLACEHOLDER;
|
||||
} else {
|
||||
throw new AssertionError("Unknown notification type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,7 +279,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
notificationAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
|
||||
void setMessage(Notification.Type type, String displayName, StatusViewData status) {
|
||||
void setMessage(Notification.Type type, String displayName,
|
||||
StatusViewData.Concrete status) {
|
||||
Context context = message.getContext();
|
||||
String format;
|
||||
switch (type) {
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
|
||||
public class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private Button loadMoreButton;
|
||||
|
||||
|
||||
PlaceholderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
loadMoreButton = itemView.findViewById(R.id.button_load_more);
|
||||
|
||||
}
|
||||
|
||||
public void setup(boolean enabled, final StatusActionListener listener){
|
||||
loadMoreButton.setEnabled(enabled);
|
||||
if(enabled) {
|
||||
loadMoreButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
loadMoreButton.setEnabled(false);
|
||||
listener.onLoadMore(getAdapterPosition());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -271,6 +271,9 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
sensitiveMediaShow.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
listener.onContentHiddenChange(false, getAdapterPosition());
|
||||
}
|
||||
v.setVisibility(View.GONE);
|
||||
sensitiveMediaWarning.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
@ -470,7 +473,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
container.setOnClickListener(viewThreadListener);
|
||||
}
|
||||
|
||||
void setupWithStatus(StatusViewData status, final StatusActionListener listener,
|
||||
void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
setDisplayName(status.getUserFullName());
|
||||
setUsername(status.getNickname());
|
||||
|
|
|
@ -85,7 +85,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
void setupWithStatus(final StatusViewData status, final StatusActionListener listener,
|
||||
void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
super.setupWithStatus(status, listener, mediaPreviewEnabled);
|
||||
reblogs.setText(status.getReblogsCount());
|
||||
|
|
|
@ -67,7 +67,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
void setupWithStatus(StatusViewData status, final StatusActionListener listener,
|
||||
void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
super.setupWithStatus(status, listener, mediaPreviewEnabled);
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
private static final int VIEW_TYPE_STATUS = 0;
|
||||
private static final int VIEW_TYPE_STATUS_DETAILED = 1;
|
||||
|
||||
private List<StatusViewData> statuses;
|
||||
private List<StatusViewData.Concrete> statuses;
|
||||
private StatusActionListener statusActionListener;
|
||||
private boolean mediaPreviewEnabled;
|
||||
private int detailedStatusPosition;
|
||||
|
@ -66,13 +66,12 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
StatusViewData.Concrete status = statuses.get(position);
|
||||
if (position == detailedStatusPosition) {
|
||||
StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder;
|
||||
StatusViewData status = statuses.get(position);
|
||||
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled);
|
||||
} else {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
StatusViewData status = statuses.get(position);
|
||||
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled);
|
||||
}
|
||||
}
|
||||
|
@ -91,13 +90,13 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
return statuses.size();
|
||||
}
|
||||
|
||||
public void setStatuses(List<StatusViewData> statuses) {
|
||||
public void setStatuses(List<StatusViewData.Concrete> statuses) {
|
||||
this.statuses.clear();
|
||||
this.statuses.addAll(statuses);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItem(int position, StatusViewData statusViewData) {
|
||||
public void addItem(int position, StatusViewData.Concrete statusViewData) {
|
||||
statuses.add(position, statusViewData);
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
|
@ -109,12 +108,12 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
notifyItemRangeRemoved(0, oldSize);
|
||||
}
|
||||
|
||||
public void addAll(int position, List<StatusViewData> statuses) {
|
||||
public void addAll(int position, List<StatusViewData.Concrete> statuses) {
|
||||
this.statuses.addAll(position, statuses);
|
||||
notifyItemRangeInserted(position, statuses.size());
|
||||
}
|
||||
|
||||
public void addAll(List<StatusViewData> statuses) {
|
||||
public void addAll(List<StatusViewData.Concrete> statuses) {
|
||||
int end = statuses.size();
|
||||
this.statuses.addAll(statuses);
|
||||
notifyItemRangeInserted(end, statuses.size());
|
||||
|
@ -126,7 +125,7 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setItem(int position, StatusViewData status, boolean notifyAdapter) {
|
||||
public void setItem(int position, StatusViewData.Concrete status, boolean notifyAdapter) {
|
||||
statuses.set(position, status);
|
||||
if (notifyAdapter) {
|
||||
notifyItemChanged(position);
|
||||
|
@ -134,7 +133,7 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public StatusViewData getItem(int position) {
|
||||
public StatusViewData.Concrete getItem(int position) {
|
||||
if (position != RecyclerView.NO_POSITION && position >= 0 && position < statuses.size()) {
|
||||
return statuses.get(position);
|
||||
} else {
|
||||
|
|
|
@ -31,6 +31,7 @@ import java.util.List;
|
|||
public class TimelineAdapter extends RecyclerView.Adapter {
|
||||
private static final int VIEW_TYPE_STATUS = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
private static final int VIEW_TYPE_PLACEHOLDER = 2;
|
||||
|
||||
private List<StatusViewData> statuses;
|
||||
private StatusActionListener statusListener;
|
||||
|
@ -59,15 +60,28 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
.inflate(R.layout.item_footer, viewGroup, false);
|
||||
return new FooterViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_PLACEHOLDER: {
|
||||
View view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_status_placeholder, viewGroup, false);
|
||||
return new PlaceholderViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position < statuses.size()) {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
StatusViewData status = statuses.get(position);
|
||||
holder.setupWithStatus(status, statusListener, mediaPreviewEnabled);
|
||||
if (status instanceof StatusViewData.Placeholder) {
|
||||
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
|
||||
holder.setup(!((StatusViewData.Placeholder) status).isLoading(), statusListener);
|
||||
} else {
|
||||
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
holder.setupWithStatus((StatusViewData.Concrete) status,
|
||||
statusListener, mediaPreviewEnabled);
|
||||
}
|
||||
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
|
@ -83,10 +97,14 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
public int getItemViewType(int position) {
|
||||
if (position == statuses.size()) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
if (statuses.get(position) instanceof StatusViewData.Placeholder) {
|
||||
return VIEW_TYPE_PLACEHOLDER;
|
||||
} else {
|
||||
return VIEW_TYPE_STATUS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void update(@Nullable List<StatusViewData> newStatuses) {
|
||||
if (newStatuses == null || newStatuses.isEmpty()) {
|
||||
|
|
|
@ -106,19 +106,17 @@ public class Status {
|
|||
public static final int MAX_MEDIA_ATTACHMENTS = 4;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Status status = (Status) o;
|
||||
return id != null ? id.equals(status.id) : status.id == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this.id == null) {
|
||||
return this == other;
|
||||
} else if (!(other instanceof Status)) {
|
||||
return false;
|
||||
}
|
||||
Status status = (Status) other;
|
||||
return status.id.equals(this.id);
|
||||
public int hashCode() {
|
||||
return id != null ? id.hashCode() : 0;
|
||||
}
|
||||
|
||||
public static class MediaAttachment {
|
||||
|
|
|
@ -44,6 +44,8 @@ import com.keylesspalace.tusky.entity.Status;
|
|||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.CollectionUtil;
|
||||
import com.keylesspalace.tusky.util.Either;
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||
import com.keylesspalace.tusky.util.ListUtils;
|
||||
import com.keylesspalace.tusky.util.PairedList;
|
||||
|
@ -54,7 +56,6 @@ import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
|||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
|
@ -65,11 +66,29 @@ public class NotificationsFragment extends SFragment implements
|
|||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener,
|
||||
NotificationsAdapter.NotificationActionListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "Notifications"; // logging tag
|
||||
private static final String TAG = "NotificationF"; // logging tag
|
||||
|
||||
private static final int LOAD_AT_ONCE = 30;
|
||||
|
||||
private enum FetchEnd {
|
||||
TOP,
|
||||
BOTTOM
|
||||
BOTTOM,
|
||||
MIDDLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for the notifications. Consider moving to the separate class to hide constructor
|
||||
* and reuse in different places as needed.
|
||||
*/
|
||||
private static final class Placeholder {
|
||||
private static final Placeholder INSTANCE = new Placeholder();
|
||||
|
||||
public static Placeholder getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private Placeholder() {
|
||||
}
|
||||
}
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
|
@ -87,11 +106,17 @@ public class NotificationsFragment extends SFragment implements
|
|||
private String bottomId;
|
||||
private String topId;
|
||||
|
||||
private final PairedList<Notification, NotificationViewData> notifications
|
||||
= new PairedList<>(new Function<Notification, NotificationViewData>() {
|
||||
// Each element is either a Notification for loading data or a Placeholder
|
||||
private final PairedList<Either<Placeholder, Notification>, NotificationViewData> notifications
|
||||
= new PairedList<>(new Function<Either<Placeholder, Notification>, NotificationViewData>() {
|
||||
@Override
|
||||
public NotificationViewData apply(Notification input) {
|
||||
return ViewDataUtils.notificationToViewData(input);
|
||||
public NotificationViewData apply(Either<Placeholder, Notification> input) {
|
||||
if (input.isRight()) {
|
||||
Notification notification = input.getAsRight();
|
||||
return ViewDataUtils.notificationToViewData(notification);
|
||||
} else {
|
||||
return new NotificationViewData.Placeholder(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -156,12 +181,10 @@ public class NotificationsFragment extends SFragment implements
|
|||
TabLayout layout = activity.findViewById(R.id.tab_layout);
|
||||
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
}
|
||||
public void onTabSelected(TabLayout.Tab tab) {}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
}
|
||||
public void onTabUnselected(TabLayout.Tab tab) {}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
|
@ -185,7 +208,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
ActionButtonActivity activity = (ActionButtonActivity) getActivity();
|
||||
FloatingActionButton composeButton = activity.getActionButton();
|
||||
|
||||
if(composeButton != null) {
|
||||
if (composeButton != null) {
|
||||
if (hideFab) {
|
||||
if (dy > 0 && composeButton.isShown()) {
|
||||
composeButton.hide(); // hides the button if we're scrolling down
|
||||
|
@ -220,18 +243,17 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
sendFetchNotificationsRequest(null, topId, FetchEnd.TOP);
|
||||
sendFetchNotificationsRequest(null, topId, FetchEnd.TOP, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReply(int position) {
|
||||
Notification notification = notifications.get(position);
|
||||
super.reply(notification.status);
|
||||
super.reply(notifications.get(position).getAsRight().status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReblog(final boolean reblog, final int position) {
|
||||
final Notification notification = notifications.get(position);
|
||||
final Notification notification = notifications.get(position).getAsRight();
|
||||
final Status status = notification.status;
|
||||
reblogWithCallback(status, reblog, new Callback<Status>() {
|
||||
@Override
|
||||
|
@ -242,7 +264,9 @@ public class NotificationsFragment extends SFragment implements
|
|||
if (status.reblog != null) {
|
||||
status.reblog.reblogged = reblog;
|
||||
}
|
||||
notifications.set(position, notification);
|
||||
// Java's type inference *eyeroll*
|
||||
notifications.set(position,
|
||||
Either.<Placeholder, Notification>right(notification));
|
||||
|
||||
adapter.updateItemWithNotify(position, notifications.getPairedItem(position), true);
|
||||
|
||||
|
@ -252,8 +276,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
|
||||
Log.d(getClass().getSimpleName(), "Failed to reblog status: " + status.id);
|
||||
t.printStackTrace();
|
||||
Log.d(getClass().getSimpleName(), "Failed to reblog status: " + status.id, t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -261,7 +284,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onFavourite(final boolean favourite, final int position) {
|
||||
final Notification notification = notifications.get(position);
|
||||
final Notification notification = notifications.get(position).getAsRight();
|
||||
final Status status = notification.status;
|
||||
favouriteWithCallback(status, favourite, new Callback<Status>() {
|
||||
@Override
|
||||
|
@ -273,7 +296,8 @@ public class NotificationsFragment extends SFragment implements
|
|||
status.reblog.favourited = favourite;
|
||||
}
|
||||
|
||||
notifications.set(position, notification);
|
||||
notifications.set(position,
|
||||
Either.<Placeholder, Notification>right(notification));
|
||||
|
||||
adapter.updateItemWithNotify(position, notifications.getPairedItem(position), true);
|
||||
|
||||
|
@ -283,15 +307,14 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
|
||||
Log.d(getClass().getSimpleName(), "Failed to favourite status: " + status.id);
|
||||
t.printStackTrace();
|
||||
Log.d(getClass().getSimpleName(), "Failed to favourite status: " + status.id, t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMore(View view, int position) {
|
||||
Notification notification = notifications.get(position);
|
||||
Notification notification = notifications.get(position).getAsRight();
|
||||
super.more(notification.status, view, position);
|
||||
}
|
||||
|
||||
|
@ -303,24 +326,25 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onViewThread(int position) {
|
||||
Notification notification = notifications.get(position);
|
||||
Notification notification = notifications.get(position).getAsRight();
|
||||
super.viewThread(notification.status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenReblog(int position) {
|
||||
Notification notification = notifications.get(position);
|
||||
if (notification != null) onViewAccount(notification.account.id);
|
||||
Notification notification = notifications.get(position).getAsRight();
|
||||
onViewAccount(notification.account.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExpandedChange(boolean expanded, int position) {
|
||||
NotificationViewData old = notifications.getPairedItem(position);
|
||||
StatusViewData statusViewData =
|
||||
NotificationViewData.Concrete old =
|
||||
(NotificationViewData.Concrete) notifications.getPairedItem(position);
|
||||
StatusViewData.Concrete statusViewData =
|
||||
new StatusViewData.Builder(old.getStatusViewData())
|
||||
.setIsExpanded(expanded)
|
||||
.createStatusViewData();
|
||||
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
|
||||
NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(),
|
||||
old.getId(), old.getAccount(), statusViewData);
|
||||
notifications.setPairedItem(position, notificationViewData);
|
||||
adapter.updateItemWithNotify(position, notificationViewData, false);
|
||||
|
@ -328,17 +352,38 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onContentHiddenChange(boolean isShowing, int position) {
|
||||
NotificationViewData old = notifications.getPairedItem(position);
|
||||
StatusViewData statusViewData =
|
||||
NotificationViewData.Concrete old =
|
||||
(NotificationViewData.Concrete) notifications.getPairedItem(position);
|
||||
StatusViewData.Concrete statusViewData =
|
||||
new StatusViewData.Builder(old.getStatusViewData())
|
||||
.setIsShowingSensitiveContent(isShowing)
|
||||
.createStatusViewData();
|
||||
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
|
||||
NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(),
|
||||
old.getId(), old.getAccount(), statusViewData);
|
||||
notifications.setPairedItem(position, notificationViewData);
|
||||
adapter.updateItemWithNotify(position, notificationViewData, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadMore(int position) {
|
||||
//check bounds before accessing list,
|
||||
if (notifications.size() >= position && position > 0) {
|
||||
Notification previous = notifications.get(position - 1).getAsRightOrNull();
|
||||
Notification next = notifications.get(position + 1).getAsRightOrNull();
|
||||
if (previous == null || next == null) {
|
||||
Log.e(TAG, "Failed to load more, invalid placeholder position: " + position);
|
||||
return;
|
||||
}
|
||||
sendFetchNotificationsRequest(previous.id, next.id, FetchEnd.MIDDLE, position);
|
||||
NotificationViewData notificationViewData =
|
||||
new NotificationViewData.Placeholder(true);
|
||||
notifications.setPairedItem(position, notificationViewData);
|
||||
adapter.updateItemWithNotify(position, notificationViewData, false);
|
||||
} else {
|
||||
Log.d(TAG, "error loading more");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewTag(String tag) {
|
||||
super.viewTag(tag);
|
||||
|
@ -374,10 +419,11 @@ public class NotificationsFragment extends SFragment implements
|
|||
@Override
|
||||
public void removeAllByAccountId(String accountId) {
|
||||
// using iterator to safely remove items while iterating
|
||||
Iterator<Notification> iterator = notifications.iterator();
|
||||
Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Notification notification = iterator.next();
|
||||
if (notification.account.id.equals(accountId)) {
|
||||
Either<Placeholder, Notification> notification = iterator.next();
|
||||
Notification maybeNotification = notification.getAsRightOrNull();
|
||||
if (maybeNotification != null && maybeNotification.account.id.equals(accountId)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
@ -385,7 +431,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void onLoadMore() {
|
||||
sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM);
|
||||
sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM, -1);
|
||||
}
|
||||
|
||||
private void jumpToTop() {
|
||||
|
@ -394,7 +440,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void sendFetchNotificationsRequest(String fromId, String uptoId,
|
||||
final FetchEnd fetchEnd) {
|
||||
final FetchEnd fetchEnd, final int pos) {
|
||||
/* 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) {
|
||||
|
@ -418,7 +464,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
});
|
||||
}
|
||||
|
||||
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, null);
|
||||
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE);
|
||||
|
||||
call.enqueue(new Callback<List<Notification>>() {
|
||||
@Override
|
||||
|
@ -426,22 +472,22 @@ public class NotificationsFragment extends SFragment implements
|
|||
@NonNull Response<List<Notification>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd);
|
||||
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos);
|
||||
} else {
|
||||
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd);
|
||||
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<Notification>> call, @NonNull Throwable t) {
|
||||
onFetchNotificationsFailure((Exception) t, fetchEnd);
|
||||
onFetchNotificationsFailure((Exception) t, fetchEnd, pos);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
|
||||
FetchEnd fetchEnd) {
|
||||
FetchEnd fetchEnd, int pos) {
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||
switch (fetchEnd) {
|
||||
case TOP: {
|
||||
|
@ -453,6 +499,10 @@ public class NotificationsFragment extends SFragment implements
|
|||
update(notifications, null, uptoId);
|
||||
break;
|
||||
}
|
||||
case MIDDLE: {
|
||||
replacePlaceholderWithNotifications(notifications, pos);
|
||||
break;
|
||||
}
|
||||
case BOTTOM: {
|
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||
String fromId = null;
|
||||
|
@ -489,7 +539,19 @@ public class NotificationsFragment extends SFragment implements
|
|||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
|
||||
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) {
|
||||
NotificationViewData placeholderVD =
|
||||
new NotificationViewData.Placeholder(false);
|
||||
notifications.setPairedItem(position, placeholderVD);
|
||||
adapter.updateItemWithNotify(position, placeholderVD, true);
|
||||
}
|
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
}
|
||||
|
||||
private void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
|
||||
@Nullable String uptoId) {
|
||||
if (ListUtils.isEmpty(newNotifications)) {
|
||||
return;
|
||||
|
@ -500,26 +562,31 @@ public class NotificationsFragment extends SFragment implements
|
|||
if (uptoId != null) {
|
||||
topId = uptoId;
|
||||
}
|
||||
List<Either<Placeholder, Notification>> liftedNew =
|
||||
liftNotificationList(newNotifications);
|
||||
if (notifications.isEmpty()) {
|
||||
// This construction removes duplicates while preserving order.
|
||||
notifications.addAll(new LinkedHashSet<>(newNotifications));
|
||||
notifications.addAll(liftedNew);
|
||||
} else {
|
||||
int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1));
|
||||
int index = notifications.indexOf(liftedNew.get(newNotifications.size() - 1));
|
||||
for (int i = 0; i < index; i++) {
|
||||
notifications.remove(0);
|
||||
}
|
||||
int newIndex = newNotifications.indexOf(notifications.get(0));
|
||||
|
||||
|
||||
int newIndex = liftedNew.indexOf(notifications.get(0));
|
||||
if (newIndex == -1) {
|
||||
notifications.addAll(0, newNotifications);
|
||||
if (index == -1 && liftedNew.size() >= LOAD_AT_ONCE) {
|
||||
liftedNew.add(Either.<Placeholder, Notification>left(Placeholder.getInstance()));
|
||||
}
|
||||
notifications.addAll(0, liftedNew);
|
||||
} else {
|
||||
List<Notification> sublist = newNotifications.subList(0, newIndex);
|
||||
notifications.addAll(0, sublist);
|
||||
notifications.addAll(0, liftedNew.subList(0, newIndex));
|
||||
}
|
||||
}
|
||||
adapter.update(notifications.getPairedCopy());
|
||||
}
|
||||
|
||||
public void addItems(List<Notification> newNotifications, @Nullable String fromId) {
|
||||
private void addItems(List<Notification> newNotifications, @Nullable String fromId) {
|
||||
if (ListUtils.isEmpty(newNotifications)) {
|
||||
return;
|
||||
}
|
||||
|
@ -527,9 +594,10 @@ public class NotificationsFragment extends SFragment implements
|
|||
bottomId = fromId;
|
||||
}
|
||||
int end = notifications.size();
|
||||
Notification last = notifications.get(end - 1);
|
||||
if (last != null && !findNotification(newNotifications, last.id)) {
|
||||
notifications.addAll(newNotifications);
|
||||
List<Either<Placeholder, Notification>> liftedNew = liftNotificationList(newNotifications);
|
||||
Either<Placeholder, Notification> last = notifications.get(end - 1);
|
||||
if (last != null && liftedNew.indexOf(last) == -1) {
|
||||
notifications.addAll(liftedNew);
|
||||
List<NotificationViewData> newViewDatas = notifications.getPairedCopy()
|
||||
.subList(notifications.size() - newNotifications.size(),
|
||||
notifications.size());
|
||||
|
@ -537,21 +605,6 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean findNotification(List<Notification> notifications, String id) {
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return 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: {
|
||||
|
@ -573,9 +626,43 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
private void replacePlaceholderWithNotifications(List<Notification> newNotifications, int pos) {
|
||||
// Remove placeholder
|
||||
notifications.remove(pos);
|
||||
|
||||
if (ListUtils.isEmpty(newNotifications)) {
|
||||
adapter.update(notifications.getPairedCopy());
|
||||
return;
|
||||
}
|
||||
|
||||
List<Either<Placeholder, Notification>> liftedNew = liftNotificationList(newNotifications);
|
||||
|
||||
// If we fetched less posts than in the limit, it means that the hole is not filled
|
||||
// If we fetched at least as much it means that there are more posts to load and we should
|
||||
// insert new placeholder
|
||||
if (newNotifications.size() >= LOAD_AT_ONCE) {
|
||||
liftedNew.add(Either.<Placeholder, Notification>left(Placeholder.getInstance()));
|
||||
}
|
||||
|
||||
notifications.addAll(pos, liftedNew);
|
||||
adapter.update(notifications.getPairedCopy());
|
||||
}
|
||||
|
||||
private final Function<Notification, Either<Placeholder, Notification>> notificationLifter =
|
||||
new Function<Notification, Either<Placeholder, Notification>>() {
|
||||
@Override
|
||||
public Either<Placeholder, Notification> apply(Notification input) {
|
||||
return Either.right(input);
|
||||
}
|
||||
};
|
||||
|
||||
private List<Either<Placeholder, Notification>> liftNotificationList(List<Notification> list) {
|
||||
return CollectionUtil.map(list, notificationLifter);
|
||||
}
|
||||
|
||||
private void fullyRefresh() {
|
||||
adapter.clear();
|
||||
notifications.clear();
|
||||
sendFetchNotificationsRequest(null, null, FetchEnd.TOP);
|
||||
sendFetchNotificationsRequest(null, null, FetchEnd.TOP, -1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
|
@ -148,10 +149,10 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
|||
Call<Relationship> call = mastodonApi.muteAccount(id);
|
||||
call.enqueue(new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, Response<Relationship> response) {}
|
||||
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Relationship> call, Throwable t) {}
|
||||
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {}
|
||||
});
|
||||
callList.add(call);
|
||||
Intent intent = new Intent(TimelineReceiver.Types.MUTE_ACCOUNT);
|
||||
|
@ -164,10 +165,10 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
|||
Call<Relationship> call = mastodonApi.blockAccount(id);
|
||||
call.enqueue(new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {}
|
||||
public void onResponse(@NonNull Call<Relationship> call, @NonNull retrofit2.Response<Relationship> response) {}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Relationship> call, Throwable t) {}
|
||||
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {}
|
||||
});
|
||||
callList.add(call);
|
||||
Intent intent = new Intent(TimelineReceiver.Types.BLOCK_ACCOUNT);
|
||||
|
@ -180,10 +181,10 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
|||
Call<ResponseBody> call = mastodonApi.deleteStatus(id);
|
||||
call.enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {}
|
||||
public void onResponse(@NonNull Call<ResponseBody> call, @NonNull retrofit2.Response<ResponseBody> response) {}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {}
|
||||
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.fragment;
|
||||
|
||||
import android.arch.core.util.Function;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
@ -25,6 +26,7 @@ import android.support.annotation.Nullable;
|
|||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v4.util.Pair;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
|
@ -43,6 +45,8 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
|||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.CollectionUtil;
|
||||
import com.keylesspalace.tusky.util.Either;
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||
import com.keylesspalace.tusky.util.ListUtils;
|
||||
import com.keylesspalace.tusky.util.PairedList;
|
||||
|
@ -52,7 +56,6 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
|||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
@ -64,10 +67,12 @@ public class TimelineFragment extends SFragment implements
|
|||
SwipeRefreshLayout.OnRefreshListener,
|
||||
StatusActionListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "Timeline"; // logging tag
|
||||
private static final String TAG = "TimelineF"; // logging tag
|
||||
private static final String KIND_ARG = "kind";
|
||||
private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id";
|
||||
|
||||
private static final int LOAD_AT_ONCE = 30;
|
||||
|
||||
public enum Kind {
|
||||
HOME,
|
||||
PUBLIC_LOCAL,
|
||||
|
@ -80,6 +85,7 @@ public class TimelineFragment extends SFragment implements
|
|||
private enum FetchEnd {
|
||||
TOP,
|
||||
BOTTOM,
|
||||
MIDDLE
|
||||
}
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
|
@ -102,8 +108,18 @@ public class TimelineFragment extends SFragment implements
|
|||
private String bottomId;
|
||||
@Nullable
|
||||
private String topId;
|
||||
private PairedList<Status, StatusViewData> statuses =
|
||||
new PairedList<>(ViewDataUtils.statusMapper());
|
||||
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
|
||||
new PairedList<>(new Function<Either<Placeholder, Status>, StatusViewData>() {
|
||||
@Override
|
||||
public StatusViewData apply(Either<Placeholder, Status> input) {
|
||||
Status status = input.getAsRightOrNull();
|
||||
if (status != null) {
|
||||
return ViewDataUtils.statusToViewData(status);
|
||||
} else {
|
||||
return new StatusViewData.Placeholder(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public static TimelineFragment newInstance(Kind kind) {
|
||||
TimelineFragment fragment = new TimelineFragment();
|
||||
|
@ -122,6 +138,17 @@ public class TimelineFragment extends SFragment implements
|
|||
return fragment;
|
||||
}
|
||||
|
||||
private static final class Placeholder {
|
||||
private final static Placeholder INSTANCE = new Placeholder();
|
||||
|
||||
public static Placeholder getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private Placeholder() {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
@ -179,12 +206,10 @@ public class TimelineFragment extends SFragment implements
|
|||
TabLayout layout = getActivity().findViewById(R.id.tab_layout);
|
||||
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
}
|
||||
public void onTabSelected(TabLayout.Tab tab) {}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
}
|
||||
public void onTabUnselected(TabLayout.Tab tab) {}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
|
@ -251,86 +276,97 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
sendFetchTimelineRequest(null, topId, FetchEnd.TOP);
|
||||
sendFetchTimelineRequest(null, topId, FetchEnd.TOP, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReply(int position) {
|
||||
super.reply(statuses.get(position));
|
||||
super.reply(statuses.get(position).getAsRight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReblog(final boolean reblog, final int position) {
|
||||
final Status status = statuses.get(position);
|
||||
final Status status = statuses.get(position).getAsRight();
|
||||
super.reblogWithCallback(status, reblog, new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
status.reblogged = reblog;
|
||||
|
||||
if (status.reblog != null) {
|
||||
status.reblog.reblogged = reblog;
|
||||
}
|
||||
|
||||
Pair<StatusViewData.Concrete, Integer> actual =
|
||||
findStatusAndPosition(position, status);
|
||||
if (actual == null) return;
|
||||
|
||||
StatusViewData newViewData =
|
||||
new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||
new StatusViewData.Builder(actual.first)
|
||||
.setReblogged(reblog)
|
||||
.createStatusViewData();
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.changeItem(position, newViewData, true);
|
||||
statuses.setPairedItem(actual.second, newViewData);
|
||||
adapter.changeItem(actual.second, newViewData, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
|
||||
Log.d(TAG, "Failed to reblog status " + status.id);
|
||||
t.printStackTrace();
|
||||
Log.d(TAG, "Failed to reblog status " + status.id, t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFavourite(final boolean favourite, final int position) {
|
||||
final Status status = statuses.get(position);
|
||||
final Status status = statuses.get(position).getAsRight();
|
||||
|
||||
super.favouriteWithCallback(status, favourite, new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
status.favourited = favourite;
|
||||
|
||||
if (status.reblog != null) {
|
||||
status.reblog.favourited = favourite;
|
||||
}
|
||||
|
||||
Pair<StatusViewData.Concrete, Integer> actual =
|
||||
findStatusAndPosition(position, status);
|
||||
if (actual == null) return;
|
||||
|
||||
StatusViewData newViewData = new StatusViewData
|
||||
.Builder(statuses.getPairedItem(position))
|
||||
.Builder(actual.first)
|
||||
.setFavourited(favourite)
|
||||
.createStatusViewData();
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.changeItem(position, newViewData, true);
|
||||
statuses.setPairedItem(actual.second, newViewData);
|
||||
adapter.changeItem(actual.second, newViewData, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
|
||||
Log.d(TAG, "Failed to favourite status " + status.id);
|
||||
t.printStackTrace();
|
||||
Log.d(TAG, "Failed to favourite status " + status.id, t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMore(View view, final int position) {
|
||||
super.more(statuses.get(position), view, position);
|
||||
super.more(statuses.get(position).getAsRight(), view, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenReblog(int position) {
|
||||
super.openReblog(statuses.get(position));
|
||||
super.openReblog(statuses.get(position).getAsRight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExpandedChange(boolean expanded, int position) {
|
||||
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||
StatusViewData newViewData = new StatusViewData.Builder(
|
||||
((StatusViewData.Concrete) statuses.getPairedItem(position)))
|
||||
.setIsExpanded(expanded).createStatusViewData();
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.changeItem(position, newViewData, false);
|
||||
|
@ -338,12 +374,33 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onContentHiddenChange(boolean isShowing, int position) {
|
||||
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||
StatusViewData newViewData = new StatusViewData.Builder(
|
||||
((StatusViewData.Concrete) statuses.getPairedItem(position)))
|
||||
.setIsShowingSensitiveContent(isShowing).createStatusViewData();
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.changeItem(position, newViewData, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadMore(int position) {
|
||||
//check bounds before accessing list,
|
||||
if (statuses.size() >= position && position > 0) {
|
||||
Status fromStatus = statuses.get(position - 1).getAsRightOrNull();
|
||||
Status toStatus = statuses.get(position + 1).getAsRightOrNull();
|
||||
if (fromStatus == null || toStatus == null) {
|
||||
Log.e(TAG, "Failed to load more at " + position + ", wrong placeholder position");
|
||||
return;
|
||||
}
|
||||
sendFetchTimelineRequest(fromStatus.id, toStatus.id, FetchEnd.MIDDLE, position);
|
||||
|
||||
StatusViewData newViewData = new StatusViewData.Placeholder(true);
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.changeItem(position, newViewData, false);
|
||||
} else {
|
||||
Log.e(TAG, "error loading more");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type,
|
||||
View view) {
|
||||
|
@ -352,7 +409,7 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onViewThread(int position) {
|
||||
super.viewThread(statuses.get(position));
|
||||
super.viewThread(statuses.get(position).getAsRight());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -417,10 +474,10 @@ public class TimelineFragment extends SFragment implements
|
|||
@Override
|
||||
public void removeAllByAccountId(String accountId) {
|
||||
// using iterator to safely remove items while iterating
|
||||
Iterator<Status> iterator = statuses.iterator();
|
||||
Iterator<Either<Placeholder, Status>> iterator = statuses.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Status status = iterator.next();
|
||||
if (status.account.id.equals(accountId)) {
|
||||
Status status = iterator.next().getAsRightOrNull();
|
||||
if (status != null && status.account.id.equals(accountId)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
@ -428,12 +485,12 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void onLoadMore() {
|
||||
sendFetchTimelineRequest(bottomId, null, FetchEnd.BOTTOM);
|
||||
sendFetchTimelineRequest(bottomId, null, FetchEnd.BOTTOM, -1);
|
||||
}
|
||||
|
||||
private void fullyRefresh() {
|
||||
adapter.clear();
|
||||
sendFetchTimelineRequest(null, null, FetchEnd.TOP);
|
||||
sendFetchTimelineRequest(null, null, FetchEnd.TOP, -1);
|
||||
}
|
||||
|
||||
private boolean jumpToTopAllowed() {
|
||||
|
@ -457,20 +514,20 @@ public class TimelineFragment extends SFragment implements
|
|||
case HOME:
|
||||
return api.homeTimeline(fromId, uptoId, null);
|
||||
case PUBLIC_FEDERATED:
|
||||
return api.publicTimeline(null, fromId, uptoId, null);
|
||||
return api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE);
|
||||
case PUBLIC_LOCAL:
|
||||
return api.publicTimeline(true, fromId, uptoId, null);
|
||||
return api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE);
|
||||
case TAG:
|
||||
return api.hashtagTimeline(tagOrId, null, fromId, uptoId, null);
|
||||
return api.hashtagTimeline(tagOrId, null, fromId, uptoId, LOAD_AT_ONCE);
|
||||
case USER:
|
||||
return api.accountStatuses(tagOrId, fromId, uptoId, null, null);
|
||||
return api.accountStatuses(tagOrId, fromId, uptoId, LOAD_AT_ONCE);
|
||||
case FAVOURITES:
|
||||
return api.favourites(fromId, uptoId, null);
|
||||
return api.favourites(fromId, uptoId, LOAD_AT_ONCE);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFetchTimelineRequest(@Nullable String fromId, @Nullable String uptoId,
|
||||
final FetchEnd fetchEnd) {
|
||||
final FetchEnd fetchEnd, final int pos) {
|
||||
/* 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) {
|
||||
|
@ -499,15 +556,15 @@ public class TimelineFragment extends SFragment implements
|
|||
public void onResponse(@NonNull Call<List<Status>> call, @NonNull Response<List<Status>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd);
|
||||
onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd, pos);
|
||||
} else {
|
||||
onFetchTimelineFailure(new Exception(response.message()), fetchEnd);
|
||||
onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<Status>> call, @NonNull Throwable t) {
|
||||
onFetchTimelineFailure((Exception) t, fetchEnd);
|
||||
onFetchTimelineFailure((Exception) t, fetchEnd, pos);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -516,8 +573,11 @@ public class TimelineFragment extends SFragment implements
|
|||
listCall.enqueue(callback);
|
||||
}
|
||||
|
||||
public void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
|
||||
FetchEnd fetchEnd) {
|
||||
private void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
|
||||
FetchEnd fetchEnd, int pos) {
|
||||
// We filled the hole (or reached the end) if the server returned less statuses than we
|
||||
// we asked for.
|
||||
boolean fullFetch = statuses.size() >= LOAD_AT_ONCE;
|
||||
filterStatuses(statuses);
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||
switch (fetchEnd) {
|
||||
|
@ -527,7 +587,11 @@ public class TimelineFragment extends SFragment implements
|
|||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
updateStatuses(statuses, null, uptoId);
|
||||
updateStatuses(statuses, null, uptoId, fullFetch);
|
||||
break;
|
||||
}
|
||||
case MIDDLE: {
|
||||
replacePlaceholderWithStatuses(statuses, fullFetch, pos);
|
||||
break;
|
||||
}
|
||||
case BOTTOM: {
|
||||
|
@ -547,7 +611,7 @@ public class TimelineFragment extends SFragment implements
|
|||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
updateStatuses(statuses, fromId, uptoId);
|
||||
updateStatuses(statuses, fromId, uptoId, fullFetch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -561,8 +625,15 @@ public class TimelineFragment extends SFragment implements
|
|||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd) {
|
||||
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
|
||||
if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) {
|
||||
StatusViewData newViewData = new StatusViewData.Placeholder(false);
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.changeItem(position, newViewData, true);
|
||||
}
|
||||
|
||||
Log.e(TAG, "Fetch Failure: " + exception.getMessage());
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
}
|
||||
|
@ -588,7 +659,7 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
protected void filterStatuses(List<Status> statuses) {
|
||||
private void filterStatuses(List<Status> statuses) {
|
||||
Iterator<Status> it = statuses.iterator();
|
||||
while (it.hasNext()) {
|
||||
Status status = it.next();
|
||||
|
@ -600,7 +671,7 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void updateStatuses(List<Status> newStatuses, @Nullable String fromId,
|
||||
@Nullable String toId) {
|
||||
@Nullable String toId, boolean fullFetch) {
|
||||
if (ListUtils.isEmpty(newStatuses)) {
|
||||
return;
|
||||
}
|
||||
|
@ -610,20 +681,26 @@ public class TimelineFragment extends SFragment implements
|
|||
if (toId != null) {
|
||||
topId = toId;
|
||||
}
|
||||
|
||||
List<Either<Placeholder, Status>> liftedNew = listStatusList(newStatuses);
|
||||
|
||||
if (statuses.isEmpty()) {
|
||||
// This construction removes duplicates while preserving order.
|
||||
statuses.addAll(new LinkedHashSet<>(newStatuses));
|
||||
statuses.addAll(liftedNew);
|
||||
} else {
|
||||
Status lastOfNew = newStatuses.get(newStatuses.size() - 1);
|
||||
Either<Placeholder, Status> lastOfNew = liftedNew.get(newStatuses.size() - 1);
|
||||
int index = statuses.indexOf(lastOfNew);
|
||||
|
||||
for (int i = 0; i < index; i++) {
|
||||
statuses.remove(0);
|
||||
}
|
||||
int newIndex = newStatuses.indexOf(statuses.get(0));
|
||||
int newIndex = liftedNew.indexOf(statuses.get(0));
|
||||
if (newIndex == -1) {
|
||||
statuses.addAll(0, newStatuses);
|
||||
if (index == -1 && fullFetch) {
|
||||
liftedNew.add(Either.<Placeholder, Status>left(Placeholder.getInstance()));
|
||||
}
|
||||
statuses.addAll(0, liftedNew);
|
||||
} else {
|
||||
statuses.addAll(0, newStatuses.subList(0, newIndex));
|
||||
statuses.addAll(0, liftedNew.subList(0, newIndex));
|
||||
}
|
||||
}
|
||||
adapter.update(statuses.getPairedCopy());
|
||||
|
@ -634,9 +711,11 @@ public class TimelineFragment extends SFragment implements
|
|||
return;
|
||||
}
|
||||
int end = statuses.size();
|
||||
Status last = statuses.get(end - 1);
|
||||
Status last = statuses.get(end - 1).getAsRightOrNull();
|
||||
// I was about to replace findStatus with indexOf but it is incorrect to compare value
|
||||
// types by ID anyway and we should change equals() for Status, I think, so this makes sense
|
||||
if (last != null && !findStatus(newStatuses, last.id)) {
|
||||
statuses.addAll(newStatuses);
|
||||
statuses.addAll(listStatusList(newStatuses));
|
||||
List<StatusViewData> newViewDatas = statuses.getPairedCopy()
|
||||
.subList(statuses.size() - newStatuses.size(), statuses.size());
|
||||
if (BuildConfig.DEBUG && newStatuses.size() != newViewDatas.size()) {
|
||||
|
@ -653,6 +732,28 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
private void replacePlaceholderWithStatuses(List<Status> newStatuses, boolean fullFetch, int pos) {
|
||||
Status status = statuses.get(pos).getAsRightOrNull();
|
||||
if (status == null) {
|
||||
statuses.remove(pos);
|
||||
}
|
||||
|
||||
if (ListUtils.isEmpty(newStatuses)) {
|
||||
adapter.update(statuses.getPairedCopy());
|
||||
return;
|
||||
}
|
||||
|
||||
List<Either<Placeholder, Status>> liftedNew = listStatusList(newStatuses);
|
||||
|
||||
if (fullFetch) {
|
||||
liftedNew.add(Either.<Placeholder, Status>left(Placeholder.getInstance()));
|
||||
}
|
||||
|
||||
statuses.addAll(pos, liftedNew);
|
||||
adapter.update(statuses.getPairedCopy());
|
||||
|
||||
}
|
||||
|
||||
private static boolean findStatus(List<Status> statuses, String id) {
|
||||
for (Status status : statuses) {
|
||||
if (status.id.equals(id)) {
|
||||
|
@ -661,4 +762,39 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private final Function<Status, Either<Placeholder, Status>> statusLifter =
|
||||
new Function<Status, Either<Placeholder, Status>>() {
|
||||
@Override
|
||||
public Either<Placeholder, Status> apply(Status input) {
|
||||
return Either.right(input);
|
||||
}
|
||||
};
|
||||
|
||||
private @Nullable
|
||||
Pair<StatusViewData.Concrete, Integer>
|
||||
findStatusAndPosition(int position, Status status) {
|
||||
StatusViewData.Concrete statusToUpdate;
|
||||
int positionToUpdate;
|
||||
StatusViewData someOldViewData = statuses.getPairedItem(position);
|
||||
|
||||
// Unlikely, but data could change between the request and response
|
||||
if ((someOldViewData instanceof StatusViewData.Placeholder) ||
|
||||
!((StatusViewData.Concrete) someOldViewData).getId().equals(status.id)) {
|
||||
// try to find the status we need to update
|
||||
int foundPos = statuses.indexOf(Either.<Placeholder, Status>right(status));
|
||||
if (foundPos < 0) return null; // okay, it's hopeless, give up
|
||||
statusToUpdate = ((StatusViewData.Concrete)
|
||||
statuses.getPairedItem(foundPos));
|
||||
positionToUpdate = position;
|
||||
} else {
|
||||
statusToUpdate = (StatusViewData.Concrete) someOldViewData;
|
||||
positionToUpdate = position;
|
||||
}
|
||||
return new Pair<>(statusToUpdate, positionToUpdate);
|
||||
}
|
||||
|
||||
private List<Either<Placeholder, Status>> listStatusList(List<Status> list) {
|
||||
return CollectionUtil.map(list, statusLifter);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ public class ViewThreadFragment extends SFragment implements
|
|||
|
||||
private int statusIndex = 0;
|
||||
|
||||
private final PairedList<Status, StatusViewData> statuses =
|
||||
private final PairedList<Status, StatusViewData.Concrete> statuses =
|
||||
new PairedList<>(ViewDataUtils.statusMapper());
|
||||
|
||||
public static ViewThreadFragment newInstance(String id) {
|
||||
|
@ -227,7 +227,8 @@ public class ViewThreadFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onExpandedChange(boolean expanded, int position) {
|
||||
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||
StatusViewData.Concrete newViewData =
|
||||
new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||
.setIsExpanded(expanded)
|
||||
.createStatusViewData();
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
|
@ -236,13 +237,19 @@ public class ViewThreadFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onContentHiddenChange(boolean isShowing, int position) {
|
||||
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||
StatusViewData.Concrete newViewData =
|
||||
new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||
.setIsShowingSensitiveContent(isShowing)
|
||||
.createStatusViewData();
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.setItem(position, newViewData, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadMore(int pos) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewTag(String tag) {
|
||||
super.viewTag(tag);
|
||||
|
@ -255,7 +262,7 @@ public class ViewThreadFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void removeItem(int position) {
|
||||
if(position == statusIndex) {
|
||||
if (position == statusIndex) {
|
||||
//the status got removed, close the activity
|
||||
getActivity().finish();
|
||||
}
|
||||
|
@ -278,7 +285,7 @@ public class ViewThreadFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
statusIndex = statuses.indexOf(status);
|
||||
if(statusIndex == -1) {
|
||||
if (statusIndex == -1) {
|
||||
//the status got removed, close the activity
|
||||
getActivity().finish();
|
||||
return;
|
||||
|
@ -379,8 +386,8 @@ public class ViewThreadFragment extends SFragment implements
|
|||
int i = statusIndex;
|
||||
statuses.add(i, status);
|
||||
adapter.setDetailedStatusPosition(i);
|
||||
StatusViewData viewData = statuses.getPairedItem(i);
|
||||
if(viewData.getCard() == null && card != null) {
|
||||
StatusViewData.Concrete viewData = statuses.getPairedItem(i);
|
||||
if (viewData.getCard() == null && card != null) {
|
||||
viewData = new StatusViewData.Builder(viewData)
|
||||
.setCard(card)
|
||||
.createStatusViewData();
|
||||
|
@ -405,7 +412,7 @@ public class ViewThreadFragment extends SFragment implements
|
|||
statusIndex = ancestors.size();
|
||||
adapter.setDetailedStatusPosition(statusIndex);
|
||||
statuses.addAll(0, ancestors);
|
||||
List<StatusViewData> ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex);
|
||||
List<StatusViewData.Concrete> ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex);
|
||||
if (BuildConfig.DEBUG && ancestors.size() != ancestorsViewDatas.size()) {
|
||||
String error = String.format(Locale.getDefault(),
|
||||
"Incorrectly got statusViewData sublist." +
|
||||
|
@ -420,8 +427,8 @@ public class ViewThreadFragment extends SFragment implements
|
|||
// In case we needed to delete everything (which is way easier than deleting
|
||||
// everything except one), re-insert the remaining status here.
|
||||
statuses.add(statusIndex, mainStatus);
|
||||
StatusViewData viewData = statuses.getPairedItem(statusIndex);
|
||||
if(viewData.getCard() == null && card != null) {
|
||||
StatusViewData.Concrete viewData = statuses.getPairedItem(statusIndex);
|
||||
if (viewData.getCard() == null && card != null) {
|
||||
viewData = new StatusViewData.Builder(viewData)
|
||||
.setCard(card)
|
||||
.createStatusViewData();
|
||||
|
@ -431,7 +438,7 @@ public class ViewThreadFragment extends SFragment implements
|
|||
|
||||
// Insert newly fetched descendants
|
||||
statuses.addAll(descendants);
|
||||
List<StatusViewData> descendantsViewData;
|
||||
List<StatusViewData.Concrete> descendantsViewData;
|
||||
descendantsViewData = statuses.getPairedCopy()
|
||||
.subList(statuses.size() - descendants.size(), statuses.size());
|
||||
if (BuildConfig.DEBUG && descendants.size() != descendantsViewData.size()) {
|
||||
|
@ -447,10 +454,9 @@ public class ViewThreadFragment extends SFragment implements
|
|||
|
||||
private void showCard(Card card) {
|
||||
this.card = card;
|
||||
if(statuses.size() != 0) {
|
||||
StatusViewData oldViewData = statuses.getPairedItem(statusIndex);
|
||||
if(oldViewData != null) {
|
||||
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(statusIndex))
|
||||
if (statuses.size() != 0) {
|
||||
StatusViewData.Concrete newViewData =
|
||||
new StatusViewData.Builder(statuses.getPairedItem(statusIndex))
|
||||
.setCard(card)
|
||||
.createStatusViewData();
|
||||
|
||||
|
@ -458,7 +464,6 @@ public class ViewThreadFragment extends SFragment implements
|
|||
adapter.setItem(statusIndex, newViewData, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
statuses.clear();
|
||||
|
|
|
@ -29,4 +29,5 @@ public interface StatusActionListener extends LinkListener {
|
|||
void onOpenReblog(int position);
|
||||
void onExpandedChange(boolean expanded, int position);
|
||||
void onContentHiddenChange(boolean isShowing, int position);
|
||||
void onLoadMore(int position);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.arch.core.util.Function;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by charlag on 05/11/17.
|
||||
*/
|
||||
|
||||
public final class CollectionUtil {
|
||||
private CollectionUtil() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public static <E, R> List<R> map(List<E> list, Function<E, R> mapper) {
|
||||
final List<R> newList = new ArrayList<>(list.size());
|
||||
for (E el : list) {
|
||||
newList.add(mapper.apply(el));
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
}
|
125
app/src/main/java/com/keylesspalace/tusky/util/Either.java
Normal file
125
app/src/main/java/com/keylesspalace/tusky/util/Either.java
Normal file
|
@ -0,0 +1,125 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Created by charlag on 05/11/17.
|
||||
*
|
||||
* Class to represent sum type/tagged union/variant/ADT e.t.c.
|
||||
* It is either Left or Right.
|
||||
*/
|
||||
public final class Either<L, R> {
|
||||
|
||||
/**
|
||||
* Constructs Left instance of either
|
||||
* @param left Object to be considered Left
|
||||
* @param <L> Left type
|
||||
* @param <R> Right type
|
||||
* @return new instance of Either which contains left.
|
||||
*/
|
||||
public static <L, R> Either<L, R> left(L left) {
|
||||
return new Either<>(left, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs Right instance of either
|
||||
* @param right Object to be considered Right
|
||||
* @param <L> Left type
|
||||
* @param <R> Right type
|
||||
* @return new instance of Either which contains right.
|
||||
*/
|
||||
public static <L, R> Either<L, R> right(R right) {
|
||||
return new Either<>(right, true);
|
||||
}
|
||||
|
||||
private final Object value;
|
||||
// we need it because of the types erasure
|
||||
private boolean isRight;
|
||||
|
||||
private Either(Object value, boolean isRight) {
|
||||
this.value = value;
|
||||
this.isRight = isRight;
|
||||
}
|
||||
|
||||
public boolean isRight() {
|
||||
return isRight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get contained object as a Left or throw an exception.
|
||||
* @throws AssertionError If contained value is Right
|
||||
* @return contained value as Right
|
||||
*/
|
||||
public @NonNull L getAsLeft() {
|
||||
if (isRight) {
|
||||
throw new AssertionError("Tried to get the Either as Left while it is Right");
|
||||
}
|
||||
//noinspection unchecked
|
||||
return (L) value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get contained object as a Right or throw an exception.
|
||||
* @throws AssertionError If contained value is Left
|
||||
* @return contained value as Right
|
||||
*/
|
||||
public @NonNull R getAsRight() {
|
||||
if (!isRight) {
|
||||
throw new AssertionError("Tried to get the Either as Right while it is Left");
|
||||
}
|
||||
//noinspection unchecked
|
||||
return (R) value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #getAsLeft()} but returns {@code null} is the value if Right instead of
|
||||
* throwing an exception.
|
||||
* @return contained value as Left or null
|
||||
*/
|
||||
public @Nullable L getAsLeftOrNull() {
|
||||
if (isRight) {
|
||||
return null;
|
||||
}
|
||||
//noinspection unchecked
|
||||
return (L) value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #getAsRightOrNull()} but returns {@code null} is the value if Left instead of
|
||||
* throwing an exception.
|
||||
* @return contained value as Right or null
|
||||
*/
|
||||
public @Nullable R getAsRightOrNull() {
|
||||
if (!isRight) {
|
||||
return null;
|
||||
}
|
||||
//noinspection unchecked
|
||||
return (R) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (!(obj instanceof Either)) return false;
|
||||
Either that = (Either) obj;
|
||||
return this.isRight == that.isRight &&
|
||||
(this.value == that.value ||
|
||||
this.value != null && this.value.equals(that.value));
|
||||
}
|
||||
}
|
|
@ -1,3 +1,18 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.arch.core.util.Function;
|
||||
|
@ -17,7 +32,7 @@ import java.util.List;
|
|||
|
||||
public final class ViewDataUtils {
|
||||
@Nullable
|
||||
public static StatusViewData statusToViewData(@Nullable Status status) {
|
||||
public static StatusViewData.Concrete statusToViewData(@Nullable Status status) {
|
||||
if (status == null) return null;
|
||||
Status visibleStatus = status.reblog == null ? status : status.reblog;
|
||||
return new StatusViewData.Builder().setId(status.id)
|
||||
|
@ -55,12 +70,12 @@ public final class ViewDataUtils {
|
|||
return viewDatas;
|
||||
}
|
||||
|
||||
public static Function<Status, StatusViewData> statusMapper() {
|
||||
public static Function<Status, StatusViewData.Concrete> statusMapper() {
|
||||
return statusMapper;
|
||||
}
|
||||
|
||||
public static NotificationViewData notificationToViewData(Notification notification) {
|
||||
return new NotificationViewData(notification.type, notification.id, notification.account,
|
||||
public static NotificationViewData.Concrete notificationToViewData(Notification notification) {
|
||||
return new NotificationViewData.Concrete(notification.type, notification.id, notification.account,
|
||||
statusToViewData(notification.status));
|
||||
}
|
||||
|
||||
|
@ -73,10 +88,10 @@ public final class ViewDataUtils {
|
|||
return viewDatas;
|
||||
}
|
||||
|
||||
private static final Function<Status, StatusViewData> statusMapper =
|
||||
new Function<Status, StatusViewData>() {
|
||||
private static final Function<Status, StatusViewData.Concrete> statusMapper =
|
||||
new Function<Status, StatusViewData.Concrete>() {
|
||||
@Override
|
||||
public StatusViewData apply(Status input) {
|
||||
public StatusViewData.Concrete apply(Status input) {
|
||||
return ViewDataUtils.statusToViewData(input);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -49,16 +49,16 @@ public class ConversationLineItemDecoration extends RecyclerView.ItemDecoration
|
|||
|
||||
int position = parent.getChildAdapterPosition(child);
|
||||
ThreadAdapter adapter = (ThreadAdapter) parent.getAdapter();
|
||||
StatusViewData current = adapter.getItem(position);
|
||||
StatusViewData.Concrete current = adapter.getItem(position);
|
||||
int dividerTop, dividerBottom;
|
||||
if (current != null) {
|
||||
StatusViewData above = adapter.getItem(position - 1);
|
||||
StatusViewData.Concrete above = adapter.getItem(position - 1);
|
||||
if (above != null && above.getId().equals(current.getInReplyToId())) {
|
||||
dividerTop = child.getTop();
|
||||
} else {
|
||||
dividerTop = child.getTop() + avatarMargin;
|
||||
}
|
||||
StatusViewData below = adapter.getItem(position + 1);
|
||||
StatusViewData.Concrete below = adapter.getItem(position + 1);
|
||||
if (below != null && current.getId().equals(below.getInReplyToId())) {
|
||||
dividerBottom = child.getBottom();
|
||||
} else {
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.viewdata;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
|
@ -5,16 +20,27 @@ import com.keylesspalace.tusky.entity.Notification;
|
|||
|
||||
/**
|
||||
* Created by charlag on 12/07/2017.
|
||||
*
|
||||
* Class to represent data required to display either a notification or a placeholder.
|
||||
* It is either a {@link Placeholder} or a {@link Concrete}.
|
||||
* It is modelled this way because close relationship between placeholder and concrete notification
|
||||
* is fine in this case. Placeholder case is not modelled as a type of notification because
|
||||
* invariants would be violated and because it would model domain incorrectly. It is prefereable to
|
||||
* {@link com.keylesspalace.tusky.util.Either} because class hierarchy is cheaper, faster and
|
||||
* more native.
|
||||
*/
|
||||
public abstract class NotificationViewData {
|
||||
private NotificationViewData() {
|
||||
}
|
||||
|
||||
public final class NotificationViewData {
|
||||
public static final class Concrete extends NotificationViewData {
|
||||
private final Notification.Type type;
|
||||
private final String id;
|
||||
private final Account account;
|
||||
private final StatusViewData statusViewData;
|
||||
private final StatusViewData.Concrete statusViewData;
|
||||
|
||||
public NotificationViewData(Notification.Type type, String id, Account account,
|
||||
StatusViewData statusViewData) {
|
||||
public Concrete(Notification.Type type, String id, Account account,
|
||||
StatusViewData.Concrete statusViewData) {
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.account = account;
|
||||
|
@ -33,7 +59,20 @@ public final class NotificationViewData {
|
|||
return account;
|
||||
}
|
||||
|
||||
public StatusViewData getStatusViewData() {
|
||||
public StatusViewData.Concrete getStatusViewData() {
|
||||
return statusViewData;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Placeholder extends NotificationViewData {
|
||||
private final boolean isLoading;
|
||||
|
||||
public Placeholder(boolean isLoading) {
|
||||
this.isLoading = isLoading;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return isLoading;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.viewdata;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
@ -12,9 +27,17 @@ import java.util.List;
|
|||
|
||||
/**
|
||||
* Created by charlag on 11/07/2017.
|
||||
*
|
||||
* Class to represent data required to display either a notification or a placeholder.
|
||||
* It is either a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}.
|
||||
*/
|
||||
|
||||
public final class StatusViewData {
|
||||
public abstract class StatusViewData {
|
||||
|
||||
private StatusViewData() {
|
||||
}
|
||||
|
||||
public static final class Concrete extends StatusViewData {
|
||||
private final String id;
|
||||
private final Spanned content;
|
||||
private final boolean reblogged;
|
||||
|
@ -48,13 +71,13 @@ public final class StatusViewData {
|
|||
@Nullable
|
||||
private final Card card;
|
||||
|
||||
public StatusViewData(String id, Spanned content, boolean reblogged, boolean favourited,
|
||||
String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments,
|
||||
String rebloggedByUsername, String rebloggedAvatar, boolean sensitive, boolean isExpanded,
|
||||
public Concrete(String id, Spanned content, boolean reblogged, boolean favourited,
|
||||
@Nullable String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments,
|
||||
@Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded,
|
||||
boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar,
|
||||
Date createdAt, String reblogsCount, String favouritesCount, String inReplyToId,
|
||||
Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
|
||||
Status.Application application, List<Status.Emoji> emojis, Card card) {
|
||||
Date createdAt, String reblogsCount, String favouritesCount, @Nullable String inReplyToId,
|
||||
@Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
|
||||
Status.Application application, List<Status.Emoji> emojis, @Nullable Card card) {
|
||||
this.id = id;
|
||||
this.content = content;
|
||||
this.reblogged = reblogged;
|
||||
|
@ -183,10 +206,25 @@ public final class StatusViewData {
|
|||
return emojis;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Card getCard() {
|
||||
return card;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final class Placeholder extends StatusViewData {
|
||||
private final boolean isLoading;
|
||||
|
||||
public Placeholder(boolean isLoading) {
|
||||
this.isLoading = isLoading;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return isLoading;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String id;
|
||||
private Spanned content;
|
||||
|
@ -217,7 +255,7 @@ public final class StatusViewData {
|
|||
public Builder() {
|
||||
}
|
||||
|
||||
public Builder(final StatusViewData viewData) {
|
||||
public Builder(final StatusViewData.Concrete viewData) {
|
||||
id = viewData.id;
|
||||
content = viewData.content;
|
||||
reblogged = viewData.reblogged;
|
||||
|
@ -243,7 +281,6 @@ public final class StatusViewData {
|
|||
application = viewData.application;
|
||||
emojis = viewData.getEmojis();
|
||||
card = viewData.getCard();
|
||||
|
||||
}
|
||||
|
||||
public Builder setId(String id) {
|
||||
|
@ -371,12 +408,15 @@ public final class StatusViewData {
|
|||
return this;
|
||||
}
|
||||
|
||||
public StatusViewData createStatusViewData() {
|
||||
public StatusViewData.Concrete createStatusViewData() {
|
||||
if (this.emojis == null) emojis = Collections.emptyList();
|
||||
return new StatusViewData(id, content, reblogged, favourited, spoilerText, visibility,
|
||||
if (this.createdAt == null) createdAt = new Date();
|
||||
|
||||
return new StatusViewData.Concrete(id, content, reblogged, favourited, spoilerText, visibility,
|
||||
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
|
||||
isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, reblogsCount,
|
||||
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, emojis, card);
|
||||
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,
|
||||
emojis, card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
8
app/src/main/res/layout/item_status_placeholder.xml
Normal file
8
app/src/main/res/layout/item_status_placeholder.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/button_load_more"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:text="@string/load_more_placeholder_text"
|
||||
android:textColor="?attr/colorAccent" />
|
|
@ -237,5 +237,6 @@
|
|||
<string name="pref_title_alway_show_sensitive_media">Always show all nsfw content</string>
|
||||
<string name="title_media">Media</string>
|
||||
<string name="replying_to">Replying to @%s</string>
|
||||
<string name="load_more_placeholder_text">load more</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue