From 74d6736afc1821ad68f4ea393586defa341a61de Mon Sep 17 00:00:00 2001 From: charlag Date: Mon, 6 Nov 2017 18:19:15 +0300 Subject: [PATCH] Make status placeholder abstraction cleaner --- .../tusky/adapter/NotificationsAdapter.java | 5 +- .../tusky/adapter/StatusBaseViewHolder.java | 2 +- .../adapter/StatusDetailedViewHolder.java | 2 +- .../tusky/adapter/StatusViewHolder.java | 2 +- .../tusky/adapter/ThreadAdapter.java | 17 +- .../tusky/adapter/TimelineAdapter.java | 10 +- .../keylesspalace/tusky/entity/Status.java | 10 +- .../tusky/fragment/NotificationsFragment.java | 15 +- .../tusky/fragment/TimelineFragment.java | 177 +++++--- .../tusky/fragment/ViewThreadFragment.java | 54 +-- .../tusky/util/ViewDataUtils.java | 17 +- .../view/ConversationLineItemDecoration.java | 6 +- .../tusky/viewdata/NotificationViewData.java | 6 +- .../tusky/viewdata/StatusViewData.java | 390 +++++++++--------- 14 files changed, 387 insertions(+), 326 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index dfbb8e211..619e6bd1e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -113,7 +113,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { switch (type) { case MENTION: { StatusViewHolder holder = (StatusViewHolder) viewHolder; - StatusViewData status = concreteNotificaton.getStatusViewData(); + StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); holder.setupWithStatus(status, statusListener, mediaPreviewEnabled); break; @@ -279,7 +279,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { notificationAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY); } - void setMessage(Notification.Type type, String displayName, StatusViewData status) { + void setMessage(Notification.Type type, String displayName, + StatusViewData.Concrete status) { Context context = message.getContext(); String format; switch (type) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index ef5e1287e..2bb0594b3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -473,7 +473,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { container.setOnClickListener(viewThreadListener); } - void setupWithStatus(StatusViewData status, final StatusActionListener listener, + void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, boolean mediaPreviewEnabled) { setDisplayName(status.getUserFullName()); setUsername(status.getNickname()); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 4098e325e..8461d7760 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -85,7 +85,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { } @Override - void setupWithStatus(final StatusViewData status, final StatusActionListener listener, + void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener, boolean mediaPreviewEnabled) { super.setupWithStatus(status, listener, mediaPreviewEnabled); reblogs.setText(status.getReblogsCount()); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 7992eeb50..943e7d729 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -67,7 +67,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { } @Override - void setupWithStatus(StatusViewData status, final StatusActionListener listener, + void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, boolean mediaPreviewEnabled) { super.setupWithStatus(status, listener, mediaPreviewEnabled); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java index 464817922..c743a54b4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java @@ -33,7 +33,7 @@ public class ThreadAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_STATUS = 0; private static final int VIEW_TYPE_STATUS_DETAILED = 1; - private List statuses; + private List statuses; private StatusActionListener statusActionListener; private boolean mediaPreviewEnabled; private int detailedStatusPosition; @@ -66,13 +66,12 @@ public class ThreadAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + StatusViewData.Concrete status = statuses.get(position); if (position == detailedStatusPosition) { StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder; - StatusViewData status = statuses.get(position); holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled); } else { StatusViewHolder holder = (StatusViewHolder) viewHolder; - StatusViewData status = statuses.get(position); holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled); } } @@ -91,13 +90,13 @@ public class ThreadAdapter extends RecyclerView.Adapter { return statuses.size(); } - public void setStatuses(List statuses) { + public void setStatuses(List statuses) { this.statuses.clear(); this.statuses.addAll(statuses); notifyDataSetChanged(); } - public void addItem(int position, StatusViewData statusViewData) { + public void addItem(int position, StatusViewData.Concrete statusViewData) { statuses.add(position, statusViewData); notifyItemInserted(position); } @@ -109,12 +108,12 @@ public class ThreadAdapter extends RecyclerView.Adapter { notifyItemRangeRemoved(0, oldSize); } - public void addAll(int position, List statuses) { + public void addAll(int position, List statuses) { this.statuses.addAll(position, statuses); notifyItemRangeInserted(position, statuses.size()); } - public void addAll(List statuses) { + public void addAll(List statuses) { int end = statuses.size(); this.statuses.addAll(statuses); notifyItemRangeInserted(end, statuses.size()); @@ -126,7 +125,7 @@ public class ThreadAdapter extends RecyclerView.Adapter { notifyDataSetChanged(); } - public void setItem(int position, StatusViewData status, boolean notifyAdapter) { + public void setItem(int position, StatusViewData.Concrete status, boolean notifyAdapter) { statuses.set(position, status); if (notifyAdapter) { notifyItemChanged(position); @@ -134,7 +133,7 @@ public class ThreadAdapter extends RecyclerView.Adapter { } @Nullable - public StatusViewData getItem(int position) { + public StatusViewData.Concrete getItem(int position) { if (position != RecyclerView.NO_POSITION && position >= 0 && position < statuses.size()) { return statuses.get(position); } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java index 2be75fa16..1041551da 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -72,12 +72,14 @@ public class TimelineAdapter extends RecyclerView.Adapter { public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if (position < statuses.size()) { StatusViewData status = statuses.get(position); - if(status.isPlaceholder()) { + if (status instanceof StatusViewData.Placeholder) { PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; - holder.setup(!status.isPlaceholderLoading(), statusListener); + holder.setup(!((StatusViewData.Placeholder) status).isLoading(), statusListener); } else { + StatusViewHolder holder = (StatusViewHolder) viewHolder; - holder.setupWithStatus(status, statusListener, mediaPreviewEnabled); + holder.setupWithStatus((StatusViewData.Concrete) status, + statusListener, mediaPreviewEnabled); } } else { @@ -96,7 +98,7 @@ public class TimelineAdapter extends RecyclerView.Adapter { if (position == statuses.size()) { return VIEW_TYPE_FOOTER; } else { - if(statuses.get(position).isPlaceholder()) { + if (statuses.get(position) instanceof StatusViewData.Placeholder) { return VIEW_TYPE_PLACEHOLDER; } else { return VIEW_TYPE_STATUS; diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java index d5d941f99..6bc81c593 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java @@ -27,10 +27,6 @@ import java.util.Date; import java.util.List; public class Status { - /*if placeholder == true, this is not a real status, but a placeholder "load more" - and the id represents the max_id for the request*/ - public boolean placeholder; - public String url; @SerializedName("reblogs_count") @@ -115,16 +111,12 @@ public class Status { if (o == null || getClass() != o.getClass()) return false; Status status = (Status) o; - - if (placeholder != status.placeholder) return false; return id != null ? id.equals(status.id) : status.id == null; } @Override public int hashCode() { - int result = (placeholder ? 1 : 0); - result = 31 * result + (id != null ? id.hashCode() : 0); - return result; + return id != null ? id.hashCode() : 0; } public static class MediaAttachment { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 0755f4023..9a0be9253 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -340,7 +340,7 @@ public class NotificationsFragment extends SFragment implements public void onExpandedChange(boolean expanded, int position) { NotificationViewData.Concrete old = (NotificationViewData.Concrete) notifications.getPairedItem(position); - StatusViewData statusViewData = + StatusViewData.Concrete statusViewData = new StatusViewData.Builder(old.getStatusViewData()) .setIsExpanded(expanded) .createStatusViewData(); @@ -354,7 +354,7 @@ public class NotificationsFragment extends SFragment implements public void onContentHiddenChange(boolean isShowing, int position) { NotificationViewData.Concrete old = (NotificationViewData.Concrete) notifications.getPairedItem(position); - StatusViewData statusViewData = + StatusViewData.Concrete statusViewData = new StatusViewData.Builder(old.getStatusViewData()) .setIsShowingSensitiveContent(isShowing) .createStatusViewData(); @@ -368,10 +368,13 @@ public class NotificationsFragment extends SFragment implements public void onLoadMore(int position) { //check bounds before accessing list, if (notifications.size() >= position && position > 0) { - // is it safe? - String fromId = notifications.get(position - 1).getAsRight().id; - String toId = notifications.get(position + 1).getAsRight().id; - sendFetchNotificationsRequest(fromId, toId, FetchEnd.MIDDLE, position); + Notification previous = notifications.get(position - 1).getAsRightOrNull(); + Notification next = notifications.get(position + 1).getAsRightOrNull(); + if (previous == null || next == null) { + Log.e(TAG, "Failed to load more, invalid placeholder position: " + position); + return; + } + sendFetchNotificationsRequest(previous.id, next.id, FetchEnd.MIDDLE, position); NotificationViewData notificationViewData = new NotificationViewData.Placeholder(true); notifications.setPairedItem(position, notificationViewData); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index a2f4acce1..f04d8a5f4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -15,6 +15,7 @@ package com.keylesspalace.tusky.fragment; +import android.arch.core.util.Function; import android.content.Context; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; @@ -25,6 +26,7 @@ import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.TabLayout; import android.support.v4.content.LocalBroadcastManager; +import android.support.v4.util.Pair; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; @@ -43,6 +45,8 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.receiver.TimelineReceiver; +import com.keylesspalace.tusky.util.CollectionUtil; +import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.HttpHeaderLink; import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.PairedList; @@ -104,8 +108,18 @@ public class TimelineFragment extends SFragment implements private String bottomId; @Nullable private String topId; - private PairedList statuses = - new PairedList<>(ViewDataUtils.statusMapper()); + private PairedList, StatusViewData> statuses = + new PairedList<>(new Function, StatusViewData>() { + @Override + public StatusViewData apply(Either input) { + Status status = input.getAsRightOrNull(); + if (status != null) { + return ViewDataUtils.statusToViewData(status); + } else { + return new StatusViewData.Placeholder(false); + } + } + }); public static TimelineFragment newInstance(Kind kind) { TimelineFragment fragment = new TimelineFragment(); @@ -124,6 +138,17 @@ public class TimelineFragment extends SFragment implements return fragment; } + private static final class Placeholder { + private final static Placeholder INSTANCE = new Placeholder(); + + public static Placeholder getInstance() { + return INSTANCE; + } + + private Placeholder() { + } + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -256,12 +281,12 @@ public class TimelineFragment extends SFragment implements @Override public void onReply(int position) { - super.reply(statuses.get(position)); + super.reply(statuses.get(position).getAsRight()); } @Override public void onReblog(final boolean reblog, final int position) { - final Status status = statuses.get(position); + final Status status = statuses.get(position).getAsRight(); super.reblogWithCallback(status, reblog, new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { @@ -271,12 +296,17 @@ public class TimelineFragment extends SFragment implements if (status.reblog != null) { status.reblog.reblogged = reblog; } + + Pair actual = + findStatusAndPosition(position, status); + if (actual == null) return; + StatusViewData newViewData = - new StatusViewData.Builder(statuses.getPairedItem(position)) + new StatusViewData.Builder(actual.first) .setReblogged(reblog) .createStatusViewData(); - statuses.setPairedItem(position, newViewData); - adapter.changeItem(position, newViewData, true); + statuses.setPairedItem(actual.second, newViewData); + adapter.changeItem(actual.second, newViewData, true); } } @@ -289,7 +319,7 @@ public class TimelineFragment extends SFragment implements @Override public void onFavourite(final boolean favourite, final int position) { - final Status status = statuses.get(position); + final Status status = statuses.get(position).getAsRight(); super.favouriteWithCallback(status, favourite, new Callback() { @Override @@ -300,12 +330,17 @@ public class TimelineFragment extends SFragment implements if (status.reblog != null) { status.reblog.favourited = favourite; } + + Pair actual = + findStatusAndPosition(position, status); + if (actual == null) return; + StatusViewData newViewData = new StatusViewData - .Builder(statuses.getPairedItem(position)) + .Builder(actual.first) .setFavourited(favourite) .createStatusViewData(); - statuses.setPairedItem(position, newViewData); - adapter.changeItem(position, newViewData, true); + statuses.setPairedItem(actual.second, newViewData); + adapter.changeItem(actual.second, newViewData, true); } } @@ -318,17 +353,18 @@ public class TimelineFragment extends SFragment implements @Override public void onMore(View view, final int position) { - super.more(statuses.get(position), view, position); + super.more(statuses.get(position).getAsRight(), view, position); } @Override public void onOpenReblog(int position) { - super.openReblog(statuses.get(position)); + super.openReblog(statuses.get(position).getAsRight()); } @Override public void onExpandedChange(boolean expanded, int position) { - StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position)) + StatusViewData newViewData = new StatusViewData.Builder( + ((StatusViewData.Concrete) statuses.getPairedItem(position))) .setIsExpanded(expanded).createStatusViewData(); statuses.setPairedItem(position, newViewData); adapter.changeItem(position, newViewData, false); @@ -336,7 +372,8 @@ public class TimelineFragment extends SFragment implements @Override public void onContentHiddenChange(boolean isShowing, int position) { - StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position)) + StatusViewData newViewData = new StatusViewData.Builder( + ((StatusViewData.Concrete) statuses.getPairedItem(position))) .setIsShowingSensitiveContent(isShowing).createStatusViewData(); statuses.setPairedItem(position, newViewData); adapter.changeItem(position, newViewData, false); @@ -346,17 +383,19 @@ public class TimelineFragment extends SFragment implements public void onLoadMore(int position) { //check bounds before accessing list, if (statuses.size() >= position && position > 0) { - String fromId = statuses.get(position - 1).id; - String toId = statuses.get(position + 1).id; - sendFetchTimelineRequest(fromId, toId, FetchEnd.MIDDLE, position); + Status fromStatus = statuses.get(position - 1).getAsRightOrNull(); + Status toStatus = statuses.get(position + 1).getAsRightOrNull(); + if (fromStatus == null || toStatus == null) { + Log.e(TAG, "Failed to load more at " + position + ", wrong placeholder position"); + return; + } + sendFetchTimelineRequest(fromStatus.id, toStatus.id, FetchEnd.MIDDLE, position); - StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position)) - .setPlaceholderLoading(true).createStatusViewData(); + StatusViewData newViewData = new StatusViewData.Placeholder(true); statuses.setPairedItem(position, newViewData); adapter.changeItem(position, newViewData, false); - } else { - Log.d(TAG, "error loading more"); + Log.e(TAG, "error loading more"); } } @@ -368,7 +407,7 @@ public class TimelineFragment extends SFragment implements @Override public void onViewThread(int position) { - super.viewThread(statuses.get(position)); + super.viewThread(statuses.get(position).getAsRight()); } @Override @@ -433,10 +472,10 @@ public class TimelineFragment extends SFragment implements @Override public void removeAllByAccountId(String accountId) { // using iterator to safely remove items while iterating - Iterator iterator = statuses.iterator(); + Iterator> iterator = statuses.iterator(); while (iterator.hasNext()) { - Status status = iterator.next(); - if (status.account.id.equals(accountId)) { + Status status = iterator.next().getAsRightOrNull(); + if (status != null && status.account.id.equals(accountId)) { iterator.remove(); } } @@ -534,6 +573,8 @@ public class TimelineFragment extends SFragment implements private void onFetchTimelineSuccess(List statuses, String linkHeader, FetchEnd fetchEnd, int pos) { + // We filled the hole (or reached the end) if the server returned less statuses than we + // we asked for. boolean fullFetch = statuses.size() >= LOAD_AT_ONCE; filterStatuses(statuses); List links = HttpHeaderLink.parse(linkHeader); @@ -548,7 +589,7 @@ public class TimelineFragment extends SFragment implements break; } case MIDDLE: { - insertStatuses(statuses,fullFetch, pos); + replacePlaceholderWithStatuses(statuses, fullFetch, pos); break; } case BOTTOM: { @@ -585,10 +626,8 @@ public class TimelineFragment extends SFragment implements private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) { swipeRefreshLayout.setRefreshing(false); - if(fetchEnd == FetchEnd.MIDDLE && statuses.getPairedItem(position).isPlaceholder()) { - - StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position)) - .setPlaceholderLoading(false).createStatusViewData(); + if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) { + StatusViewData newViewData = new StatusViewData.Placeholder(true); statuses.setPairedItem(position, newViewData); adapter.changeItem(position, newViewData, true); } @@ -640,25 +679,26 @@ public class TimelineFragment extends SFragment implements if (toId != null) { topId = toId; } + + List> liftedNew = listStatusList(newStatuses); + if (statuses.isEmpty()) { - statuses.addAll(newStatuses); + statuses.addAll(liftedNew); } else { - Status lastOfNew = newStatuses.get(newStatuses.size() - 1); + Either lastOfNew = liftedNew.get(newStatuses.size() - 1); int index = statuses.indexOf(lastOfNew); for (int i = 0; i < index; i++) { statuses.remove(0); } - int newIndex = newStatuses.indexOf(statuses.get(0)); + int newIndex = liftedNew.indexOf(statuses.get(0)); if (newIndex == -1) { - if(index == -1 && fullFetch) { - Status placeholder = new Status(); - placeholder.placeholder = true; - newStatuses.add(placeholder); + if (index == -1 && fullFetch) { + liftedNew.add(Either.left(Placeholder.getInstance())); } - statuses.addAll(0, newStatuses); + statuses.addAll(0, liftedNew); } else { - statuses.addAll(0, newStatuses.subList(0, newIndex)); + statuses.addAll(0, liftedNew.subList(0, newIndex)); } } adapter.update(statuses.getPairedCopy()); @@ -669,9 +709,11 @@ public class TimelineFragment extends SFragment implements return; } int end = statuses.size(); - Status last = statuses.get(end - 1); + Status last = statuses.get(end - 1).getAsRightOrNull(); + // I was about to replace findStatus with indexOf but it is incorrect to compare value + // types by ID anyway and we should change equals() for Status, I think, so this makes sense if (last != null && !findStatus(newStatuses, last.id)) { - statuses.addAll(newStatuses); + statuses.addAll(listStatusList(newStatuses)); List newViewDatas = statuses.getPairedCopy() .subList(statuses.size() - newStatuses.size(), statuses.size()); if (BuildConfig.DEBUG && newStatuses.size() != newViewDatas.size()) { @@ -688,9 +730,9 @@ public class TimelineFragment extends SFragment implements } } - private void insertStatuses(List newStatuses, boolean fullFetch, int pos) { - - if(statuses.get(pos).placeholder) { + private void replacePlaceholderWithStatuses(List newStatuses, boolean fullFetch, int pos) { + Status status = statuses.get(pos).getAsRightOrNull(); + if (status == null) { statuses.remove(pos); } @@ -699,13 +741,13 @@ public class TimelineFragment extends SFragment implements return; } - if(fullFetch) { - Status placeholder = new Status(); - placeholder.placeholder = true; - newStatuses.add(placeholder); + List> liftedNew = listStatusList(newStatuses); + + if (fullFetch) { + liftedNew.add(Either.left(Placeholder.getInstance())); } - statuses.addAll(pos, newStatuses); + statuses.addAll(pos, liftedNew); adapter.update(statuses.getPairedCopy()); } @@ -718,4 +760,39 @@ public class TimelineFragment extends SFragment implements } return false; } + + private final Function> statusLifter = + new Function>() { + @Override + public Either apply(Status input) { + return Either.right(input); + } + }; + + private @Nullable + Pair + findStatusAndPosition(int position, Status status) { + StatusViewData.Concrete statusToUpdate; + int positionToUpdate; + StatusViewData someOldViewData = statuses.getPairedItem(position); + + // Unlikely, but data could change between the request and response + if ((someOldViewData instanceof StatusViewData.Placeholder) || + !((StatusViewData.Concrete) someOldViewData).getId().equals(status.id)) { + // try to find the status we need to update + int foundPos = statuses.indexOf(Either.right(status)); + if (foundPos < 0) return null; // okay, it's hopeless, give up + statusToUpdate = ((StatusViewData.Concrete) + statuses.getPairedItem(foundPos)); + positionToUpdate = position; + } else { + statusToUpdate = (StatusViewData.Concrete) someOldViewData; + positionToUpdate = position; + } + return new Pair<>(statusToUpdate, positionToUpdate); + } + + private List> listStatusList(List list) { + return CollectionUtil.map(list, statusLifter); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index ccef1d573..aedf6dd05 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -69,7 +69,7 @@ public class ViewThreadFragment extends SFragment implements private int statusIndex = 0; - private final PairedList statuses = + private final PairedList statuses = new PairedList<>(ViewDataUtils.statusMapper()); public static ViewThreadFragment newInstance(String id) { @@ -83,7 +83,7 @@ public class ViewThreadFragment extends SFragment implements @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { + @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false); Context context = getContext(); @@ -227,18 +227,20 @@ public class ViewThreadFragment extends SFragment implements @Override public void onExpandedChange(boolean expanded, int position) { - StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position)) - .setIsExpanded(expanded) - .createStatusViewData(); + StatusViewData.Concrete 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(); + StatusViewData.Concrete newViewData = + new StatusViewData.Builder(statuses.getPairedItem(position)) + .setIsShowingSensitiveContent(isShowing) + .createStatusViewData(); statuses.setPairedItem(position, newViewData); adapter.setItem(position, newViewData, false); } @@ -260,7 +262,7 @@ public class ViewThreadFragment extends SFragment implements @Override public void removeItem(int position) { - if(position == statusIndex) { + if (position == statusIndex) { //the status got removed, close the activity getActivity().finish(); } @@ -283,7 +285,7 @@ public class ViewThreadFragment extends SFragment implements } } statusIndex = statuses.indexOf(status); - if(statusIndex == -1) { + if (statusIndex == -1) { //the status got removed, close the activity getActivity().finish(); return; @@ -384,8 +386,8 @@ public class ViewThreadFragment extends SFragment implements int i = statusIndex; statuses.add(i, status); adapter.setDetailedStatusPosition(i); - StatusViewData viewData = statuses.getPairedItem(i); - if(viewData.getCard() == null && card != null) { + StatusViewData.Concrete viewData = statuses.getPairedItem(i); + if (viewData.getCard() == null && card != null) { viewData = new StatusViewData.Builder(viewData) .setCard(card) .createStatusViewData(); @@ -410,7 +412,7 @@ public class ViewThreadFragment extends SFragment implements statusIndex = ancestors.size(); adapter.setDetailedStatusPosition(statusIndex); statuses.addAll(0, ancestors); - List ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex); + List ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex); if (BuildConfig.DEBUG && ancestors.size() != ancestorsViewDatas.size()) { String error = String.format(Locale.getDefault(), "Incorrectly got statusViewData sublist." + @@ -425,8 +427,8 @@ public class ViewThreadFragment extends SFragment implements // In case we needed to delete everything (which is way easier than deleting // everything except one), re-insert the remaining status here. statuses.add(statusIndex, mainStatus); - StatusViewData viewData = statuses.getPairedItem(statusIndex); - if(viewData.getCard() == null && card != null) { + StatusViewData.Concrete viewData = statuses.getPairedItem(statusIndex); + if (viewData.getCard() == null && card != null) { viewData = new StatusViewData.Builder(viewData) .setCard(card) .createStatusViewData(); @@ -436,9 +438,9 @@ public class ViewThreadFragment extends SFragment implements // Insert newly fetched descendants statuses.addAll(descendants); - List descendantsViewData; - descendantsViewData = statuses.getPairedCopy() - .subList(statuses.size() - descendants.size(), statuses.size()); + List 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." + @@ -452,16 +454,14 @@ public class ViewThreadFragment extends SFragment implements private void showCard(Card card) { this.card = card; - if(statuses.size() != 0) { - StatusViewData oldViewData = statuses.getPairedItem(statusIndex); - if(oldViewData != null) { - StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(statusIndex)) - .setCard(card) - .createStatusViewData(); + if (statuses.size() != 0) { + StatusViewData.Concrete newViewData = + new StatusViewData.Builder(statuses.getPairedItem(statusIndex)) + .setCard(card) + .createStatusViewData(); - statuses.setPairedItem(statusIndex, newViewData); - adapter.setItem(statusIndex, newViewData, true); - } + statuses.setPairedItem(statusIndex, newViewData); + adapter.setItem(statusIndex, newViewData, true); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index 8812ed0c7..502d558c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -32,13 +32,8 @@ import java.util.List; public final class ViewDataUtils { @Nullable - public static StatusViewData statusToViewData(@Nullable Status status) { + public static StatusViewData.Concrete statusToViewData(@Nullable Status status) { if (status == null) return null; - if (status.placeholder) { - return new StatusViewData.Builder().setId(status.id) - .setPlaceholder(true) - .createStatusViewData(); - } Status visibleStatus = status.reblog == null ? status : status.reblog; return new StatusViewData.Builder().setId(status.id) .setAttachments(visibleStatus.attachments) @@ -75,11 +70,11 @@ public final class ViewDataUtils { return viewDatas; } - public static Function statusMapper() { + public static Function statusMapper() { return statusMapper; } - public static NotificationViewData notificationToViewData(Notification notification) { + public static NotificationViewData.Concrete notificationToViewData(Notification notification) { return new NotificationViewData.Concrete(notification.type, notification.id, notification.account, statusToViewData(notification.status)); } @@ -93,10 +88,10 @@ public final class ViewDataUtils { return viewDatas; } - private static final Function statusMapper = - new Function() { + private static final Function statusMapper = + new Function() { @Override - public StatusViewData apply(Status input) { + public StatusViewData.Concrete apply(Status input) { return ViewDataUtils.statusToViewData(input); } }; diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.java b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.java index 4159c5879..c90bf5f5c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.java +++ b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.java @@ -49,16 +49,16 @@ public class ConversationLineItemDecoration extends RecyclerView.ItemDecoration int position = parent.getChildAdapterPosition(child); ThreadAdapter adapter = (ThreadAdapter) parent.getAdapter(); - StatusViewData current = adapter.getItem(position); + StatusViewData.Concrete current = adapter.getItem(position); int dividerTop, dividerBottom; if (current != null) { - StatusViewData above = adapter.getItem(position - 1); + StatusViewData.Concrete above = adapter.getItem(position - 1); if (above != null && above.getId().equals(current.getInReplyToId())) { dividerTop = child.getTop(); } else { dividerTop = child.getTop() + avatarMargin; } - StatusViewData below = adapter.getItem(position + 1); + StatusViewData.Concrete below = adapter.getItem(position + 1); if (below != null && current.getId().equals(below.getInReplyToId())) { dividerBottom = child.getBottom(); } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java index 0219f0d6d..d03450174 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java @@ -37,10 +37,10 @@ public abstract class NotificationViewData { private final Notification.Type type; private final String id; private final Account account; - private final StatusViewData statusViewData; + private final StatusViewData.Concrete statusViewData; public Concrete(Notification.Type type, String id, Account account, - StatusViewData statusViewData) { + StatusViewData.Concrete statusViewData) { this.type = type; this.id = id; this.account = account; @@ -59,7 +59,7 @@ public abstract class NotificationViewData { return account; } - public StatusViewData getStatusViewData() { + public StatusViewData.Concrete getStatusViewData() { return statusViewData; } } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java index ed4a415cc..2d91ac365 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -27,195 +27,202 @@ import java.util.List; /** * Created by charlag on 11/07/2017. + * + * Class to represent data required to display either a notification or a placeholder. + * It is either a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}. */ -public final class StatusViewData { - 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; - private final String reblogsCount; - private final String favouritesCount; - @Nullable - private final String inReplyToId; - // 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; - private final Status.Application application; - private final List emojis; - @Nullable - private final Card card; +public abstract class StatusViewData { - private final boolean placeholder; - - private final boolean placeholderLoading; - - public StatusViewData(String id, Spanned content, boolean reblogged, boolean favourited, - @Nullable String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments, - @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, - boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar, - Date createdAt, String reblogsCount, String favouritesCount, @Nullable String inReplyToId, - @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, - Status.Application application, List emojis, @Nullable Card card, - boolean placeholder, boolean placeholderLoading) { - this.id = id; - this.content = content; - 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.reblogsCount = reblogsCount; - this.favouritesCount = favouritesCount; - this.inReplyToId = inReplyToId; - this.mentions = mentions; - this.senderId = senderId; - this.rebloggingEnabled = rebloggingEnabled; - this.application = application; - this.emojis = emojis; - this.card = card; - this.placeholder = placeholder; - this.placeholderLoading = placeholderLoading; + private StatusViewData() { } - public String getId() { - return id; + public static final class Concrete extends 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; + private final String reblogsCount; + private final String favouritesCount; + @Nullable + private final String inReplyToId; + // 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; + private final Status.Application application; + private final List emojis; + @Nullable + private final Card card; + + public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, + @Nullable String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments, + @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, + boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar, + Date createdAt, String reblogsCount, String favouritesCount, @Nullable String inReplyToId, + @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, + Status.Application application, List emojis, @Nullable Card card) { + this.id = id; + this.content = content; + 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.reblogsCount = reblogsCount; + this.favouritesCount = favouritesCount; + this.inReplyToId = inReplyToId; + this.mentions = mentions; + this.senderId = senderId; + this.rebloggingEnabled = rebloggingEnabled; + this.application = application; + this.emojis = emojis; + this.card = card; + } + + 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 getReblogsCount() { + return reblogsCount; + } + + public String getFavouritesCount() { + return favouritesCount; + } + + @Nullable + public String getInReplyToId() { + return inReplyToId; + } + + public String getSenderId() { + return senderId; + } + + public Boolean getRebloggingEnabled() { + return rebloggingEnabled; + } + + @Nullable + public Status.Mention[] getMentions() { + return mentions; + } + + public Status.Application getApplication() { + return application; + } + + public List getEmojis() { + return emojis; + } + + @Nullable + public Card getCard() { + return card; + } + } - public Spanned getContent() { - return content; - } + public static final class Placeholder extends StatusViewData { + private final boolean isLoading; - public boolean isReblogged() { - return reblogged; - } + public Placeholder(boolean isLoading) { + this.isLoading = isLoading; + } - 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 getReblogsCount() { - return reblogsCount; - } - - public String getFavouritesCount() { - return favouritesCount; - } - - @Nullable - public String getInReplyToId() { - return inReplyToId; - } - - public String getSenderId() { - return senderId; - } - - public Boolean getRebloggingEnabled() { - return rebloggingEnabled; - } - - @Nullable - public Status.Mention[] getMentions() { - return mentions; - } - - public Status.Application getApplication() { - return application; - } - - public List getEmojis() { - return emojis; - } - - @Nullable - public Card getCard() { - return card; - } - - public boolean isPlaceholder() { - return placeholder; - } - - public boolean isPlaceholderLoading() { - return placeholderLoading; + public boolean isLoading() { + return isLoading; + } } public static class Builder { @@ -244,13 +251,11 @@ public final class StatusViewData { private Status.Application application; private List emojis; private Card card; - private boolean placeholder; - private boolean placeholderLoading; public Builder() { } - public Builder(final StatusViewData viewData) { + public Builder(final StatusViewData.Concrete viewData) { id = viewData.id; content = viewData.content; reblogged = viewData.reblogged; @@ -276,9 +281,6 @@ public final class StatusViewData { application = viewData.application; emojis = viewData.getEmojis(); card = viewData.getCard(); - placeholder = viewData.isPlaceholder(); - placeholderLoading = viewData.isPlaceholderLoading(); - } public Builder setId(String id) { @@ -406,25 +408,15 @@ public final class StatusViewData { return this; } - public Builder setPlaceholder(boolean placeholder) { - this.placeholder = placeholder; - return this; - } - - public Builder setPlaceholderLoading(boolean placeholderLoading) { - this.placeholderLoading = placeholderLoading; - return this; - } - - public StatusViewData createStatusViewData() { + public StatusViewData.Concrete createStatusViewData() { if (this.emojis == null) emojis = Collections.emptyList(); if (this.createdAt == null) createdAt = new Date(); - return new StatusViewData(id, content, reblogged, favourited, spoilerText, visibility, + return new StatusViewData.Concrete(id, content, reblogged, favourited, spoilerText, visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, reblogsCount, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, - emojis, card, placeholder, placeholderLoading); + emojis, card); } } }