Preserve status states on updates. UI layer refactoring.
Some things were pulled out of adapters to fragments. New classes were introduced - StatusViewData and NotificationViewData. They not only have view state in them but also help decoupling. Because introducing parallel model list requires a lot of synchronisation PairedList was added. Also synchronisation between fragments and adapters is quiet tedious and error-prone and should be replaces with better solution. Oh, I also couldn’t resist and fixed bug with buttons animation in the same commit.
This commit is contained in:
parent
f68f6d7473
commit
90c1a83ba4
|
@ -59,10 +59,6 @@ public class TuskyApplication extends Application {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Picasso.with(this).setLoggingEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Install the new provider or, if there's a pre-existing older version, replace the
|
/* Install the new provider or, if there's a pre-existing older version, replace the
|
||||||
* existing version of it. */
|
* existing version of it. */
|
||||||
final String providerName = "BC";
|
final String providerName = "BC";
|
||||||
|
|
|
@ -31,13 +31,13 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
|
||||||
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
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 {
|
||||||
|
@ -46,16 +46,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
|
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
|
||||||
private static final int VIEW_TYPE_FOLLOW = 3;
|
private static final int VIEW_TYPE_FOLLOW = 3;
|
||||||
|
|
||||||
private List<Notification> notifications;
|
private List<NotificationViewData> notifications;
|
||||||
private StatusActionListener statusListener;
|
private StatusActionListener statusListener;
|
||||||
private NotificationActionListener notificationActionListener;
|
private NotificationActionListener notificationActionListener;
|
||||||
private FooterViewHolder.State footerState;
|
private FooterViewHolder.State footerState;
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
private String bottomId;
|
|
||||||
private String topId;
|
|
||||||
|
|
||||||
public NotificationsAdapter(StatusActionListener statusListener,
|
public NotificationsAdapter(StatusActionListener statusListener,
|
||||||
NotificationActionListener notificationActionListener) {
|
NotificationActionListener notificationActionListener) {
|
||||||
super();
|
super();
|
||||||
notifications = new ArrayList<>();
|
notifications = new ArrayList<>();
|
||||||
this.statusListener = statusListener;
|
this.statusListener = statusListener;
|
||||||
|
@ -94,28 +92,29 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
if (position < notifications.size()) {
|
if (position < notifications.size()) {
|
||||||
Notification notification = notifications.get(position);
|
NotificationViewData notification = notifications.get(position);
|
||||||
Notification.Type type = notification.type;
|
Notification.Type type = notification.getType();
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MENTION: {
|
case MENTION: {
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||||
Status status = notification.status;
|
StatusViewData status = notification.getStatusViewData();
|
||||||
holder.setupWithStatus(status, statusListener, mediaPreviewEnabled);
|
holder.setupWithStatus(status,
|
||||||
|
statusListener, mediaPreviewEnabled);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FAVOURITE:
|
case FAVOURITE:
|
||||||
case REBLOG: {
|
case REBLOG: {
|
||||||
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
||||||
holder.setMessage(type, notification.account.getDisplayName(),
|
holder.setMessage(type, notification.getStatusViewData().getUserFullName(),
|
||||||
notification.status);
|
notification.getStatusViewData());
|
||||||
holder.setupButtons(notificationActionListener, notification.account.id);
|
holder.setupButtons(notificationActionListener, notification.getAccount().id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FOLLOW: {
|
case FOLLOW: {
|
||||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
||||||
holder.setMessage(notification.account.getDisplayName(),
|
holder.setMessage(notification.getAccount().getDisplayName(),
|
||||||
notification.account.username, notification.account.avatar);
|
notification.getAccount().username, notification.getAccount().avatar);
|
||||||
holder.setupButtons(notificationActionListener, notification.account.id);
|
holder.setupButtons(notificationActionListener, notification.getAccount().id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,8 +134,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
if (position == notifications.size()) {
|
if (position == notifications.size()) {
|
||||||
return VIEW_TYPE_FOOTER;
|
return VIEW_TYPE_FOOTER;
|
||||||
} else {
|
} else {
|
||||||
Notification notification = notifications.get(position);
|
NotificationViewData notification = notifications.get(position);
|
||||||
switch (notification.type) {
|
switch (notification.getType()) {
|
||||||
default:
|
default:
|
||||||
case MENTION: {
|
case MENTION: {
|
||||||
return VIEW_TYPE_MENTION;
|
return VIEW_TYPE_MENTION;
|
||||||
|
@ -160,9 +159,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeAllByAccountId(String id) {
|
public void removeAllByAccountId(String id) {
|
||||||
for (int i = 0; i < notifications.size();) {
|
for (int i = 0; i < notifications.size(); ) {
|
||||||
Notification notification = notifications.get(i);
|
NotificationViewData notification = notifications.get(i);
|
||||||
if (id.equals(notification.account.id)) {
|
if (id.equals(notification.getAccount().id)) {
|
||||||
notifications.remove(i);
|
notifications.remove(i);
|
||||||
notifyItemRemoved(i);
|
notifyItemRemoved(i);
|
||||||
} else {
|
} else {
|
||||||
|
@ -172,61 +171,31 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Notification getItem(int position) {
|
public NotificationViewData 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(@Nullable List<Notification> newNotifications, @Nullable String fromId,
|
public void update(@Nullable List<NotificationViewData> newNotifications) {
|
||||||
@Nullable String uptoId) {
|
|
||||||
if (newNotifications == null || newNotifications.isEmpty()) {
|
if (newNotifications == null || newNotifications.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (fromId != null) {
|
notifications.clear();
|
||||||
bottomId = fromId;
|
notifications.addAll(newNotifications);
|
||||||
}
|
|
||||||
if (uptoId != null) {
|
|
||||||
topId = uptoId;
|
|
||||||
}
|
|
||||||
if (notifications.isEmpty()) {
|
|
||||||
// This construction removes duplicates.
|
|
||||||
notifications = new ArrayList<>(new HashSet<>(newNotifications));
|
|
||||||
} else {
|
|
||||||
int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1));
|
|
||||||
for (int i = 0; i < index; i++) {
|
|
||||||
notifications.remove(0);
|
|
||||||
}
|
|
||||||
int newIndex = newNotifications.indexOf(notifications.get(0));
|
|
||||||
if (newIndex == -1) {
|
|
||||||
notifications.addAll(0, newNotifications);
|
|
||||||
} else {
|
|
||||||
notifications.addAll(0, newNotifications.subList(0, newIndex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addItems(List<Notification> newNotifications, @Nullable String fromId) {
|
public void updateItemWithNotify(int position, NotificationViewData notification,
|
||||||
if (fromId != null) {
|
boolean notifyAdapter) {
|
||||||
bottomId = fromId;
|
notifications.set(position, notification);
|
||||||
}
|
if (notifyAdapter) notifyDataSetChanged();
|
||||||
int end = notifications.size();
|
|
||||||
Notification last = notifications.get(end - 1);
|
|
||||||
if (last != null && !findNotification(newNotifications, last.id)) {
|
|
||||||
notifications.addAll(newNotifications);
|
|
||||||
notifyItemRangeInserted(end, newNotifications.size());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean findNotification(List<Notification> notifications, String id) {
|
public void addItems(List<NotificationViewData> newNotifications, @Nullable String fromId) {
|
||||||
for (Notification notification : notifications) {
|
notifications.addAll(newNotifications);
|
||||||
if (notification.id.equals(id)) {
|
notifyItemRangeInserted(notifications.size(), newNotifications.size());
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
|
@ -238,16 +207,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
footerState = newFooterState;
|
footerState = newFooterState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
}
|
}
|
||||||
|
@ -314,7 +273,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
container = (ViewGroup) itemView.findViewById(R.id.notification_container);
|
container = (ViewGroup) itemView.findViewById(R.id.notification_container);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMessage(Notification.Type type, String displayName, Status status) {
|
void setMessage(Notification.Type type, String displayName, StatusViewData status) {
|
||||||
Context context = message.getContext();
|
Context context = message.getContext();
|
||||||
String format;
|
String format;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -339,7 +298,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
||||||
str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(),
|
str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(),
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
message.setText(str);
|
message.setText(str);
|
||||||
statusContent.setText(status.content);
|
statusContent.setText(status.getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupButtons(final NotificationActionListener listener, final String accountId) {
|
void setupButtons(final NotificationActionListener listener, final String accountId) {
|
||||||
|
|
|
@ -38,13 +38,14 @@ import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.util.DateUtils;
|
import com.keylesspalace.tusky.util.DateUtils;
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
import com.varunest.sparkbutton.SparkButton;
|
import com.varunest.sparkbutton.SparkButton;
|
||||||
import com.varunest.sparkbutton.SparkEventListener;
|
import com.varunest.sparkbutton.SparkEventListener;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
class StatusViewHolder extends RecyclerView.ViewHolder {
|
public class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
private View container;
|
private View container;
|
||||||
private TextView displayName;
|
private TextView displayName;
|
||||||
private TextView username;
|
private TextView username;
|
||||||
|
@ -173,7 +174,7 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
reblogButton.setChecked(reblogged);
|
reblogButton.setChecked(reblogged);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This should only be called after setReblogged, in order to override the tint correctly. */
|
// This should only be called after setReblogged, in order to override the tint correctly.
|
||||||
private void setRebloggingEnabled(boolean enabled, Status.Visibility visibility) {
|
private void setRebloggingEnabled(boolean enabled, Status.Visibility visibility) {
|
||||||
reblogButton.setEnabled(enabled);
|
reblogButton.setEnabled(enabled);
|
||||||
|
|
||||||
|
@ -202,7 +203,7 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setMediaPreviews(final Status.MediaAttachment[] attachments, boolean sensitive,
|
private void setMediaPreviews(final Status.MediaAttachment[] attachments, boolean sensitive,
|
||||||
final StatusActionListener listener) {
|
final StatusActionListener listener, boolean showingSensitive) {
|
||||||
final ImageView[] previews = {
|
final ImageView[] previews = {
|
||||||
mediaPreview0,
|
mediaPreview0,
|
||||||
mediaPreview1,
|
mediaPreview1,
|
||||||
|
@ -257,10 +258,13 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
sensitiveMediaWarning.setVisibility(View.VISIBLE);
|
sensitiveMediaWarning.setVisibility(showingSensitive ? View.GONE : View.VISIBLE);
|
||||||
sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() {
|
sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||||
|
listener.onContentHiddenChange(true, getAdapterPosition());
|
||||||
|
}
|
||||||
v.setVisibility(View.GONE);
|
v.setVisibility(View.GONE);
|
||||||
v.setOnClickListener(null);
|
v.setOnClickListener(null);
|
||||||
}
|
}
|
||||||
|
@ -277,23 +281,29 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
private static String getLabelTypeText(Context context, Status.MediaAttachment.Type type) {
|
private static String getLabelTypeText(Context context, Status.MediaAttachment.Type type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
default:
|
default:
|
||||||
case IMAGE: return context.getString(R.string.status_media_images);
|
case IMAGE:
|
||||||
|
return context.getString(R.string.status_media_images);
|
||||||
case GIFV:
|
case GIFV:
|
||||||
case VIDEO: return context.getString(R.string.status_media_video);
|
case VIDEO:
|
||||||
|
return context.getString(R.string.status_media_video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @DrawableRes int getLabelIcon(Status.MediaAttachment.Type type) {
|
private static
|
||||||
|
@DrawableRes
|
||||||
|
int getLabelIcon(Status.MediaAttachment.Type type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
default:
|
default:
|
||||||
case IMAGE: return R.drawable.ic_photo_24dp;
|
case IMAGE:
|
||||||
|
return R.drawable.ic_photo_24dp;
|
||||||
case GIFV:
|
case GIFV:
|
||||||
case VIDEO: return R.drawable.ic_videocam_24dp;
|
case VIDEO:
|
||||||
|
return R.drawable.ic_videocam_24dp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setMediaLabel(Status.MediaAttachment[] attachments, boolean sensitive,
|
private void setMediaLabel(Status.MediaAttachment[] attachments, boolean sensitive,
|
||||||
final StatusActionListener listener) {
|
final StatusActionListener listener) {
|
||||||
if (attachments.length == 0) {
|
if (attachments.length == 0) {
|
||||||
mediaLabel.setVisibility(View.GONE);
|
mediaLabel.setVisibility(View.GONE);
|
||||||
return;
|
return;
|
||||||
|
@ -334,15 +344,17 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
sensitiveMediaWarning.setVisibility(View.GONE);
|
sensitiveMediaWarning.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSpoilerText(String spoilerText) {
|
private void setSpoilerText(String spoilerText, final boolean expanded, final StatusActionListener listener) {
|
||||||
contentWarningDescription.setText(spoilerText);
|
contentWarningDescription.setText(spoilerText);
|
||||||
contentWarningBar.setVisibility(View.VISIBLE);
|
contentWarningBar.setVisibility(View.VISIBLE);
|
||||||
content.setVisibility(View.GONE);
|
contentWarningButton.setChecked(expanded);
|
||||||
contentWarningButton.setChecked(false);
|
|
||||||
contentWarningButton.setOnCheckedChangeListener(
|
contentWarningButton.setOnCheckedChangeListener(
|
||||||
new CompoundButton.OnCheckedChangeListener() {
|
new CompoundButton.OnCheckedChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
|
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||||
|
listener.onExpandedChange(isChecked, getAdapterPosition());
|
||||||
|
}
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
content.setVisibility(View.VISIBLE);
|
content.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
|
@ -350,6 +362,11 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (expanded) {
|
||||||
|
content.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
content.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideSpoilerText() {
|
private void hideSpoilerText() {
|
||||||
|
@ -378,19 +395,21 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
reblogButton.setEventListener(new SparkEventListener() {
|
reblogButton.setEventListener(new SparkEventListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onEvent(ImageView button, boolean buttonState) {
|
public void onEvent(ImageView button, boolean buttonState) {
|
||||||
int position = getAdapterPosition();
|
int position = getAdapterPosition();
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
listener.onReblog(!reblogged, position);
|
listener.onReblog(!reblogged, position);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEventAnimationEnd(ImageView button, boolean buttonState) {}
|
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEventAnimationStart(ImageView button, boolean buttonState) {}
|
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
favouriteButton.setEventListener(new SparkEventListener() {
|
favouriteButton.setEventListener(new SparkEventListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -402,10 +421,12 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEventAnimationEnd(ImageView button, boolean buttonState) {}
|
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEventAnimationStart(ImageView button, boolean buttonState) {}
|
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
moreButton.setOnClickListener(new View.OnClickListener() {
|
moreButton.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -433,27 +454,25 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
container.setOnClickListener(viewThreadListener);
|
container.setOnClickListener(viewThreadListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupWithStatus(Status status, final StatusActionListener listener,
|
void setupWithStatus(StatusViewData status, final StatusActionListener listener,
|
||||||
boolean mediaPreviewEnabled) {
|
boolean mediaPreviewEnabled) {
|
||||||
Status realStatus = status.getActionableStatus();
|
setDisplayName(status.getUserFullName());
|
||||||
|
setUsername(status.getNickname());
|
||||||
setDisplayName(realStatus.account.getDisplayName());
|
setCreatedAt(status.getCreatedAt());
|
||||||
setUsername(realStatus.account.username);
|
setContent(status.getContent(), status.getMentions(), listener);
|
||||||
setCreatedAt(realStatus.createdAt);
|
setAvatar(status.getAvatar());
|
||||||
setContent(realStatus.content, realStatus.mentions, listener);
|
setReblogged(status.isReblogged());
|
||||||
setAvatar(realStatus.account.avatar);
|
setFavourited(status.isFavourited());
|
||||||
setReblogged(realStatus.reblogged);
|
String rebloggedByDisplayName = status.getRebloggedByUsername();
|
||||||
setFavourited(realStatus.favourited);
|
if (rebloggedByDisplayName == null) {
|
||||||
String rebloggedByDisplayName = status.account.getDisplayName();
|
|
||||||
if (status.reblog == null) {
|
|
||||||
hideRebloggedByDisplayName();
|
hideRebloggedByDisplayName();
|
||||||
} else {
|
} else {
|
||||||
setRebloggedByDisplayName(rebloggedByDisplayName);
|
setRebloggedByDisplayName(rebloggedByDisplayName);
|
||||||
}
|
}
|
||||||
Status.MediaAttachment[] attachments = realStatus.attachments;
|
Status.MediaAttachment[] attachments = status.getAttachments();
|
||||||
boolean sensitive = realStatus.sensitive;
|
boolean sensitive = status.isSensitive();
|
||||||
if (mediaPreviewEnabled) {
|
if (mediaPreviewEnabled) {
|
||||||
setMediaPreviews(attachments, sensitive, listener);
|
setMediaPreviews(attachments, sensitive, listener, status.isShowingSensitiveContent());
|
||||||
/* A status without attachments is sometimes still marked sensitive, so it's necessary
|
/* A status without attachments is sometimes still marked sensitive, so it's necessary
|
||||||
* to check both whether there are any attachments and if it's marked sensitive. */
|
* to check both whether there are any attachments and if it's marked sensitive. */
|
||||||
if (!sensitive || attachments.length == 0) {
|
if (!sensitive || attachments.length == 0) {
|
||||||
|
@ -475,12 +494,12 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
videoIndicator.setVisibility(View.GONE);
|
videoIndicator.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupButtons(listener, realStatus.account.id);
|
setupButtons(listener, status.getSenderId());
|
||||||
setRebloggingEnabled(status.rebloggingAllowed(), status.getVisibility());
|
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
|
||||||
if (realStatus.spoilerText.isEmpty()) {
|
if (status.getSpoilerText() == null || status.getSpoilerText().isEmpty()) {
|
||||||
hideSpoilerText();
|
hideSpoilerText();
|
||||||
} else {
|
} else {
|
||||||
setSpoilerText(realStatus.spoilerText);
|
setSpoilerText(status.getSpoilerText(), status.isExpanded(), listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
// I think it's not efficient to create new object every time we bind a holder.
|
// I think it's not efficient to create new object every time we bind a holder.
|
||||||
|
|
|
@ -21,23 +21,20 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
public class ThreadAdapter extends RecyclerView.Adapter {
|
||||||
private List<Status> statuses;
|
private List<StatusViewData> statuses;
|
||||||
private StatusActionListener statusActionListener;
|
private StatusActionListener statusActionListener;
|
||||||
private int statusIndex;
|
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
|
|
||||||
public ThreadAdapter(StatusActionListener listener) {
|
public ThreadAdapter(StatusActionListener listener) {
|
||||||
this.statusActionListener = listener;
|
this.statusActionListener = listener;
|
||||||
this.statuses = new ArrayList<>();
|
this.statuses = new ArrayList<>();
|
||||||
this.statusIndex = 0;
|
|
||||||
mediaPreviewEnabled = true;
|
mediaPreviewEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,8 +48,9 @@ public class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRe
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||||
Status status = statuses.get(position);
|
StatusViewData status = statuses.get(position);
|
||||||
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled);
|
holder.setupWithStatus(status,
|
||||||
|
statusActionListener, mediaPreviewEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -60,77 +58,42 @@ public class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRe
|
||||||
return statuses.size();
|
return statuses.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void setStatuses(List<StatusViewData> statuses) {
|
||||||
public void removeItem(int position) {
|
this.statuses.clear();
|
||||||
statuses.remove(position);
|
this.statuses.addAll(statuses);
|
||||||
notifyItemRemoved(position);
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void addItem(int position, StatusViewData statusViewData) {
|
||||||
public void removeAllByAccountId(String accountId) {
|
statuses.add(position, statusViewData);
|
||||||
for (int i = 0; i < statuses.size();) {
|
notifyItemInserted(position);
|
||||||
Status status = statuses.get(i);
|
|
||||||
if (accountId.equals(status.account.id)) {
|
|
||||||
statuses.remove(i);
|
|
||||||
notifyItemRemoved(i);
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Status getItem(int position) {
|
public void clearItems() {
|
||||||
return statuses.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int setStatus(Status status) {
|
|
||||||
if (statuses.size() > 0
|
|
||||||
&& statusIndex < statuses.size()
|
|
||||||
&& statuses.get(statusIndex).equals(status)) {
|
|
||||||
// Do not add this status on refresh, it's already in there.
|
|
||||||
statuses.set(statusIndex, status);
|
|
||||||
return statusIndex;
|
|
||||||
}
|
|
||||||
int i = statusIndex;
|
|
||||||
statuses.add(i, status);
|
|
||||||
notifyItemInserted(i);
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContext(List<Status> ancestors, List<Status> descendants) {
|
|
||||||
Status mainStatus = null;
|
|
||||||
|
|
||||||
// In case of refresh, remove old ancestors and descendants first. We'll remove all blindly,
|
|
||||||
// as we have no guarantee on their order to be the same as before
|
|
||||||
int oldSize = statuses.size();
|
int oldSize = statuses.size();
|
||||||
if (oldSize > 1) {
|
statuses.clear();
|
||||||
mainStatus = statuses.get(statusIndex);
|
notifyItemRangeRemoved(0, oldSize);
|
||||||
statuses.clear();
|
}
|
||||||
notifyItemRangeRemoved(0, oldSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert newly fetched ancestors
|
public void addAll(int position, List<StatusViewData> statuses) {
|
||||||
statusIndex = ancestors.size();
|
this.statuses.addAll(position, statuses);
|
||||||
statuses.addAll(0, ancestors);
|
notifyItemRangeInserted(position, statuses.size());
|
||||||
notifyItemRangeInserted(0, statusIndex);
|
}
|
||||||
|
|
||||||
if (mainStatus != null) {
|
public void addAll(List<StatusViewData> statuses) {
|
||||||
// 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);
|
|
||||||
notifyItemInserted(statusIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert newly fetched descendants
|
|
||||||
int end = statuses.size();
|
int end = statuses.size();
|
||||||
statuses.addAll(descendants);
|
this.statuses.addAll(statuses);
|
||||||
notifyItemRangeInserted(end, descendants.size());
|
notifyItemRangeInserted(end, statuses.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
statuses.clear();
|
statuses.clear();
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
statusIndex = 0;
|
}
|
||||||
|
|
||||||
|
public void setItem(int position, StatusViewData status, boolean notifyAdapter) {
|
||||||
|
statuses.set(position, status);
|
||||||
|
if (notifyAdapter) notifyItemChanged(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMediaPreviewEnabled(boolean enabled) {
|
public void setMediaPreviewEnabled(boolean enabled) {
|
||||||
|
|
|
@ -22,24 +22,20 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
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 {
|
||||||
private static final int VIEW_TYPE_STATUS = 0;
|
private static final int VIEW_TYPE_STATUS = 0;
|
||||||
private static final int VIEW_TYPE_FOOTER = 1;
|
private static final int VIEW_TYPE_FOOTER = 1;
|
||||||
|
|
||||||
private List<Status> statuses;
|
private List<StatusViewData> statuses;
|
||||||
private StatusActionListener statusListener;
|
private StatusActionListener statusListener;
|
||||||
private FooterViewHolder.State footerState;
|
private FooterViewHolder.State footerState;
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
private String topId;
|
|
||||||
private String bottomId;
|
|
||||||
|
|
||||||
public TimelineAdapter(StatusActionListener statusListener) {
|
public TimelineAdapter(StatusActionListener statusListener) {
|
||||||
super();
|
super();
|
||||||
|
@ -70,7 +66,7 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
if (position < statuses.size()) {
|
if (position < statuses.size()) {
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||||
Status status = statuses.get(position);
|
StatusViewData status = statuses.get(position);
|
||||||
holder.setupWithStatus(status, statusListener, mediaPreviewEnabled);
|
holder.setupWithStatus(status, statusListener, mediaPreviewEnabled);
|
||||||
} else {
|
} else {
|
||||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||||
|
@ -92,73 +88,23 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void update(@Nullable List<StatusViewData> newStatuses) {
|
||||||
public void removeItem(int position) {
|
|
||||||
statuses.remove(position);
|
|
||||||
notifyItemRemoved(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeAllByAccountId(String accountId) {
|
|
||||||
for (int i = 0; i < statuses.size();) {
|
|
||||||
Status status = statuses.get(i);
|
|
||||||
if (accountId.equals(status.account.id)) {
|
|
||||||
statuses.remove(i);
|
|
||||||
notifyItemRemoved(i);
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
statuses.clear();
|
||||||
bottomId = fromId;
|
statuses.addAll(newStatuses);
|
||||||
}
|
|
||||||
if (uptoId != null) {
|
|
||||||
topId = uptoId;
|
|
||||||
}
|
|
||||||
if (statuses.isEmpty()) {
|
|
||||||
// This construction removes duplicates.
|
|
||||||
statuses = new ArrayList<>(new HashSet<>(newStatuses));
|
|
||||||
} else {
|
|
||||||
int index = statuses.indexOf(newStatuses.get(newStatuses.size() - 1));
|
|
||||||
for (int i = 0; i < index; i++) {
|
|
||||||
statuses.remove(0);
|
|
||||||
}
|
|
||||||
int newIndex = newStatuses.indexOf(statuses.get(0));
|
|
||||||
if (newIndex == -1) {
|
|
||||||
statuses.addAll(0, newStatuses);
|
|
||||||
} else {
|
|
||||||
statuses.addAll(0, newStatuses.subList(0, newIndex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addItems(List<Status> newStatuses, @Nullable String fromId) {
|
public void addItems(List<StatusViewData> newStatuses) {
|
||||||
if (fromId != null) {
|
statuses.addAll(newStatuses);
|
||||||
bottomId = fromId;
|
notifyItemRangeInserted(statuses.size(), newStatuses.size());
|
||||||
}
|
|
||||||
int end = statuses.size();
|
|
||||||
Status last = statuses.get(end - 1);
|
|
||||||
if (last != null && !findStatus(newStatuses, last.id)) {
|
|
||||||
statuses.addAll(newStatuses);
|
|
||||||
notifyItemRangeInserted(end, newStatuses.size());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean findStatus(List<Status> statuses, String id) {
|
public void changeItem(int position, StatusViewData newData, boolean notifyAdapter) {
|
||||||
for (Status status : statuses) {
|
statuses.set(position, newData);
|
||||||
if (status.id.equals(id)) {
|
if (notifyAdapter) notifyDataSetChanged();
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
|
@ -166,13 +112,6 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Status getItem(int position) {
|
|
||||||
if (position >= 0 && position < statuses.size()) {
|
|
||||||
return statuses.get(position);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFooterState(FooterViewHolder.State newFooterState) {
|
public void setFooterState(FooterViewHolder.State newFooterState) {
|
||||||
FooterViewHolder.State oldValue = footerState;
|
FooterViewHolder.State oldValue = footerState;
|
||||||
|
@ -185,14 +124,4 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment;
|
package com.keylesspalace.tusky.fragment;
|
||||||
|
|
||||||
|
import android.arch.core.util.Function;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
@ -39,12 +40,19 @@ import com.keylesspalace.tusky.adapter.NotificationsAdapter;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
||||||
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.HttpHeaderLink;
|
||||||
|
import com.keylesspalace.tusky.util.PairedList;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||||
|
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
|
@ -54,15 +62,16 @@ import retrofit2.Response;
|
||||||
public class NotificationsFragment extends SFragment implements
|
public class NotificationsFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener,
|
SwipeRefreshLayout.OnRefreshListener, StatusActionListener,
|
||||||
NotificationsAdapter.NotificationActionListener,
|
NotificationsAdapter.NotificationActionListener,
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener, AdapterItemRemover {
|
||||||
private static final String TAG = "Notifications"; // logging tag
|
private static final String TAG = "Notifications"; // logging tag
|
||||||
|
|
||||||
private enum FetchEnd {
|
private enum FetchEnd {
|
||||||
TOP,
|
TOP,
|
||||||
BOTTOM,
|
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;
|
||||||
|
@ -74,6 +83,16 @@ public class NotificationsFragment extends SFragment implements
|
||||||
private int topFetches;
|
private int topFetches;
|
||||||
private boolean bottomLoading;
|
private boolean bottomLoading;
|
||||||
private int bottomFetches;
|
private int bottomFetches;
|
||||||
|
private String bottomId;
|
||||||
|
private String topId;
|
||||||
|
|
||||||
|
private final PairedList<Notification, NotificationViewData> notifications
|
||||||
|
= new PairedList<>(new Function<Notification, NotificationViewData>() {
|
||||||
|
@Override
|
||||||
|
public NotificationViewData apply(Notification input) {
|
||||||
|
return ViewDataUtils.notificationToViewData(input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
public static NotificationsFragment newInstance() {
|
public static NotificationsFragment newInstance() {
|
||||||
NotificationsFragment fragment = new NotificationsFragment();
|
NotificationsFragment fragment = new NotificationsFragment();
|
||||||
|
@ -128,10 +147,12 @@ public class NotificationsFragment extends SFragment implements
|
||||||
TabLayout layout = (TabLayout) activity.findViewById(R.id.tab_layout);
|
TabLayout layout = (TabLayout) activity.findViewById(R.id.tab_layout);
|
||||||
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTabSelected(TabLayout.Tab tab) {}
|
public void onTabSelected(TabLayout.Tab tab) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabUnselected(TabLayout.Tab tab) {}
|
public void onTabUnselected(TabLayout.Tab tab) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabReselected(TabLayout.Tab tab) {
|
public void onTabReselected(TabLayout.Tab tab) {
|
||||||
|
@ -167,7 +188,7 @@ 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) {
|
||||||
NotificationsFragment.this.onLoadMore(view);
|
NotificationsFragment.this.onLoadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -187,30 +208,30 @@ public class NotificationsFragment extends SFragment implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRefresh() {
|
public void onRefresh() {
|
||||||
sendFetchNotificationsRequest(null, adapter.getTopId(), FetchEnd.TOP);
|
sendFetchNotificationsRequest(null, topId, FetchEnd.TOP);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReply(int position) {
|
public void onReply(int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = notifications.get(position);
|
||||||
super.reply(notification.status);
|
super.reply(notification.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReblog(boolean reblog, int position) {
|
public void onReblog(boolean reblog, int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = notifications.get(position);
|
||||||
super.reblog(notification.status, reblog, adapter, position);
|
super.reblog(notification.status, reblog, adapter, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFavourite(boolean favourite, int position) {
|
public void onFavourite(boolean favourite, int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = notifications.get(position);
|
||||||
super.favourite(notification.status, favourite, adapter, position);
|
super.favourite(notification.status, favourite, adapter, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMore(View view, int position) {
|
public void onMore(View view, int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = notifications.get(position);
|
||||||
super.more(notification.status, view, adapter, position);
|
super.more(notification.status, view, adapter, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,16 +242,42 @@ public class NotificationsFragment extends SFragment implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewThread(int position) {
|
public void onViewThread(int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = notifications.get(position);
|
||||||
super.viewThread(notification.status);
|
super.viewThread(notification.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOpenReblog(int position) {
|
public void onOpenReblog(int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = notifications.get(position);
|
||||||
if (notification != null) onViewAccount(notification.account.id);
|
if (notification != null) onViewAccount(notification.account.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExpandedChange(boolean expanded, int position) {
|
||||||
|
NotificationViewData old = notifications.getPairedItem(position);
|
||||||
|
StatusViewData statusViewData =
|
||||||
|
new StatusViewData.Builder(old.getStatusViewData())
|
||||||
|
.setIsExpanded(expanded)
|
||||||
|
.createStatusViewData();
|
||||||
|
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
|
||||||
|
old.getId(), old.getAccount(), statusViewData);
|
||||||
|
notifications.setPairedItem(position, notificationViewData);
|
||||||
|
adapter.updateItemWithNotify(position, notificationViewData, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContentHiddenChange(boolean isShowing, int position) {
|
||||||
|
NotificationViewData old = notifications.getPairedItem(position);
|
||||||
|
StatusViewData statusViewData =
|
||||||
|
new StatusViewData.Builder(old.getStatusViewData())
|
||||||
|
.setIsShowingSensitiveContent(isShowing)
|
||||||
|
.createStatusViewData();
|
||||||
|
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
|
||||||
|
old.getId(), old.getAccount(), statusViewData);
|
||||||
|
notifications.setPairedItem(position, notificationViewData);
|
||||||
|
adapter.updateItemWithNotify(position, notificationViewData, false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewTag(String tag) {
|
public void onViewTag(String tag) {
|
||||||
super.viewTag(tag);
|
super.viewTag(tag);
|
||||||
|
@ -257,9 +304,27 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onLoadMore(RecyclerView view) {
|
@Override
|
||||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
public void removeItem(int position) {
|
||||||
sendFetchNotificationsRequest(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
notifications.remove(position);
|
||||||
|
adapter.update(notifications.getPairedCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAllByAccountId(String accountId) {
|
||||||
|
// using iterator to safely remove items while iterating
|
||||||
|
Iterator<Notification> iterator = notifications.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Notification notification = iterator.next();
|
||||||
|
if (notification.account.id.equals(accountId)) {
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.update(notifications.getPairedCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onLoadMore() {
|
||||||
|
sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void jumpToTop() {
|
private void jumpToTop() {
|
||||||
|
@ -268,7 +333,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendFetchNotificationsRequest(String fromId, String uptoId,
|
private void sendFetchNotificationsRequest(String fromId, String uptoId,
|
||||||
final FetchEnd fetchEnd) {
|
final FetchEnd fetchEnd) {
|
||||||
/* If there is a fetch already ongoing, record however many fetches are requested and
|
/* If there is a fetch already ongoing, record however many fetches are requested and
|
||||||
* fulfill them after it's complete. */
|
* fulfill them after it's complete. */
|
||||||
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
||||||
|
@ -297,7 +362,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
call.enqueue(new Callback<List<Notification>>() {
|
call.enqueue(new Callback<List<Notification>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<List<Notification>> call,
|
public void onResponse(Call<List<Notification>> call,
|
||||||
Response<List<Notification>> response) {
|
Response<List<Notification>> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
String linkHeader = response.headers().get("Link");
|
String linkHeader = response.headers().get("Link");
|
||||||
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd);
|
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd);
|
||||||
|
@ -315,7 +380,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
|
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
|
||||||
FetchEnd fetchEnd) {
|
FetchEnd fetchEnd) {
|
||||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||||
switch (fetchEnd) {
|
switch (fetchEnd) {
|
||||||
case TOP: {
|
case TOP: {
|
||||||
|
@ -324,7 +389,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
uptoId = previous.uri.getQueryParameter("since_id");
|
uptoId = previous.uri.getQueryParameter("since_id");
|
||||||
}
|
}
|
||||||
adapter.update(notifications, null, uptoId);
|
update(notifications, null, uptoId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case BOTTOM: {
|
case BOTTOM: {
|
||||||
|
@ -334,7 +399,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
fromId = next.uri.getQueryParameter("max_id");
|
fromId = next.uri.getQueryParameter("max_id");
|
||||||
}
|
}
|
||||||
if (adapter.getItemCount() > 1) {
|
if (adapter.getItemCount() > 1) {
|
||||||
adapter.addItems(notifications, fromId);
|
addItems(notifications, fromId);
|
||||||
} else {
|
} else {
|
||||||
/* If this is the first fetch, also save the id from the "previous" link and
|
/* 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
|
* treat this operation as a refresh so the scroll position doesn't get pushed
|
||||||
|
@ -344,7 +409,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
uptoId = previous.uri.getQueryParameter("since_id");
|
uptoId = previous.uri.getQueryParameter("since_id");
|
||||||
}
|
}
|
||||||
adapter.update(notifications, fromId, uptoId);
|
update(notifications, fromId, uptoId);
|
||||||
}
|
}
|
||||||
/* Set last update id for pull notifications so that we don't get notified
|
/* Set last update id for pull notifications so that we don't get notified
|
||||||
* about things we already loaded here */
|
* about things we already loaded here */
|
||||||
|
@ -363,6 +428,60 @@ public class NotificationsFragment extends SFragment implements
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
|
||||||
|
@Nullable String uptoId) {
|
||||||
|
if (newNotifications == null || newNotifications.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fromId != null) {
|
||||||
|
bottomId = fromId;
|
||||||
|
}
|
||||||
|
if (uptoId != null) {
|
||||||
|
topId = uptoId;
|
||||||
|
}
|
||||||
|
if (notifications.isEmpty()) {
|
||||||
|
// This construction removes duplicates.
|
||||||
|
notifications.addAll(new HashSet<>(newNotifications));
|
||||||
|
} else {
|
||||||
|
int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1));
|
||||||
|
for (int i = 0; i < index; i++) {
|
||||||
|
notifications.remove(0);
|
||||||
|
}
|
||||||
|
int newIndex = newNotifications.indexOf(notifications.get(0));
|
||||||
|
if (newIndex == -1) {
|
||||||
|
notifications.addAll(0, newNotifications);
|
||||||
|
} else {
|
||||||
|
List<Notification> sublist = newNotifications.subList(0, newIndex);
|
||||||
|
notifications.addAll(0, sublist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.update(notifications.getPairedCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addItems(List<Notification> newNotifications, @Nullable String fromId) {
|
||||||
|
if (fromId != null) {
|
||||||
|
bottomId = fromId;
|
||||||
|
}
|
||||||
|
int end = notifications.size();
|
||||||
|
Notification last = notifications.get(end - 1);
|
||||||
|
if (last != null && !findNotification(newNotifications, last.id)) {
|
||||||
|
notifications.addAll(newNotifications);
|
||||||
|
List<NotificationViewData> newViewDatas = notifications.getPairedCopy()
|
||||||
|
.subList(notifications.size() - newNotifications.size(),
|
||||||
|
notifications.size() - 1);
|
||||||
|
adapter.addItems(newViewDatas, fromId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd) {
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||||
|
@ -375,7 +494,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
bottomLoading = false;
|
bottomLoading = false;
|
||||||
if (bottomFetches > 0) {
|
if (bottomFetches > 0) {
|
||||||
bottomFetches--;
|
bottomFetches--;
|
||||||
onLoadMore(recyclerView);
|
onLoadMore();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -392,6 +511,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
|
|
||||||
private void fullyRefresh() {
|
private void fullyRefresh() {
|
||||||
adapter.clear();
|
adapter.clear();
|
||||||
|
notifications.clear();
|
||||||
sendFetchNotificationsRequest(null, null, FetchEnd.TOP);
|
sendFetchNotificationsRequest(null, null, FetchEnd.TOP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.support.v7.widget.PopupMenu;
|
import android.support.v7.widget.PopupMenu;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||||
import com.keylesspalace.tusky.util.HtmlUtils;
|
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -107,9 +109,7 @@ public abstract class SFragment extends BaseFragment {
|
||||||
|
|
||||||
protected void reblog(final Status status, final boolean reblog,
|
protected void reblog(final Status status, final boolean reblog,
|
||||||
final RecyclerView.Adapter adapter, final int position) {
|
final RecyclerView.Adapter adapter, final int position) {
|
||||||
String id = status.getActionableId();
|
reblogWithCallback(status, reblog, new Callback<Status>() {
|
||||||
|
|
||||||
Callback<Status> cb = new Callback<Status>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
@ -124,8 +124,16 @@ public abstract class SFragment extends BaseFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<Status> call, Throwable t) {}
|
public void onFailure(Call<Status> call, Throwable t) {
|
||||||
};
|
Log.d(getClass().getSimpleName(), "Failed to reblog status: " + status.id);
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void reblogWithCallback(final Status status, final boolean reblog,
|
||||||
|
Callback<Status> callback) {
|
||||||
|
String id = status.getActionableId();
|
||||||
|
|
||||||
Call<Status> call;
|
Call<Status> call;
|
||||||
if (reblog) {
|
if (reblog) {
|
||||||
|
@ -133,15 +141,12 @@ public abstract class SFragment extends BaseFragment {
|
||||||
} else {
|
} else {
|
||||||
call = mastodonApi.unreblogStatus(id);
|
call = mastodonApi.unreblogStatus(id);
|
||||||
}
|
}
|
||||||
call.enqueue(cb);
|
call.enqueue(callback);
|
||||||
callList.add(call);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void favourite(final Status status, final boolean favourite,
|
protected void favourite(final Status status, final boolean favourite,
|
||||||
final RecyclerView.Adapter adapter, final int position) {
|
final RecyclerView.Adapter adapter, final int position) {
|
||||||
String id = status.getActionableId();
|
favouriteWithCallback(status, favourite, new Callback<Status>() {
|
||||||
|
|
||||||
Callback<Status> cb = new Callback<Status>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
@ -156,8 +161,16 @@ public abstract class SFragment extends BaseFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<Status> call, Throwable t) {}
|
public void onFailure(Call<Status> call, Throwable t) {
|
||||||
};
|
Log.d(getClass().getSimpleName(), "Failed to favourite status: " + status.id);
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void favouriteWithCallback(final Status status, final boolean favourite,
|
||||||
|
final Callback<Status> callback) {
|
||||||
|
String id = status.getActionableId();
|
||||||
|
|
||||||
Call<Status> call;
|
Call<Status> call;
|
||||||
if (favourite) {
|
if (favourite) {
|
||||||
|
@ -165,7 +178,7 @@ public abstract class SFragment extends BaseFragment {
|
||||||
} else {
|
} else {
|
||||||
call = mastodonApi.unfavouriteStatus(id);
|
call = mastodonApi.unfavouriteStatus(id);
|
||||||
}
|
}
|
||||||
call.enqueue(cb);
|
call.enqueue(callback);
|
||||||
callList.add(call);
|
callList.add(call);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,20 +33,27 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.BuildConfig;
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
import com.keylesspalace.tusky.MainActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
||||||
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.AdapterItemRemover;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
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.HttpHeaderLink;
|
||||||
|
import com.keylesspalace.tusky.util.PairedList;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
|
@ -55,8 +62,11 @@ import retrofit2.Response;
|
||||||
public class TimelineFragment extends SFragment implements
|
public class TimelineFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener,
|
SwipeRefreshLayout.OnRefreshListener,
|
||||||
StatusActionListener,
|
StatusActionListener,
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||||
|
AdapterItemRemover {
|
||||||
private static final String TAG = "Timeline"; // logging tag
|
private static final String TAG = "Timeline"; // logging tag
|
||||||
|
private static final String KIND_ARG = "kind";
|
||||||
|
private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id";
|
||||||
|
|
||||||
public enum Kind {
|
public enum Kind {
|
||||||
HOME,
|
HOME,
|
||||||
|
@ -88,11 +98,17 @@ public class TimelineFragment extends SFragment implements
|
||||||
private int topFetches;
|
private int topFetches;
|
||||||
private boolean bottomLoading;
|
private boolean bottomLoading;
|
||||||
private int bottomFetches;
|
private int bottomFetches;
|
||||||
|
@Nullable
|
||||||
|
private String bottomId;
|
||||||
|
@Nullable
|
||||||
|
private String upToId;
|
||||||
|
private PairedList<Status, StatusViewData> statuses =
|
||||||
|
new PairedList<>(ViewDataUtils.statusMapper());
|
||||||
|
|
||||||
public static TimelineFragment newInstance(Kind kind) {
|
public static TimelineFragment newInstance(Kind kind) {
|
||||||
TimelineFragment fragment = new TimelineFragment();
|
TimelineFragment fragment = new TimelineFragment();
|
||||||
Bundle arguments = new Bundle();
|
Bundle arguments = new Bundle();
|
||||||
arguments.putString("kind", kind.name());
|
arguments.putString(KIND_ARG, kind.name());
|
||||||
fragment.setArguments(arguments);
|
fragment.setArguments(arguments);
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
@ -100,19 +116,19 @@ public class TimelineFragment extends SFragment implements
|
||||||
public static TimelineFragment newInstance(Kind kind, String hashtagOrId) {
|
public static TimelineFragment newInstance(Kind kind, String hashtagOrId) {
|
||||||
TimelineFragment fragment = new TimelineFragment();
|
TimelineFragment fragment = new TimelineFragment();
|
||||||
Bundle arguments = new Bundle();
|
Bundle arguments = new Bundle();
|
||||||
arguments.putString("kind", kind.name());
|
arguments.putString(KIND_ARG, kind.name());
|
||||||
arguments.putString("hashtag_or_id", hashtagOrId);
|
arguments.putString(HASHTAG_OR_ID_ARG, hashtagOrId);
|
||||||
fragment.setArguments(arguments);
|
fragment.setArguments(arguments);
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
Bundle arguments = getArguments();
|
Bundle arguments = getArguments();
|
||||||
kind = Kind.valueOf(arguments.getString("kind"));
|
kind = Kind.valueOf(arguments.getString(KIND_ARG));
|
||||||
if (kind == Kind.TAG || kind == Kind.USER) {
|
if (kind == Kind.TAG || kind == Kind.USER) {
|
||||||
hashtagOrId = arguments.getString("hashtag_or_id");
|
hashtagOrId = arguments.getString(HASHTAG_OR_ID_ARG);
|
||||||
}
|
}
|
||||||
|
|
||||||
final View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
final View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
||||||
|
@ -140,7 +156,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
timelineReceiver = new TimelineReceiver(adapter, this);
|
timelineReceiver = new TimelineReceiver(this, this);
|
||||||
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
||||||
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(kind));
|
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(kind));
|
||||||
|
|
||||||
|
@ -155,10 +171,12 @@ public class TimelineFragment extends SFragment implements
|
||||||
TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
||||||
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTabSelected(TabLayout.Tab tab) {}
|
public void onTabSelected(TabLayout.Tab tab) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabUnselected(TabLayout.Tab tab) {}
|
public void onTabUnselected(TabLayout.Tab tab) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabReselected(TabLayout.Tab tab) {
|
public void onTabReselected(TabLayout.Tab tab) {
|
||||||
|
@ -196,7 +214,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||||
TimelineFragment.this.onLoadMore(view);
|
TimelineFragment.this.onLoadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
@ -204,7 +222,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
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) {
|
||||||
TimelineFragment.this.onLoadMore(view);
|
TimelineFragment.this.onLoadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -223,32 +241,98 @@ public class TimelineFragment extends SFragment implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRefresh() {
|
public void onRefresh() {
|
||||||
sendFetchTimelineRequest(null, adapter.getTopId(), FetchEnd.TOP);
|
sendFetchTimelineRequest(null, upToId, FetchEnd.TOP);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReply(int position) {
|
public void onReply(int position) {
|
||||||
super.reply(adapter.getItem(position));
|
super.reply(statuses.get(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReblog(final boolean reblog, final int position) {
|
public void onReblog(final boolean reblog, final int position) {
|
||||||
super.reblog(adapter.getItem(position), reblog, adapter, position);
|
final Status status = statuses.get(position);
|
||||||
|
super.reblogWithCallback(status, reblog, new Callback<Status>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
status.reblogged = reblog;
|
||||||
|
|
||||||
|
if (status.reblog != null) {
|
||||||
|
status.reblog.reblogged = reblog;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusViewData newViewData =
|
||||||
|
new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||||
|
.setReblogged(reblog)
|
||||||
|
.createStatusViewData();
|
||||||
|
statuses.setPairedItem(position, newViewData);
|
||||||
|
adapter.changeItem(position, newViewData, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<Status> call, Throwable t) {
|
||||||
|
Log.d(TAG, "Failed to reblog status " + status.id);
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFavourite(final boolean favourite, final int position) {
|
public void onFavourite(final boolean favourite, final int position) {
|
||||||
super.favourite(adapter.getItem(position), favourite, adapter, position);
|
final Status status = statuses.get(position);
|
||||||
|
|
||||||
|
super.favouriteWithCallback(status, favourite, new Callback<Status>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
status.favourited = favourite;
|
||||||
|
|
||||||
|
if (status.reblog != null) {
|
||||||
|
status.reblog.favourited = favourite;
|
||||||
|
}
|
||||||
|
StatusViewData newViewData = new StatusViewData
|
||||||
|
.Builder(statuses.getPairedItem(position))
|
||||||
|
.setFavourited(favourite)
|
||||||
|
.createStatusViewData();
|
||||||
|
statuses.setPairedItem(position, newViewData);
|
||||||
|
adapter.changeItem(position, newViewData, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<Status> call, Throwable t) {
|
||||||
|
Log.d(TAG, "Failed to favourite status " + status.id);
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMore(View view, final int position) {
|
public void onMore(View view, final int position) {
|
||||||
super.more(adapter.getItem(position), view, adapter, position);
|
super.more(statuses.get(position), view, this, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOpenReblog(int position) {
|
public void onOpenReblog(int position) {
|
||||||
super.openReblog(adapter.getItem(position));
|
super.openReblog(statuses.get(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExpandedChange(boolean expanded, int position) {
|
||||||
|
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||||
|
.setIsExpanded(expanded).createStatusViewData();
|
||||||
|
statuses.setPairedItem(position, newViewData);
|
||||||
|
adapter.changeItem(position, newViewData, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContentHiddenChange(boolean isShowing, int position) {
|
||||||
|
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||||
|
.setIsShowingSensitiveContent(isShowing).createStatusViewData();
|
||||||
|
statuses.setPairedItem(position, newViewData);
|
||||||
|
adapter.changeItem(position, newViewData, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -258,7 +342,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewThread(int position) {
|
public void onViewThread(int position) {
|
||||||
super.viewThread(adapter.getItem(position));
|
super.viewThread(statuses.get(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -314,9 +398,27 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onLoadMore(RecyclerView view) {
|
@Override
|
||||||
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
|
public void removeItem(int position) {
|
||||||
sendFetchTimelineRequest(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
statuses.remove(position);
|
||||||
|
adapter.update(statuses.getPairedCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAllByAccountId(String accountId) {
|
||||||
|
// using iterator to safely remove items while iterating
|
||||||
|
Iterator<Status> iterator = statuses.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Status status = iterator.next();
|
||||||
|
if (status.account.id.equals(accountId)) {
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.update(statuses.getPairedCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onLoadMore() {
|
||||||
|
sendFetchTimelineRequest(bottomId, null, FetchEnd.BOTTOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fullyRefresh() {
|
private void fullyRefresh() {
|
||||||
|
@ -338,21 +440,27 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private Call<List<Status>> getFetchCallByTimelineType(Kind kind, String tagOrId, String fromId,
|
private Call<List<Status>> getFetchCallByTimelineType(Kind kind, String tagOrId, String fromId,
|
||||||
String uptoId) {
|
String uptoId) {
|
||||||
MastodonApi api = mastodonApi;
|
MastodonApi api = mastodonApi;
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
default:
|
default:
|
||||||
case HOME: return api.homeTimeline(fromId, uptoId, null);
|
case HOME:
|
||||||
case PUBLIC_FEDERATED: return api.publicTimeline(null, fromId, uptoId, null);
|
return api.homeTimeline(fromId, uptoId, null);
|
||||||
case PUBLIC_LOCAL: return api.publicTimeline(true, fromId, uptoId, null);
|
case PUBLIC_FEDERATED:
|
||||||
case TAG: return api.hashtagTimeline(tagOrId, null, fromId, uptoId, null);
|
return api.publicTimeline(null, fromId, uptoId, null);
|
||||||
case USER: return api.accountStatuses(tagOrId, fromId, uptoId, null);
|
case PUBLIC_LOCAL:
|
||||||
case FAVOURITES: return api.favourites(fromId, uptoId, null);
|
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,
|
private void sendFetchTimelineRequest(@Nullable String fromId, @Nullable String uptoId,
|
||||||
final FetchEnd fetchEnd) {
|
final FetchEnd fetchEnd) {
|
||||||
/* If there is a fetch already ongoing, record however many fetches are requested and
|
/* If there is a fetch already ongoing, record however many fetches are requested and
|
||||||
* fulfill them after it's complete. */
|
* fulfill them after it's complete. */
|
||||||
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
||||||
|
@ -399,7 +507,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
|
public void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
|
||||||
FetchEnd fetchEnd) {
|
FetchEnd fetchEnd) {
|
||||||
filterStatuses(statuses);
|
filterStatuses(statuses);
|
||||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||||
switch (fetchEnd) {
|
switch (fetchEnd) {
|
||||||
|
@ -409,7 +517,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
uptoId = previous.uri.getQueryParameter("since_id");
|
uptoId = previous.uri.getQueryParameter("since_id");
|
||||||
}
|
}
|
||||||
adapter.update(statuses, null, uptoId);
|
updateStatuses(statuses, null, uptoId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case BOTTOM: {
|
case BOTTOM: {
|
||||||
|
@ -419,7 +527,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
fromId = next.uri.getQueryParameter("max_id");
|
fromId = next.uri.getQueryParameter("max_id");
|
||||||
}
|
}
|
||||||
if (adapter.getItemCount() > 1) {
|
if (adapter.getItemCount() > 1) {
|
||||||
adapter.addItems(statuses, fromId);
|
addItems(statuses, fromId);
|
||||||
} else {
|
} else {
|
||||||
/* If this is the first fetch, also save the id from the "previous" link and
|
/* 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
|
* treat this operation as a refresh so the scroll position doesn't get pushed
|
||||||
|
@ -429,7 +537,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
uptoId = previous.uri.getQueryParameter("since_id");
|
uptoId = previous.uri.getQueryParameter("since_id");
|
||||||
}
|
}
|
||||||
adapter.update(statuses, fromId, uptoId);
|
updateStatuses(statuses, fromId, uptoId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -455,7 +563,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
bottomLoading = false;
|
bottomLoading = false;
|
||||||
if (bottomFetches > 0) {
|
if (bottomFetches > 0) {
|
||||||
bottomFetches--;
|
bottomFetches--;
|
||||||
onLoadMore(recyclerView);
|
onLoadMore();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -480,4 +588,63 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateStatuses(List<Status> newStatuses, @Nullable String fromId,
|
||||||
|
@Nullable String toId) {
|
||||||
|
if (newStatuses == null || newStatuses.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fromId != null) {
|
||||||
|
bottomId = fromId;
|
||||||
|
}
|
||||||
|
if (toId != null) {
|
||||||
|
upToId = toId;
|
||||||
|
}
|
||||||
|
if (statuses.isEmpty()) {
|
||||||
|
// This construction removes duplicates.
|
||||||
|
statuses.addAll(new HashSet<>(newStatuses));
|
||||||
|
} else {
|
||||||
|
Status lastOfNew = newStatuses.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));
|
||||||
|
if (newIndex == -1) {
|
||||||
|
statuses.addAll(0, newStatuses);
|
||||||
|
} else {
|
||||||
|
statuses.addAll(0, newStatuses.subList(0, newIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.update(statuses.getPairedCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addItems(List<Status> newStatuses, @Nullable String fromId) {
|
||||||
|
int end = statuses.size();
|
||||||
|
Status last = statuses.get(end - 1);
|
||||||
|
if (last != null && !findStatus(newStatuses, last.id)) {
|
||||||
|
statuses.addAll(newStatuses);
|
||||||
|
List<StatusViewData> newViewDatas = statuses.getPairedCopy()
|
||||||
|
.subList(statuses.size() - newStatuses.size(), statuses.size());
|
||||||
|
if (BuildConfig.DEBUG && newStatuses.size() != newViewDatas.size()) {
|
||||||
|
String error = String.format(Locale.getDefault(),
|
||||||
|
"Incorrectly got statusViewData sublist." +
|
||||||
|
" newStatuses.size == %d newViewDatas.size == %d, statuses.size == %d",
|
||||||
|
newStatuses.size(), newViewDatas.size(), statuses.size());
|
||||||
|
throw new AssertionError(error);
|
||||||
|
}
|
||||||
|
if (fromId != null) bottomId = fromId;
|
||||||
|
adapter.addItems(newViewDatas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static boolean findStatus(List<Status> statuses, String id) {
|
||||||
|
for (Status status : statuses) {
|
||||||
|
if (status.id.equals(id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,22 +33,32 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.BuildConfig;
|
||||||
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.adapter.ThreadAdapter;
|
import com.keylesspalace.tusky.adapter.ThreadAdapter;
|
||||||
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.R;
|
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
||||||
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.PairedList;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||||
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
|
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class ViewThreadFragment extends SFragment implements
|
public class ViewThreadFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener {
|
SwipeRefreshLayout.OnRefreshListener, StatusActionListener,
|
||||||
|
AdapterItemRemover {
|
||||||
private static final String TAG = "ViewThreadFragment";
|
private static final String TAG = "ViewThreadFragment";
|
||||||
|
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
|
@ -57,6 +67,11 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
private String thisThreadsStatusId;
|
private String thisThreadsStatusId;
|
||||||
private TimelineReceiver timelineReceiver;
|
private TimelineReceiver timelineReceiver;
|
||||||
|
|
||||||
|
int statusIndex = 0;
|
||||||
|
|
||||||
|
private final PairedList<Status, StatusViewData> statuses =
|
||||||
|
new PairedList<>(ViewDataUtils.statusMapper());
|
||||||
|
|
||||||
public static ViewThreadFragment newInstance(String id) {
|
public static ViewThreadFragment newInstance(String id) {
|
||||||
Bundle arguments = new Bundle();
|
Bundle arguments = new Bundle();
|
||||||
ViewThreadFragment fragment = new ViewThreadFragment();
|
ViewThreadFragment fragment = new ViewThreadFragment();
|
||||||
|
@ -96,7 +111,7 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
|
|
||||||
thisThreadsStatusId = null;
|
thisThreadsStatusId = null;
|
||||||
|
|
||||||
timelineReceiver = new TimelineReceiver(adapter, this);
|
timelineReceiver = new TimelineReceiver(this, this);
|
||||||
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
||||||
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(null));
|
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(null));
|
||||||
|
|
||||||
|
@ -125,22 +140,65 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReply(int position) {
|
public void onReply(int position) {
|
||||||
super.reply(adapter.getItem(position));
|
super.reply(statuses.get(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReblog(boolean reblog, int position) {
|
public void onReblog(final boolean reblog, final int position) {
|
||||||
super.reblog(adapter.getItem(position), reblog, adapter, position);
|
final Status status = statuses.get(position);
|
||||||
|
super.reblogWithCallback(statuses.get(position), reblog, new Callback<Status>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<Status> call, Response<Status> response) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
status.reblogged = reblog;
|
||||||
|
|
||||||
|
if (status.reblog != null) {
|
||||||
|
status.reblog.reblogged = reblog;
|
||||||
|
}
|
||||||
|
// create new viewData as side effect
|
||||||
|
statuses.set(position, status);
|
||||||
|
|
||||||
|
adapter.setItem(position, statuses.getPairedItem(position), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<Status> call, Throwable t) {
|
||||||
|
Log.d(getClass().getSimpleName(), "Failed to reblog status: " + status.id);
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFavourite(boolean favourite, int position) {
|
public void onFavourite(final boolean favourite, final int position) {
|
||||||
super.favourite(adapter.getItem(position), favourite, adapter, position);
|
final Status status = statuses.get(position);
|
||||||
|
super.favouriteWithCallback(statuses.get(position), favourite, new Callback<Status>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<Status> call, Response<Status> response) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
status.favourited = favourite;
|
||||||
|
|
||||||
|
if (status.reblog != null) {
|
||||||
|
status.reblog.favourited = favourite;
|
||||||
|
}
|
||||||
|
// create new viewData as side effect
|
||||||
|
statuses.set(position, status);
|
||||||
|
adapter.setItem(position, statuses.getPairedItem(position), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<Status> call, Throwable t) {
|
||||||
|
Log.d(getClass().getSimpleName(), "Failed to favourite status: " + status.id);
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMore(View view, int position) {
|
public void onMore(View view, int position) {
|
||||||
super.more(adapter.getItem(position), view, adapter, position);
|
super.more(statuses.get(position), view, this, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -150,7 +208,7 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewThread(int position) {
|
public void onViewThread(int position) {
|
||||||
Status status = adapter.getItem(position);
|
Status status = statuses.get(position);
|
||||||
if (thisThreadsStatusId.equals(status.id)) {
|
if (thisThreadsStatusId.equals(status.id)) {
|
||||||
// If already viewing this thread, don't reopen it.
|
// If already viewing this thread, don't reopen it.
|
||||||
return;
|
return;
|
||||||
|
@ -161,7 +219,25 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
@Override
|
@Override
|
||||||
public void onOpenReblog(int position) {
|
public void onOpenReblog(int position) {
|
||||||
// there should be no reblogs in the thread but let's implement it to be sure
|
// there should be no reblogs in the thread but let's implement it to be sure
|
||||||
super.openReblog(adapter.getItem(position));
|
super.openReblog(statuses.get(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExpandedChange(boolean expanded, int position) {
|
||||||
|
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||||
|
.setIsExpanded(expanded)
|
||||||
|
.createStatusViewData();
|
||||||
|
statuses.setPairedItem(position, newViewData);
|
||||||
|
adapter.setItem(position, newViewData, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContentHiddenChange(boolean isShowing, int position) {
|
||||||
|
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||||
|
.setIsShowingSensitiveContent(isShowing)
|
||||||
|
.createStatusViewData();
|
||||||
|
statuses.setPairedItem(position, newViewData);
|
||||||
|
adapter.setItem(position, newViewData, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -174,13 +250,37 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
super.viewAccount(id);
|
super.viewAccount(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeItem(int position) {
|
||||||
|
statuses.remove(position);
|
||||||
|
adapter.setStatuses(statuses.getPairedCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAllByAccountId(String accountId) {
|
||||||
|
Status status = null;
|
||||||
|
if (!statuses.isEmpty()) {
|
||||||
|
status = statuses.get(statusIndex);
|
||||||
|
}
|
||||||
|
// using iterator to safely remove items while iterating
|
||||||
|
Iterator<Status> iterator = statuses.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Status s = iterator.next();
|
||||||
|
if (s.account.id.equals(accountId)) {
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statusIndex = statuses.indexOf(status);
|
||||||
|
adapter.setStatuses(statuses.getPairedCopy());
|
||||||
|
}
|
||||||
|
|
||||||
private void sendStatusRequest(final String id) {
|
private void sendStatusRequest(final String id) {
|
||||||
Call<Status> call = mastodonApi.status(id);
|
Call<Status> call = mastodonApi.status(id);
|
||||||
call.enqueue(new Callback<Status>() {
|
call.enqueue(new Callback<Status>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<Status> call, Response<Status> response) {
|
public void onResponse(Call<Status> call, Response<Status> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
int position = adapter.setStatus(response.body());
|
int position = setStatus(response.body());
|
||||||
recyclerView.scrollToPosition(position);
|
recyclerView.scrollToPosition(position);
|
||||||
} else {
|
} else {
|
||||||
onThreadRequestFailure(id);
|
onThreadRequestFailure(id);
|
||||||
|
@ -203,7 +303,7 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
StatusContext context = response.body();
|
StatusContext context = response.body();
|
||||||
adapter.setContext(context.ancestors, context.descendants);
|
setContext(context.ancestors, context.descendants);
|
||||||
} else {
|
} else {
|
||||||
onThreadRequestFailure(id);
|
onThreadRequestFailure(id);
|
||||||
}
|
}
|
||||||
|
@ -234,4 +334,72 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
Log.e(TAG, "Couldn't display thread fetch error message");
|
Log.e(TAG, "Couldn't display thread fetch error message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int setStatus(Status status) {
|
||||||
|
if (statuses.size() > 0
|
||||||
|
&& statusIndex < statuses.size()
|
||||||
|
&& statuses.get(statusIndex).equals(status)) {
|
||||||
|
// Do not add this status on refresh, it's already in there.
|
||||||
|
statuses.set(statusIndex, status);
|
||||||
|
return statusIndex;
|
||||||
|
}
|
||||||
|
int i = statusIndex;
|
||||||
|
statuses.add(i, status);
|
||||||
|
adapter.addItem(i, statuses.getPairedItem(i));
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContext(List<Status> ancestors, List<Status> descendants) {
|
||||||
|
Status mainStatus = null;
|
||||||
|
|
||||||
|
// In case of refresh, remove old ancestors and descendants first. We'll remove all blindly,
|
||||||
|
// as we have no guarantee on their order to be the same as before
|
||||||
|
int oldSize = statuses.size();
|
||||||
|
if (oldSize > 1) {
|
||||||
|
mainStatus = statuses.get(statusIndex);
|
||||||
|
statuses.clear();
|
||||||
|
adapter.clearItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert newly fetched ancestors
|
||||||
|
statusIndex = ancestors.size();
|
||||||
|
statuses.addAll(0, ancestors);
|
||||||
|
List<StatusViewData> ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex);
|
||||||
|
if (BuildConfig.DEBUG && ancestors.size() != ancestorsViewDatas.size()) {
|
||||||
|
String error = String.format(Locale.getDefault(),
|
||||||
|
"Incorrectly got statusViewData sublist." +
|
||||||
|
" ancestors.size == %d ancestorsViewDatas.size == %d," +
|
||||||
|
" statuses.size == %d",
|
||||||
|
ancestors.size(), ancestorsViewDatas.size(), statuses.size());
|
||||||
|
throw new AssertionError(error);
|
||||||
|
}
|
||||||
|
adapter.addAll(0, ancestorsViewDatas);
|
||||||
|
|
||||||
|
if (mainStatus != null) {
|
||||||
|
// 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);
|
||||||
|
adapter.addItem(statusIndex, statuses.getPairedItem(statusIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert newly fetched descendants
|
||||||
|
statuses.addAll(descendants);
|
||||||
|
List<StatusViewData> descendantsViewData;
|
||||||
|
descendantsViewData = statuses.getPairedCopy()
|
||||||
|
.subList(statuses.size() - descendants.size(), statuses.size());
|
||||||
|
if (BuildConfig.DEBUG && descendants.size() != descendantsViewData.size()) {
|
||||||
|
String error = String.format(Locale.getDefault(),
|
||||||
|
"Incorrectly got statusViewData sublist." +
|
||||||
|
" descendants.size == %d descendantsViewData.size == %d," +
|
||||||
|
" statuses.size == %d",
|
||||||
|
descendants.size(), descendantsViewData.size(), statuses.size());
|
||||||
|
throw new AssertionError(error);
|
||||||
|
}
|
||||||
|
adapter.addAll(descendantsViewData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
statuses.clear();
|
||||||
|
adapter.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ package com.keylesspalace.tusky.interfaces;
|
||||||
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.adapter.StatusViewHolder;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
public interface StatusActionListener extends LinkListener {
|
public interface StatusActionListener extends LinkListener {
|
||||||
|
@ -27,4 +28,6 @@ public interface StatusActionListener extends LinkListener {
|
||||||
void onViewMedia(String[] urls, int index, Status.MediaAttachment.Type type);
|
void onViewMedia(String[] urls, int index, Status.MediaAttachment.Type type);
|
||||||
void onViewThread(int position);
|
void onViewThread(int position);
|
||||||
void onOpenReblog(int position);
|
void onOpenReblog(int position);
|
||||||
|
void onExpandedChange(boolean expanded, int position);
|
||||||
|
void onContentHiddenChange(boolean isShowing, int position);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
package com.keylesspalace.tusky.util;
|
||||||
|
|
||||||
|
import android.arch.core.util.Function;
|
||||||
|
|
||||||
|
import java.util.AbstractList;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This list implementation can help to keep two lists in sync - like real models and view models.
|
||||||
|
* Every operation on the main list triggers update of the supplementary list (but not vice versa).
|
||||||
|
* This makes sure that the main list is always the source of truth.
|
||||||
|
* Main list is projected to the supplementary list by the passed mapper function.
|
||||||
|
* Paired list is newer actually exposed and clients are provided with {@code getPairedCopy()},
|
||||||
|
* {@code getPairedItem()} and {@code setPairedItem()}. This prevents modifications of the
|
||||||
|
* supplementary list size so lists are always have the same length.
|
||||||
|
* This implementation will not try to recover from exceptional cases so lists may be out of sync
|
||||||
|
* after the exception.
|
||||||
|
*
|
||||||
|
* It is most useful with immutable data because we cannot track changes inside stored objects.
|
||||||
|
* @param <T> type of elements in the main list
|
||||||
|
* @param <V> type of elements in supplementary list
|
||||||
|
*/
|
||||||
|
public final class PairedList<T, V> extends AbstractList<T> {
|
||||||
|
private final List<T> main = new ArrayList<>();
|
||||||
|
private final List<V> synced = new ArrayList<>();
|
||||||
|
private final Function<T, ? extends V> mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct new paired list. Main and supplementary lists will be empty.
|
||||||
|
* @param mapper Function, which will be used to translate items from the main list to the
|
||||||
|
* supplementary one.
|
||||||
|
*/
|
||||||
|
public PairedList(Function<T, ? extends V> mapper) {
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<V> getPairedCopy() {
|
||||||
|
return new ArrayList<>(synced);
|
||||||
|
}
|
||||||
|
|
||||||
|
public V getPairedItem(int index) {
|
||||||
|
return synced.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPairedItem(int index, V element) {
|
||||||
|
synced.set(index, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T get(int index) {
|
||||||
|
return main.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T set(int index, T element) {
|
||||||
|
synced.set(index, mapper.apply(element));
|
||||||
|
return main.set(index, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean add(T t) {
|
||||||
|
synced.add(mapper.apply(t));
|
||||||
|
return main.add(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(int index, T element) {
|
||||||
|
synced.add(index, mapper.apply(element));
|
||||||
|
main.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T remove(int index) {
|
||||||
|
synced.remove(index);
|
||||||
|
return main.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return main.size();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package com.keylesspalace.tusky.util;
|
||||||
|
|
||||||
|
import android.arch.core.util.Function;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by charlag on 12/07/2017.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public final class ViewDataUtils {
|
||||||
|
@Nullable
|
||||||
|
public static StatusViewData statusToViewData(@Nullable Status status) {
|
||||||
|
if (status == null) return null;
|
||||||
|
Status visibleStatus = status.reblog == null ? status : status.reblog;
|
||||||
|
return new StatusViewData.Builder()
|
||||||
|
.setId(status.id)
|
||||||
|
.setAttachments(status.attachments)
|
||||||
|
.setAvatar(visibleStatus.account.avatar)
|
||||||
|
.setContent(visibleStatus.content)
|
||||||
|
.setCreatedAt(visibleStatus.createdAt)
|
||||||
|
.setFavourited(visibleStatus.favourited)
|
||||||
|
.setReblogged(visibleStatus.reblogged)
|
||||||
|
.setIsExpanded(false)
|
||||||
|
.setIsShowingSensitiveContent(false)
|
||||||
|
.setMentions(visibleStatus.mentions)
|
||||||
|
.setNickname(visibleStatus.account.username)
|
||||||
|
.setRebloggedAvatar(visibleStatus.account.avatar)
|
||||||
|
.setSensitive(visibleStatus.sensitive)
|
||||||
|
.setSpoilerText(visibleStatus.spoilerText)
|
||||||
|
.setRebloggedByUsername(status.reblog == null ? null : status.account.username)
|
||||||
|
.setUserFullName(visibleStatus.account.getDisplayName())
|
||||||
|
.setSenderId(status.account.id)
|
||||||
|
.setRebloggingEnabled(visibleStatus.rebloggingAllowed())
|
||||||
|
.createStatusViewData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<StatusViewData> statusListToViewDataList(List<Status> statuses) {
|
||||||
|
List<StatusViewData> viewDatas = new ArrayList<>(statuses.size());
|
||||||
|
for (Status s : statuses) {
|
||||||
|
viewDatas.add(statusToViewData(s));
|
||||||
|
}
|
||||||
|
return viewDatas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Function<Status, StatusViewData> statusMapper() {
|
||||||
|
return statusMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NotificationViewData notificationToViewData(Notification notification) {
|
||||||
|
return new NotificationViewData(notification.type, notification.id, notification.account,
|
||||||
|
statusToViewData(notification.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<NotificationViewData>
|
||||||
|
notificationListToViewDataList(List<Notification> notifications) {
|
||||||
|
List<NotificationViewData> viewDatas = new ArrayList<>(notifications.size());
|
||||||
|
for (Notification n : notifications) {
|
||||||
|
viewDatas.add(notificationToViewData(n));
|
||||||
|
}
|
||||||
|
return viewDatas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Function<Status, StatusViewData> statusMapper =
|
||||||
|
new Function<Status, StatusViewData>() {
|
||||||
|
@Override
|
||||||
|
public StatusViewData apply(Status input) {
|
||||||
|
return ViewDataUtils.statusToViewData(input);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.keylesspalace.tusky.viewdata;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by charlag on 12/07/2017.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public final class NotificationViewData {
|
||||||
|
private final Notification.Type type;
|
||||||
|
private final String id;
|
||||||
|
private final Account account;
|
||||||
|
private final StatusViewData statusViewData;
|
||||||
|
|
||||||
|
public NotificationViewData(Notification.Type type, String id, Account account,
|
||||||
|
StatusViewData statusViewData) {
|
||||||
|
this.type = type;
|
||||||
|
this.id = id;
|
||||||
|
this.account = account;
|
||||||
|
this.statusViewData = statusViewData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Notification.Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account getAccount() {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatusViewData getStatusViewData() {
|
||||||
|
return statusViewData;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,296 @@
|
||||||
|
package com.keylesspalace.tusky.viewdata;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.Spanned;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by charlag on 11/07/2017.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public final class StatusViewData {
|
||||||
|
private final String id;
|
||||||
|
private final Spanned content;
|
||||||
|
private final boolean reblogged;
|
||||||
|
private final boolean favourited;
|
||||||
|
@Nullable
|
||||||
|
private final String spoilerText;
|
||||||
|
private final Status.Visibility visibility;
|
||||||
|
private final Status.MediaAttachment[] attachments;
|
||||||
|
@Nullable
|
||||||
|
private final String rebloggedByUsername;
|
||||||
|
@Nullable
|
||||||
|
private final String rebloggedAvatar;
|
||||||
|
private final boolean isSensitive;
|
||||||
|
private final boolean isExpanded;
|
||||||
|
private final boolean isShowingSensitiveContent;
|
||||||
|
private final String userFullName;
|
||||||
|
private final String nickname;
|
||||||
|
private final String avatar;
|
||||||
|
private final Date createdAt;
|
||||||
|
// I would rather have something else but it would be too much of a rewrite
|
||||||
|
@Nullable
|
||||||
|
private final Status.Mention[] mentions;
|
||||||
|
private final String senderId;
|
||||||
|
private final boolean rebloggingEnabled;
|
||||||
|
|
||||||
|
public StatusViewData(String id, Spanned contnet, boolean reblogged, boolean favourited,
|
||||||
|
String spoilerText, Status.Visibility visibility,
|
||||||
|
Status.MediaAttachment[] attachments, String rebloggedByUsername,
|
||||||
|
String rebloggedAvatar, boolean sensitive, boolean isExpanded,
|
||||||
|
boolean isShowingSensitiveWarning, String userFullName, String nickname,
|
||||||
|
String avatar, Date createdAt, Status.Mention[] mentions,
|
||||||
|
String senderId, boolean rebloggingEnabled) {
|
||||||
|
this.id = id;
|
||||||
|
this.content = contnet;
|
||||||
|
this.reblogged = reblogged;
|
||||||
|
this.favourited = favourited;
|
||||||
|
this.spoilerText = spoilerText;
|
||||||
|
this.visibility = visibility;
|
||||||
|
this.attachments = attachments;
|
||||||
|
this.rebloggedByUsername = rebloggedByUsername;
|
||||||
|
this.rebloggedAvatar = rebloggedAvatar;
|
||||||
|
this.isSensitive = sensitive;
|
||||||
|
this.isExpanded = isExpanded;
|
||||||
|
this.isShowingSensitiveContent = isShowingSensitiveWarning;
|
||||||
|
this.userFullName = userFullName;
|
||||||
|
this.nickname = nickname;
|
||||||
|
this.avatar = avatar;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.mentions = mentions;
|
||||||
|
this.senderId = senderId;
|
||||||
|
this.rebloggingEnabled = rebloggingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Spanned getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReblogged() {
|
||||||
|
return reblogged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFavourited() {
|
||||||
|
return favourited;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getSpoilerText() {
|
||||||
|
return spoilerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Status.Visibility getVisibility() {
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Status.MediaAttachment[] getAttachments() {
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getRebloggedByUsername() {
|
||||||
|
return rebloggedByUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSensitive() {
|
||||||
|
return isSensitive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpanded() {
|
||||||
|
return isExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShowingSensitiveContent() {
|
||||||
|
return isShowingSensitiveContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getRebloggedAvatar() {
|
||||||
|
return rebloggedAvatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserFullName() {
|
||||||
|
return userFullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNickname() {
|
||||||
|
return nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSenderId() {
|
||||||
|
return senderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getRebloggingEnabled() {
|
||||||
|
return rebloggingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Status.Mention[] getMentions() {
|
||||||
|
return mentions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private String id;
|
||||||
|
private Spanned contnet;
|
||||||
|
private boolean reblogged;
|
||||||
|
private boolean favourited;
|
||||||
|
private String spoilerText;
|
||||||
|
private Status.Visibility visibility;
|
||||||
|
private Status.MediaAttachment[] attachments;
|
||||||
|
private String rebloggedByUsername;
|
||||||
|
private String rebloggedAvatar;
|
||||||
|
private boolean isSensitive;
|
||||||
|
private boolean isExpanded;
|
||||||
|
private boolean isShowingSensitiveContent;
|
||||||
|
private String userFullName;
|
||||||
|
private String nickname;
|
||||||
|
private String avatar;
|
||||||
|
private Date createdAt;
|
||||||
|
private Status.Mention[] mentions;
|
||||||
|
private String senderId;
|
||||||
|
private boolean rebloggingEnabled;
|
||||||
|
|
||||||
|
public Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder(final StatusViewData viewData) {
|
||||||
|
id = viewData.id;
|
||||||
|
contnet = viewData.content;
|
||||||
|
reblogged = viewData.reblogged;
|
||||||
|
favourited = viewData.favourited;
|
||||||
|
spoilerText = viewData.spoilerText;
|
||||||
|
visibility = viewData.visibility;
|
||||||
|
attachments = viewData.attachments == null ? null : viewData.attachments.clone();
|
||||||
|
rebloggedByUsername = viewData.rebloggedByUsername;
|
||||||
|
rebloggedAvatar = viewData.rebloggedAvatar;
|
||||||
|
isSensitive = viewData.isSensitive;
|
||||||
|
isExpanded = viewData.isExpanded;
|
||||||
|
isShowingSensitiveContent = viewData.isShowingSensitiveContent;
|
||||||
|
userFullName = viewData.userFullName;
|
||||||
|
nickname = viewData.nickname;
|
||||||
|
avatar = viewData.avatar;
|
||||||
|
createdAt = new Date(viewData.createdAt.getTime());
|
||||||
|
mentions = viewData.mentions == null ? null : viewData.mentions.clone();
|
||||||
|
senderId = viewData.senderId;
|
||||||
|
rebloggingEnabled = viewData.rebloggingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setContent(Spanned content) {
|
||||||
|
this.contnet = content;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setReblogged(boolean reblogged) {
|
||||||
|
this.reblogged = reblogged;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setFavourited(boolean favourited) {
|
||||||
|
this.favourited = favourited;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setSpoilerText(String spoilerText) {
|
||||||
|
this.spoilerText = spoilerText;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setVisibility(Status.Visibility visibility) {
|
||||||
|
this.visibility = visibility;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setAttachments(Status.MediaAttachment[] attachments) {
|
||||||
|
this.attachments = attachments;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setRebloggedByUsername(String rebloggedByUsername) {
|
||||||
|
this.rebloggedByUsername = rebloggedByUsername;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setRebloggedAvatar(String rebloggedAvatar) {
|
||||||
|
this.rebloggedAvatar = rebloggedAvatar;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setSensitive(boolean sensitive) {
|
||||||
|
this.isSensitive = sensitive;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setIsExpanded(boolean isExpanded) {
|
||||||
|
this.isExpanded = isExpanded;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setIsShowingSensitiveContent(boolean isShowingSensitiveContent) {
|
||||||
|
this.isShowingSensitiveContent = isShowingSensitiveContent;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setUserFullName(String userFullName) {
|
||||||
|
this.userFullName = userFullName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setNickname(String nickname) {
|
||||||
|
this.nickname = nickname;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setAvatar(String avatar) {
|
||||||
|
this.avatar = avatar;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setCreatedAt(Date createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setMentions(Status.Mention[] mentions) {
|
||||||
|
this.mentions = mentions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setSenderId(String senderId) {
|
||||||
|
this.senderId = senderId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setRebloggingEnabled(boolean rebloggingEnabled) {
|
||||||
|
this.rebloggingEnabled = rebloggingEnabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatusViewData createStatusViewData() {
|
||||||
|
return new StatusViewData(id, contnet, reblogged, favourited, spoilerText, visibility,
|
||||||
|
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
|
||||||
|
isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, mentions,
|
||||||
|
senderId, rebloggingEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -297,7 +297,8 @@
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:padding="4dp"
|
android:padding="4dp"
|
||||||
android:contentDescription="@string/action_reblog" />
|
android:contentDescription="@string/action_reblog"
|
||||||
|
android:clipToPadding="false"/>
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -315,7 +316,8 @@
|
||||||
app:sparkbutton_secondaryColor="@color/status_favourite_button_marked_light"
|
app:sparkbutton_secondaryColor="@color/status_favourite_button_marked_light"
|
||||||
android:id="@+id/status_favourite"
|
android:id="@+id/status_favourite"
|
||||||
android:padding="4dp"
|
android:padding="4dp"
|
||||||
android:contentDescription="@string/action_favourite" />
|
android:contentDescription="@string/action_favourite"
|
||||||
|
android:clipToPadding="false"/>
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
Loading…
Reference in New Issue