Merge pull request #435 from Vavassor/timeline-improvement

"load more" Placeholder
main
Konrad Pozniak 7 years ago committed by GitHub
commit 55f7c98450
  1. 66
      app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
  2. 49
      app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java
  3. 5
      app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
  4. 2
      app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java
  5. 2
      app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java
  6. 17
      app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java
  7. 24
      app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java
  8. 18
      app/src/main/java/com/keylesspalace/tusky/entity/Status.java
  9. 231
      app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
  10. 13
      app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
  11. 256
      app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
  12. 61
      app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java
  13. 1
      app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java
  14. 38
      app/src/main/java/com/keylesspalace/tusky/util/CollectionUtil.java
  15. 125
      app/src/main/java/com/keylesspalace/tusky/util/Either.java
  16. 29
      app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java
  17. 6
      app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.java
  18. 85
      app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java
  19. 338
      app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java
  20. 8
      app/src/main/res/layout/item_status_placeholder.xml
  21. 1
      app/src/main/res/values/strings.xml

@ -47,6 +47,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_FOOTER = 1;
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
private static final int VIEW_TYPE_FOLLOW = 3;
private static final int VIEW_TYPE_PLACEHOLDER = 4;
private List<NotificationViewData> notifications;
private StatusActionListener statusListener;
@ -88,6 +89,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.inflate(R.layout.item_follow, parent, false);
return new FollowViewHolder(view);
}
case VIEW_TYPE_PLACEHOLDER: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_status_placeholder, parent, false);
return new PlaceholderViewHolder(view);
}
}
}
@ -95,11 +101,19 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < notifications.size()) {
NotificationViewData notification = notifications.get(position);
Notification.Type type = notification.getType();
if (notification instanceof NotificationViewData.Placeholder) {
NotificationViewData.Placeholder placeholder = ((NotificationViewData.Placeholder) notification);
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
holder.setup(!placeholder.isLoading(), statusListener);
return;
}
NotificationViewData.Concrete concreteNotificaton =
(NotificationViewData.Concrete) notification;
Notification.Type type = concreteNotificaton.getType();
switch (type) {
case MENTION: {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
StatusViewData status = notification.getStatusViewData();
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
holder.setupWithStatus(status,
statusListener, mediaPreviewEnabled);
break;
@ -107,18 +121,18 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case FAVOURITE:
case REBLOG: {
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
holder.setMessage(type, notification.getAccount().getDisplayName(),
notification.getStatusViewData());
holder.setupButtons(notificationActionListener, notification.getAccount().id);
holder.setAvatars(notification.getStatusViewData().getAvatar(),
notification.getAccount().avatar);
holder.setMessage(type, concreteNotificaton.getAccount().getDisplayName(),
concreteNotificaton.getStatusViewData());
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().id);
holder.setAvatars(concreteNotificaton.getStatusViewData().getAvatar(),
concreteNotificaton.getAccount().avatar);
break;
}
case FOLLOW: {
FollowViewHolder holder = (FollowViewHolder) viewHolder;
holder.setMessage(notification.getAccount().getDisplayName(),
notification.getAccount().username, notification.getAccount().avatar);
holder.setupButtons(notificationActionListener, notification.getAccount().id);
holder.setMessage(concreteNotificaton.getAccount().getDisplayName(),
concreteNotificaton.getAccount().username, concreteNotificaton.getAccount().avatar);
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().id);
break;
}
}
@ -139,18 +153,25 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
return VIEW_TYPE_FOOTER;
} else {
NotificationViewData notification = notifications.get(position);
switch (notification.getType()) {
default:
case MENTION: {
return VIEW_TYPE_MENTION;
}
case FAVOURITE:
case REBLOG: {
return VIEW_TYPE_STATUS_NOTIFICATION;
}
case FOLLOW: {
return VIEW_TYPE_FOLLOW;
if (notification instanceof NotificationViewData.Concrete) {
NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification);
switch (concrete.getType()) {
default:
case MENTION: {
return VIEW_TYPE_MENTION;
}
case FAVOURITE:
case REBLOG: {
return VIEW_TYPE_STATUS_NOTIFICATION;
}
case FOLLOW: {
return VIEW_TYPE_FOLLOW;
}
}
} else if (notification instanceof NotificationViewData.Placeholder) {
return VIEW_TYPE_PLACEHOLDER;
} else {
throw new AssertionError("Unknown notification type");
}
}
}
@ -258,7 +279,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
notificationAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
}
void setMessage(Notification.Type type, String displayName, StatusViewData status) {
void setMessage(Notification.Type type, String displayName,
StatusViewData.Concrete status) {
Context context = message.getContext();
String format;
switch (type) {

@ -0,0 +1,49 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
public class PlaceholderViewHolder extends RecyclerView.ViewHolder {
private Button loadMoreButton;
PlaceholderViewHolder(View itemView) {
super(itemView);
loadMoreButton = itemView.findViewById(R.id.button_load_more);
}
public void setup(boolean enabled, final StatusActionListener listener){
loadMoreButton.setEnabled(enabled);
if(enabled) {
loadMoreButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadMoreButton.setEnabled(false);
listener.onLoadMore(getAdapterPosition());
}
});
}
}
}

@ -271,6 +271,9 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
sensitiveMediaShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(false, getAdapterPosition());
}
v.setVisibility(View.GONE);
sensitiveMediaWarning.setVisibility(View.VISIBLE);
}
@ -470,7 +473,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
container.setOnClickListener(viewThreadListener);
}
void setupWithStatus(StatusViewData status, final StatusActionListener listener,
void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled) {
setDisplayName(status.getUserFullName());
setUsername(status.getNickname());

@ -85,7 +85,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
@Override
void setupWithStatus(final StatusViewData status, final StatusActionListener listener,
void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled) {
super.setupWithStatus(status, listener, mediaPreviewEnabled);
reblogs.setText(status.getReblogsCount());

@ -67,7 +67,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
}
@Override
void setupWithStatus(StatusViewData status, final StatusActionListener listener,
void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled) {
super.setupWithStatus(status, listener, mediaPreviewEnabled);

@ -33,7 +33,7 @@ public class ThreadAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_STATUS_DETAILED = 1;
private List<StatusViewData> statuses;
private List<StatusViewData.Concrete> statuses;
private StatusActionListener statusActionListener;
private boolean mediaPreviewEnabled;
private int detailedStatusPosition;
@ -66,13 +66,12 @@ public class ThreadAdapter extends RecyclerView.Adapter {
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
StatusViewData.Concrete status = statuses.get(position);
if (position == detailedStatusPosition) {
StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder;
StatusViewData status = statuses.get(position);
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled);
} else {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
StatusViewData status = statuses.get(position);
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled);
}
}
@ -91,13 +90,13 @@ public class ThreadAdapter extends RecyclerView.Adapter {
return statuses.size();
}
public void setStatuses(List<StatusViewData> statuses) {
public void setStatuses(List<StatusViewData.Concrete> statuses) {
this.statuses.clear();
this.statuses.addAll(statuses);
notifyDataSetChanged();
}
public void addItem(int position, StatusViewData statusViewData) {
public void addItem(int position, StatusViewData.Concrete statusViewData) {
statuses.add(position, statusViewData);
notifyItemInserted(position);
}
@ -109,12 +108,12 @@ public class ThreadAdapter extends RecyclerView.Adapter {
notifyItemRangeRemoved(0, oldSize);
}
public void addAll(int position, List<StatusViewData> statuses) {
public void addAll(int position, List<StatusViewData.Concrete> statuses) {
this.statuses.addAll(position, statuses);
notifyItemRangeInserted(position, statuses.size());
}
public void addAll(List<StatusViewData> statuses) {
public void addAll(List<StatusViewData.Concrete> statuses) {
int end = statuses.size();
this.statuses.addAll(statuses);
notifyItemRangeInserted(end, statuses.size());
@ -126,7 +125,7 @@ public class ThreadAdapter extends RecyclerView.Adapter {
notifyDataSetChanged();
}
public void setItem(int position, StatusViewData status, boolean notifyAdapter) {
public void setItem(int position, StatusViewData.Concrete status, boolean notifyAdapter) {
statuses.set(position, status);
if (notifyAdapter) {
notifyItemChanged(position);
@ -134,7 +133,7 @@ public class ThreadAdapter extends RecyclerView.Adapter {
}
@Nullable
public StatusViewData getItem(int position) {
public StatusViewData.Concrete getItem(int position) {
if (position != RecyclerView.NO_POSITION && position >= 0 && position < statuses.size()) {
return statuses.get(position);
} else {

@ -31,6 +31,7 @@ import java.util.List;
public class TimelineAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_FOOTER = 1;
private static final int VIEW_TYPE_PLACEHOLDER = 2;
private List<StatusViewData> statuses;
private StatusActionListener statusListener;
@ -59,15 +60,28 @@ public class TimelineAdapter extends RecyclerView.Adapter {
.inflate(R.layout.item_footer, viewGroup, false);
return new FooterViewHolder(view);
}
case VIEW_TYPE_PLACEHOLDER: {
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_status_placeholder, viewGroup, false);
return new PlaceholderViewHolder(view);
}
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < statuses.size()) {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
StatusViewData status = statuses.get(position);
holder.setupWithStatus(status, statusListener, mediaPreviewEnabled);
if (status instanceof StatusViewData.Placeholder) {
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
holder.setup(!((StatusViewData.Placeholder) status).isLoading(), statusListener);
} else {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.setupWithStatus((StatusViewData.Concrete) status,
statusListener, mediaPreviewEnabled);
}
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
@ -84,7 +98,11 @@ public class TimelineAdapter extends RecyclerView.Adapter {
if (position == statuses.size()) {
return VIEW_TYPE_FOOTER;
} else {
return VIEW_TYPE_STATUS;
if (statuses.get(position) instanceof StatusViewData.Placeholder) {
return VIEW_TYPE_PLACEHOLDER;
} else {
return VIEW_TYPE_STATUS;
}
}
}

@ -106,19 +106,17 @@ public class Status {
public static final int MAX_MEDIA_ATTACHMENTS = 4;
@Override
public int hashCode() {
return id.hashCode();
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Status status = (Status) o;
return id != null ? id.equals(status.id) : status.id == null;
}
@Override
public boolean equals(Object other) {
if (this.id == null) {
return this == other;
} else if (!(other instanceof Status)) {
return false;
}
Status status = (Status) other;
return status.id.equals(this.id);
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
public static class MediaAttachment {

@ -44,6 +44,8 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.CollectionUtil;
import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink;
import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.PairedList;
@ -54,7 +56,6 @@ import com.keylesspalace.tusky.viewdata.NotificationViewData;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import retrofit2.Call;
@ -65,11 +66,29 @@ public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener,
NotificationsAdapter.NotificationActionListener,
SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "Notifications"; // logging tag
private static final String TAG = "NotificationF"; // logging tag
private static final int LOAD_AT_ONCE = 30;
private enum FetchEnd {
TOP,
BOTTOM
BOTTOM,
MIDDLE
}
/**
* Placeholder for the notifications. Consider moving to the separate class to hide constructor
* and reuse in different places as needed.
*/
private static final class Placeholder {
private static final Placeholder INSTANCE = new Placeholder();
public static Placeholder getInstance() {
return INSTANCE;
}
private Placeholder() {
}
}
private SwipeRefreshLayout swipeRefreshLayout;
@ -87,11 +106,17 @@ public class NotificationsFragment extends SFragment implements
private String bottomId;
private String topId;
private final PairedList<Notification, NotificationViewData> notifications
= new PairedList<>(new Function<Notification, NotificationViewData>() {
// Each element is either a Notification for loading data or a Placeholder
private final PairedList<Either<Placeholder, Notification>, NotificationViewData> notifications
= new PairedList<>(new Function<Either<Placeholder, Notification>, NotificationViewData>() {
@Override
public NotificationViewData apply(Notification input) {
return ViewDataUtils.notificationToViewData(input);
public NotificationViewData apply(Either<Placeholder, Notification> input) {
if (input.isRight()) {
Notification notification = input.getAsRight();
return ViewDataUtils.notificationToViewData(notification);
} else {
return new NotificationViewData.Placeholder(false);
}
}
});
@ -156,12 +181,10 @@ public class NotificationsFragment extends SFragment implements
TabLayout layout = activity.findViewById(R.id.tab_layout);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
}
public void onTabSelected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
@ -185,7 +208,7 @@ public class NotificationsFragment extends SFragment implements
ActionButtonActivity activity = (ActionButtonActivity) getActivity();
FloatingActionButton composeButton = activity.getActionButton();
if(composeButton != null) {
if (composeButton != null) {
if (hideFab) {
if (dy > 0 && composeButton.isShown()) {
composeButton.hide(); // hides the button if we're scrolling down
@ -220,18 +243,17 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onRefresh() {
sendFetchNotificationsRequest(null, topId, FetchEnd.TOP);
sendFetchNotificationsRequest(null, topId, FetchEnd.TOP, -1);
}
@Override
public void onReply(int position) {
Notification notification = notifications.get(position);
super.reply(notification.status);
super.reply(notifications.get(position).getAsRight().status);
}
@Override
public void onReblog(final boolean reblog, final int position) {
final Notification notification = notifications.get(position);
final Notification notification = notifications.get(position).getAsRight();
final Status status = notification.status;
reblogWithCallback(status, reblog, new Callback<Status>() {
@Override
@ -242,7 +264,9 @@ public class NotificationsFragment extends SFragment implements
if (status.reblog != null) {
status.reblog.reblogged = reblog;
}
notifications.set(position, notification);
// Java's type inference *eyeroll*
notifications.set(position,
Either.<Placeholder, Notification>right(notification));
adapter.updateItemWithNotify(position, notifications.getPairedItem(position), true);
@ -252,8 +276,7 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
Log.d(getClass().getSimpleName(), "Failed to reblog status: " + status.id);
t.printStackTrace();
Log.d(getClass().getSimpleName(), "Failed to reblog status: " + status.id, t);
}
});
}
@ -261,7 +284,7 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onFavourite(final boolean favourite, final int position) {
final Notification notification = notifications.get(position);
final Notification notification = notifications.get(position).getAsRight();
final Status status = notification.status;
favouriteWithCallback(status, favourite, new Callback<Status>() {
@Override
@ -273,7 +296,8 @@ public class NotificationsFragment extends SFragment implements
status.reblog.favourited = favourite;
}
notifications.set(position, notification);
notifications.set(position,
Either.<Placeholder, Notification>right(notification));
adapter.updateItemWithNotify(position, notifications.getPairedItem(position), true);
@ -283,15 +307,14 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
Log.d(getClass().getSimpleName(), "Failed to favourite status: " + status.id);
t.printStackTrace();
Log.d(getClass().getSimpleName(), "Failed to favourite status: " + status.id, t);
}
});
}
@Override
public void onMore(View view, int position) {
Notification notification = notifications.get(position);
Notification notification = notifications.get(position).getAsRight();
super.more(notification.status, view, position);
}
@ -303,24 +326,25 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onViewThread(int position) {
Notification notification = notifications.get(position);
Notification notification = notifications.get(position).getAsRight();
super.viewThread(notification.status);
}
@Override
public void onOpenReblog(int position) {
Notification notification = notifications.get(position);
if (notification != null) onViewAccount(notification.account.id);
Notification notification = notifications.get(position).getAsRight();
onViewAccount(notification.account.id);
}
@Override
public void onExpandedChange(boolean expanded, int position) {
NotificationViewData old = notifications.getPairedItem(position);
StatusViewData statusViewData =
NotificationViewData.Concrete old =
(NotificationViewData.Concrete) notifications.getPairedItem(position);
StatusViewData.Concrete statusViewData =
new StatusViewData.Builder(old.getStatusViewData())
.setIsExpanded(expanded)
.createStatusViewData();
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(),
old.getId(), old.getAccount(), statusViewData);
notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, false);
@ -328,17 +352,38 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onContentHiddenChange(boolean isShowing, int position) {
NotificationViewData old = notifications.getPairedItem(position);
StatusViewData statusViewData =
NotificationViewData.Concrete old =
(NotificationViewData.Concrete) notifications.getPairedItem(position);
StatusViewData.Concrete statusViewData =
new StatusViewData.Builder(old.getStatusViewData())
.setIsShowingSensitiveContent(isShowing)
.createStatusViewData();
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(),
old.getId(), old.getAccount(), statusViewData);
notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, false);
}
@Override
public void onLoadMore(int position) {
//check bounds before accessing list,
if (notifications.size() >= position && position > 0) {
Notification previous = notifications.get(position - 1).getAsRightOrNull();
Notification next = notifications.get(position + 1).getAsRightOrNull();
if (previous == null || next == null) {
Log.e(TAG, "Failed to load more, invalid placeholder position: " + position);
return;
}
sendFetchNotificationsRequest(previous.id, next.id, FetchEnd.MIDDLE, position);
NotificationViewData notificationViewData =
new NotificationViewData.Placeholder(true);
notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, false);
} else {
Log.d(TAG, "error loading more");
}
}
@Override
public void onViewTag(String tag) {
super.viewTag(tag);
@ -374,10 +419,11 @@ public class NotificationsFragment extends SFragment implements
@Override
public void removeAllByAccountId(String accountId) {
// using iterator to safely remove items while iterating
Iterator<Notification> iterator = notifications.iterator();
Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator();
while (iterator.hasNext()) {
Notification notification = iterator.next();
if (notification.account.id.equals(accountId)) {
Either<Placeholder, Notification> notification = iterator.next();
Notification maybeNotification = notification.getAsRightOrNull();
if (maybeNotification != null && maybeNotification.account.id.equals(accountId)) {
iterator.remove();
}
}
@ -385,7 +431,7 @@ public class NotificationsFragment extends SFragment implements
}
private void onLoadMore() {
sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM);
sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM, -1);
}
private void jumpToTop() {
@ -394,7 +440,7 @@ public class NotificationsFragment extends SFragment implements
}
private void sendFetchNotificationsRequest(String fromId, String uptoId,
final FetchEnd fetchEnd) {
final FetchEnd fetchEnd, final int pos) {
/* If there is a fetch already ongoing, record however many fetches are requested and
* fulfill them after it's complete. */
if (fetchEnd == FetchEnd.TOP && topLoading) {
@ -418,7 +464,7 @@ public class NotificationsFragment extends SFragment implements
});
}
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, null);
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE);
call.enqueue(new Callback<List<Notification>>() {
@Override
@ -426,22 +472,22 @@ public class NotificationsFragment extends SFragment implements
@NonNull Response<List<Notification>> response) {
if (response.isSuccessful()) {
String linkHeader = response.headers().get("Link");
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd);
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos);
} else {
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd);
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos);
}
}
@Override
public void onFailure(@NonNull Call<List<Notification>> call, @NonNull Throwable t) {
onFetchNotificationsFailure((Exception) t, fetchEnd);
onFetchNotificationsFailure((Exception) t, fetchEnd, pos);
}
});
callList.add(call);
}
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
FetchEnd fetchEnd) {
FetchEnd fetchEnd, int pos) {
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
switch (fetchEnd) {
case TOP: {
@ -453,6 +499,10 @@ public class NotificationsFragment extends SFragment implements
update(notifications, null, uptoId);
break;
}
case MIDDLE: {
replacePlaceholderWithNotifications(notifications, pos);
break;
}
case BOTTOM: {
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
String fromId = null;
@ -489,8 +539,20 @@ public class NotificationsFragment extends SFragment implements
swipeRefreshLayout.setRefreshing(false);
}
public void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
@Nullable String uptoId) {
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) {
swipeRefreshLayout.setRefreshing(false);
if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) {
NotificationViewData placeholderVD =
new NotificationViewData.Placeholder(false);
notifications.setPairedItem(position, placeholderVD);
adapter.updateItemWithNotify(position, placeholderVD, true);
}
Log.e(TAG, "Fetch failure: " + exception.getMessage());
fulfillAnyQueuedFetches(fetchEnd);
}
private void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
@Nullable String uptoId) {
if (ListUtils.isEmpty(newNotifications)) {
return;
}
@ -500,26 +562,31 @@ public class NotificationsFragment extends SFragment implements
if (uptoId != null) {
topId = uptoId;
}
List<Either<Placeholder, Notification>> liftedNew =
liftNotificationList(newNotifications);
if (notifications.isEmpty()) {
// This construction removes duplicates while preserving order.
notifications.addAll(new LinkedHashSet<>(newNotifications));
notifications.addAll(liftedNew);
} else {
int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1));
int index = notifications.indexOf(liftedNew.get(newNotifications.size() - 1));
for (int i = 0; i < index; i++) {
notifications.remove(0);
}
int newIndex = newNotifications.indexOf(notifications.get(0));
int newIndex = liftedNew.indexOf(notifications.get(0));
if (newIndex == -1) {
notifications.addAll(0, newNotifications);
if (index == -1 && liftedNew.size() >= LOAD_AT_ONCE) {
liftedNew.add(Either.<Placeholder, Notification>left(Placeholder.getInstance()));
}
notifications.addAll(0, liftedNew);
} else {
List<Notification> sublist = newNotifications.subList(0, newIndex);
notifications.addAll(0, sublist);
notifications.addAll(0, liftedNew.subList(0, newIndex));
}
}
adapter.update(notifications.getPairedCopy());
}
public void addItems(List<Notification> newNotifications, @Nullable String fromId) {
private void addItems(List<Notification> newNotifications, @Nullable String fromId) {
if (ListUtils.isEmpty(newNotifications)) {
return;
}
@ -527,9 +594,10 @@ public class NotificationsFragment extends SFragment implements
bottomId = fromId;
}
int end = notifications.size();
Notification last = notifications.get(end - 1);
if (last != null && !findNotification(newNotifications, last.id)) {
notifications.addAll(newNotifications);
List<Either<Placeholder, Notification>> liftedNew = liftNotificationList(newNotifications);
Either<Placeholder, Notification> last = notifications.get(end - 1);
if (last != null && liftedNew.indexOf(last) == -1) {
notifications.addAll(liftedNew);
List<NotificationViewData> newViewDatas = notifications.getPairedCopy()
.subList(notifications.size() - newNotifications.size(),
notifications.size());
@ -537,21 +605,6 @@ public class NotificationsFragment extends SFragment implements
}
}
private static boolean findNotification(List<Notification> notifications, String id) {
for (Notification notification : notifications) {
if (notification.id.equals(id)) {
return true;
}
}
return false;
}
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd) {
swipeRefreshLayout.setRefreshing(false);
Log.e(TAG, "Fetch failure: " + exception.getMessage());
fulfillAnyQueuedFetches(fetchEnd);
}
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
switch (fetchEnd) {
case BOTTOM: {
@ -573,9 +626,43 @@ public class NotificationsFragment extends SFragment implements
}
}
private void replacePlaceholderWithNotifications(List<Notification> newNotifications, int pos) {
// Remove placeholder
notifications.remove(pos);
if (ListUtils.isEmpty(newNotifications)) {
adapter.update(notifications.getPairedCopy());
return;
}
List<Either<Placeholder, Notification>> liftedNew = liftNotificationList(newNotifications);
// If we fetched less posts than in the limit, it means that the hole is not filled
// If we fetched at least as much it means that there are more posts to load and we should
// insert new placeholder
if (newNotifications.size() >= LOAD_AT_ONCE) {
liftedNew.add(Either.<Placeholder, Notification>left(Placeholder.getInstance()));
}
notifications.addAll(pos, liftedNew);
adapter.update(notifications.getPairedCopy());
}
private final Function<Notification, Either<Placeholder, Notification>> notificationLifter =
new Function<Notification, Either<Placeholder, Notification>>() {
@Override
public Either<Placeholder, Notification> apply(Notification input) {
return Either.right(input);
}
};
private List<Either<Placeholder, Notification>> liftNotificationList(List<Notification> list) {
return CollectionUtil.map(list, notificationLifter);
}
private void fullyRefresh() {
adapter.clear();
notifications.clear();
sendFetchNotificationsRequest(null, null, FetchEnd.TOP);
sendFetchNotificationsRequest(null, null, FetchEnd.TOP, -1);
}
}

@ -21,6 +21,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.LocalBroadcastManager;
@ -148,10 +149,10 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
Call<Relationship> call = mastodonApi.muteAccount(id);
call.enqueue(new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, Response<Relationship> response) {}
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {}
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {}
});
callList.add(call);
Intent intent = new Intent(TimelineReceiver.Types.MUTE_ACCOUNT);
@ -164,10 +165,10 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
Call<Relationship> call = mastodonApi.blockAccount(id);
call.enqueue(new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {}
public void onResponse(@NonNull Call<Relationship> call, @NonNull retrofit2.Response<Relationship> response) {}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {}
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {}
});
callList.add(call);
Intent intent = new Intent(TimelineReceiver.Types.BLOCK_ACCOUNT);
@ -180,10 +181,10 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
Call<ResponseBody> call = mastodonApi.deleteStatus(id);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {}
public void onResponse(@NonNull Call<ResponseBody> call, @NonNull retrofit2.Response<ResponseBody> response) {}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {}
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {}
});
callList.add(call);
}

@ -15,6 +15,7 @@
package com.keylesspalace.tusky.fragment;
import android.arch.core.util.Function;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
@ -25,6 +26,7 @@ import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.TabLayout;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.util.Pair;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
@ -43,6 +45,8 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.CollectionUtil;
import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink;
import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.PairedList;
@ -52,7 +56,6 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
@ -64,10 +67,12 @@ public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "Timeline"; // logging tag
private static final String TAG = "TimelineF"; // logging tag
private static final String KIND_ARG = "kind";
private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id";
private static final int LOAD_AT_ONCE = 30;
public enum Kind {
HOME,
PUBLIC_LOCAL,
@ -80,6 +85,7 @@ public class TimelineFragment extends SFragment implements
private enum FetchEnd {
TOP,
BOTTOM,
MIDDLE
}
private SwipeRefreshLayout swipeRefreshLayout;
@ -102,8 +108,18 @@ public class TimelineFragment extends SFragment implements
private String bottomId;
@Nullable
private String topId;
private PairedList<Status, StatusViewData> statuses =
new PairedList<>(ViewDataUtils.statusMapper());
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
new PairedList<>(new Function<Either<Placeholder, Status>, StatusViewData>() {
@Override
public StatusViewData apply(Either<Placeholder, Status> input) {
Status status = input.getAsRightOrNull();
if (status != null) {
return ViewDataUtils.statusToViewData(status);
} else {
return new StatusViewData.Placeholder(false);
}
}
});
public static TimelineFragment newInstance(Kind kind) {
TimelineFragment fragment = new TimelineFragment();
@ -122,6 +138,17 @@ public class TimelineFragment extends SFragment implements
return fragment;
}
private static final class Placeholder {
private final static Placeholder INSTANCE = new Placeholder();
public static Placeholder getInstance() {
return INSTANCE;
}
private Placeholder() {
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@ -179,12 +206,10 @@ public class TimelineFragment extends SFragment implements
TabLayout layout = getActivity().findViewById(R.id.tab_layout);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
}
public void onTabSelected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
@ -219,7 +244,7 @@ public class TimelineFragment extends SFragment implements
} else if (!composeButton.isShown()) {
composeButton.show();
}
}
}
}
@Override
@ -251,86 +276,97 @@ public class TimelineFragment extends SFragment implements
@Override
public void onRefresh() {
sendFetchTimelineRequest(null, topId, FetchEnd.TOP);
sendFetchTimelineRequest(null, topId, FetchEnd.TOP, -1);
}
@Override
public void onReply(int position) {
super.reply(statuses.get(position));
super.reply(statuses.get(position).getAsRight());
}
@Override
public void onReblog(final boolean reblog, final int position) {
final Status status = statuses.get(position);
final Status status = statuses.get(position).getAsRight();
super.reblogWithCallback(status, reblog, new Callback<Status>() {
@Override
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) {
status.reblogged = reblog;
if (status.reblog != null) {
status.reblog.reblogged = reblog;
}
Pair<StatusViewData.Concrete, Integer> actual =
findStatusAndPosition(position, status);
if (actual == null) return;
StatusViewData newViewData =
new StatusViewData.Builder(statuses.getPairedItem(position))
new StatusViewData.Builder(actual.first)
.setReblogged(reblog)
.createStatusViewData();
statuses.setPairedItem(position, newViewData);
adapter.changeItem(position, newViewData, true);
statuses.setPairedItem(actual.second, newViewData);
adapter.changeItem(actual.second, newViewData, true);
}
}
@Override
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
Log.d(TAG, "Failed to reblog status " + status.id);
t.printStackTrace();
Log.d(TAG, "Failed to reblog status " + status.id, t);
}
});
}
@Override
public void onFavourite(final boolean favourite, final int position) {
final Status status = statuses.get(position);
final Status status = statuses.get(position).getAsRight();
super.favouriteWithCallback(status, favourite, new Callback<Status>() {
@Override
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) {
status.favourited = favourite;
if (status.reblog != null) {
status.reblog.favourited = favourite;
}
Pair<StatusViewData.Concrete, Integer> actual =
findStatusAndPosition(position, status);
if (actual == null) return;
StatusViewData newViewData = new StatusViewData
.Builder(statuses.getPairedItem(position))
.Builder(actual.first)
.setFavourited(favourite)
.createStatusViewData();
statuses.setPairedItem(position, newViewData);
adapter.changeItem(position, newViewData, true);
statuses.setPairedItem(actual.second, newViewData);
adapter.changeItem(actual.second, newViewData, true);
}
}
@Override
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
Log.d(TAG, "Failed to favourite status " + status.id);
t.printStackTrace();
Log.d(TAG, "Failed to favourite status " + status.id, t);
}
});
}
@Override
public void onMore(View view, final int position) {
super.more(statuses.get(position), view, position);
super.more(statuses.get(position).getAsRight(), view, position);
}
@Override
public void onOpenReblog(int position) {
super.openReblog(statuses.get(position));
super.openReblog(statuses.get(position).getAsRight());
}
@Override
public void onExpandedChange(boolean expanded, int position) {
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
StatusViewData newViewData = new StatusViewData.Builder(
((StatusViewData.Concrete) statuses.getPairedItem(position)))
.setIsExpanded(expanded).createStatusViewData();
statuses.setPairedItem(position, newViewData);
adapter.changeItem(position, newViewData, false);
@ -338,12 +374,33 @@ public class TimelineFragment extends SFragment implements
@Override
public void onContentHiddenChange(boolean isShowing, int position) {
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
StatusViewData newViewData = new StatusViewData.Builder(
((StatusViewData.Concrete) statuses.getPairedItem(position)))
.setIsShowingSensitiveContent(isShowing).createStatusViewData();
statuses.setPairedItem(position, newViewData);
adapter.changeItem(position, newViewData, false);
}
@Override
public void onLoadMore(int position) {
//check bounds before accessing list,
if (statuses.size() >= position && position > 0) {
Status fromStatus = statuses.get(position - 1).getAsRightOrNull();
Status toStatus = statuses.get(position + 1).getAsRightOrNull();
if (fromStatus == null || toStatus == null) {
Log.e(TAG, "Failed to load more at " + position + ", wrong placeholder position");
return;
}
sendFetchTimelineRequest(fromStatus.id, toStatus.id, FetchEnd.MIDDLE, position);
StatusViewData newViewData = new StatusViewData.Placeholder(true);
statuses.setPairedItem(position, newViewData);
adapter.changeItem(position, newViewData, false);
} else {
Log.e(TAG, "error loading more");
}
}
@Override
public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type,
View view) {
@ -352,7 +409,7 @@ public class TimelineFragment extends SFragment implements
@Override
public void onViewThread(int position) {
super.viewThread(statuses.get(position));
super.viewThread(statuses.get(position).getAsRight());
}
@Override
@ -417,10 +474,10 @@ public class TimelineFragment extends SFragment implements
@Override
public void removeAllByAccountId(String accountId) {
// using iterator to safely remove items while iterating
Iterator<Status> iterator = statuses.iterator();
Iterator<Either<Placeholder, Status>> iterator = statuses.iterator();
while (iterator.hasNext()) {
Status status = iterator.next();
if (status.account.id.equals(accountId)) {
Status status = iterator.next().getAsRightOrNull();
if (status != null && status.account.id.equals(accountId)) {
iterator.remove();
}
}
@ -428,12 +485,12 @@ public class TimelineFragment extends SFragment implements
}
private void onLoadMore() {
sendFetchTimelineRequest(bottomId, null, FetchEnd.BOTTOM);
sendFetchTimelineRequest(bottomId, null, FetchEnd.BOTTOM, -1);
}
private void fullyRefresh() {
adapter.clear();
sendFetchTimelineRequest(null, null, FetchEnd.TOP);
sendFetchTimelineRequest(null, null, FetchEnd.TOP, -1);
}
private boolean jumpToTopAllowed() {
@ -457,20 +514,20 @@ public class TimelineFragment extends SFragment implements
case HOME:
return api.homeTimeline(fromId, uptoId, null);
case PUBLIC_FEDERATED:
return api.publicTimeline(null, fromId, uptoId, null);
return api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE);
case PUBLIC_LOCAL:
return api.publicTimeline(true, fromId, uptoId, null);
return api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE);
case TAG:
return api.hashtagTimeline(tagOrId, null, fromId, uptoId, null);
return api.hashtagTimeline(tagOrId, null, fromId, uptoId, LOAD_AT_ONCE);
case USER:
return api.accountStatuses(tagOrId, fromId, uptoId, null, null);
return api.accountStatuses(tagOrId, fromId, uptoId, LOAD_AT_ONCE);
case FAVOURITES:
return api.favourites(fromId, uptoId, null);
return api.favourites(fromId, uptoId, LOAD_AT_ONCE);
}
}
private void sendFetchTimelineRequest(@Nullable String fromId, @Nullable String uptoId,
final FetchEnd fetchEnd) {
final FetchEnd fetchEnd, final int pos) {
/* If there is a fetch already ongoing, record however many fetches are requested and
* fulfill them after it's complete. */
if (fetchEnd == FetchEnd.TOP && topLoading) {
@ -499,15 +556,15 @@ public class TimelineFragment extends SFragment implements
public void onResponse(@NonNull Call<List<Status>> call, @NonNull Response<List<Status>> response) {
if (response.isSuccessful()) {
String linkHeader = response.headers().get("Link");
onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd);
onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd, pos);
} else {
onFetchTimelineFailure(new Exception(response.message()), fetchEnd);
onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos);
}
}
@Override
public void onFailure(@NonNull Call<List<Status>> call, @NonNull Throwable t) {
onFetchTimelineFailure((Exception) t, fetchEnd);
onFetchTimelineFailure((Exception) t, fetchEnd, pos);
}
};
@ -516,8 +573,11 @@ public class TimelineFragment extends SFragment implements
listCall.enqueue(callback);
}
public void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
FetchEnd fetchEnd) {
private void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
FetchEnd fetchEnd, int pos) {
// We filled the hole (or reached the end) if the server returned less statuses than we
// we asked for.
boolean fullFetch = statuses.size() >= LOAD_AT_ONCE;
filterStatuses(statuses);
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
switch (fetchEnd) {
@ -527,7 +587,11 @@ public class TimelineFragment extends SFragment implements
if (previous != null) {
uptoId = previous.uri.getQueryParameter("since_id");
}
updateStatuses(statuses, null, uptoId);
updateStatuses(statuses, null, uptoId, fullFetch);
break;
}
case MIDDLE: {
replacePlaceholderWithStatuses(statuses, fullFetch, pos);
break;
}
case BOTTOM: {
@ -547,7 +611,7 @@ public class TimelineFragment extends SFragment implements
if (previous != null) {
uptoId = previous.uri.getQueryParameter("since_id");
}
updateStatuses(statuses, fromId, uptoId);
updateStatuses(statuses, fromId, uptoId, fullFetch);
}
break;
}
@ -561,8 +625,15 @@ public class TimelineFragment extends SFragment implements
swipeRefreshLayout.setRefreshing(false);
}
public void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd) {
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
swipeRefreshLayout.setRefreshing(false);
if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) {
StatusViewData newViewData = new StatusViewData.Placeholder(false);
statuses.setPairedItem(position, newViewData);
adapter.changeItem(position, newViewData, true);
}
Log.e(TAG, "Fetch Failure: " + exception.getMessage());
fulfillAnyQueuedFetches(fetchEnd);
}
@ -588,7 +659,7 @@ public class TimelineFragment extends SFragment implements
}
}
protected void filterStatuses(List<Status> statuses) {
private void filterStatuses(List<Status> statuses) {
Iterator<Status> it = statuses.iterator();
while (it.hasNext()) {
Status status = it.next();
@ -600,7 +671,7 @@ public class TimelineFragment extends SFragment implements
}
private void updateStatuses(List<Status> newStatuses, @Nullable String fromId,
@Nullable String toId) {
@Nullable String toId, boolean fullFetch) {
if (ListUtils.isEmpty(newStatuses)) {
return;
}
@ -610,20 +681,26 @@ public class TimelineFragment extends SFragment implements
if (toId != null) {
topId = toId;
}
List<Either<Placeholder, Status>> liftedNew = listStatusList(newStatuses);
if (statuses.isEmpty()) {
// This construction removes duplicates while preserving order.
statuses.addAll(new LinkedHashSet<>(newStatuses));
statuses.addAll(liftedNew);
} else {
Status lastOfNew = newStatuses.get(newStatuses.size() - 1);
Either<Placeholder, Status> lastOfNew = liftedNew.get(newStatuses.size() - 1);
int index = statuses.indexOf(lastOfNew);
for (int i = 0; i < index; i++) {
statuses.remove(0);
}
int newIndex = newStatuses.indexOf(statuses.get(0));
int newIndex = liftedNew.indexOf(statuses.get(0));
if (newIndex == -1) {
statuses.addAll(0, newStatuses);
if (index == -1 && fullFetch) {
liftedNew.add(Either.<Placeholder, Status>left(Placeholder.getInstance()));
}
statuses.addAll(0, liftedNew);
} else {
statuses.addAll(0, newStatuses.subList(0, newIndex));
statuses.addAll(0, liftedNew.subList(0, newIndex));
}
}
adapter.update(statuses.getPairedCopy());
@ -634,15 +711,17 @@ public class TimelineFragment extends SFragment implements
return;
}
int end = statuses.size();
Status last = statuses.get(end - 1);
Status last = statuses.get(end - 1).getAsRightOrNull();
// I was about to replace findStatus with indexOf but it is incorrect to compare value
// types by ID anyway and we should change equals() for Status, I think, so this makes sense
if (last != null && !findStatus(newStatuses, last.id)) {
statuses.addAll(newStatuses);
statuses.addAll(listStatusList(newStatuses));
List<StatusViewData> newViewDatas = statuses.getPairedCopy()
.subList(statuses.size() - newStatuses.size(), statuses.size());
if (BuildConfig.DEBUG && newStatuses.size() != newViewDatas.size()) {
String error = String.format(Locale.getDefault(),
"Incorrectly got statusViewData sublist." +
" newStatuses.size == %d newViewDatas.size == %d, statuses.size == %d",
" newStatuses.size == %d newViewDatas.size == %d, statuses.size == %d",
newStatuses.size(), newViewDatas.size(), statuses.size());
throw new AssertionError(error);
}
@ -653,6 +732,28 @@ public class TimelineFragment extends SFragment implements
}
}
private void replacePlaceholderWithStatuses(List<Status> newStatuses, boolean fullFetch, int pos) {
Status status = statuses.get(pos).getAsRightOrNull();
if (status == null) {
statuses.remove(pos);
}
if (ListUtils.isEmpty(newStatuses)) {
adapter.update(statuses.getPairedCopy());
return;
}
List<Either<Placeholder, Status>> liftedNew = listStatusList(newStatuses);
if (fullFetch) {
liftedNew.add(Either.<Placeholder, Status>left(Placeholder.getInstance()));
}
statuses.addAll(pos, liftedNew);
adapter.update(statuses.getPairedCopy());
}
private static boolean findStatus(List<Status> statuses, String id) {
for (Status status : statuses) {
if (status.id.equals(id)) {
@ -661,4 +762,39 @@ public class TimelineFragment extends SFragment implements
}
return false;
}
private final Function<Status, Either<Placeholder, Status>> statusLifter =
new Function<Status, Either<Placeholder, Status>>() {
@Override
public Either<Placeholder, Status> apply(Status input) {
return Either.right(input);
}
};
private @Nullable
Pair<StatusViewData.Concrete, Integer>
findStatusAndPosition(int position, Status status) {
StatusViewData.Concrete statusToUpdate;
int positionToUpdate;
StatusViewData someOldViewData = statuses.getPairedItem(position);
// Unlikely, but data could change between the request and response
if ((someOldViewData instanceof StatusViewData.Placeholder) ||
!((StatusViewData.Concrete) someOldViewData).getId().equals(status.id)) {
// try to find the status we need to update
int foundPos = statuses.indexOf(Either.<Placeholder, Status>right(status));
if (foundPos < 0) return null; // okay, it's hopeless, give up
statusToUpdate = ((StatusViewData.Concrete)
statuses.getPairedItem(foundPos));
positionToUpdate = position;
} else {
statusToUpdate = (StatusViewData.Concrete) someOldViewData;
positionToUpdate = position;
}
return new Pair<>(statusToUpdate, positionToUpdate);
}
private List<Either<Placeholder, Status>> listStatusList(List<Status> list) {
return CollectionUtil.map(list, statusLifter);
}
}

@ -69,7 +69,7 @@ public class ViewThreadFragment extends SFragment implements
private int statusIndex = 0;
private final PairedList<Status, StatusViewData> statuses =
private final PairedList<Status, StatusViewData.Concrete> statuses =
new PairedList<>(ViewDataUtils.statusMapper());
public static ViewThreadFragment newInstance(String id) {
@ -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,22 +227,29 @@ 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);
}
@Override
public void onLoadMore(int pos) {
}
@Override
public void onViewTag(String tag) {
super.viewTag(tag);
@ -255,7 +262,7 @@ public class ViewThreadFragment extends SFragment implements
@Override
public void removeItem(int position) {
if(position == statusIndex) {
if (position == statusIndex) {
//the status got removed, close the activity
getActivity().finish();
}
@ -278,7 +285,7 @@ public class ViewThreadFragment extends SFragment implements
}
}
statusIndex = statuses.indexOf(status);
if(statusIndex == -1) {
if (statusIndex == -1) {
//the status got removed, close the activity
getActivity().finish();
return;
@ -379,8 +386,8 @@ public class ViewThreadFragment extends SFragment implements
int i = statusIndex;
statuses.add(i, status);
adapter.setDetailedStatusPosition(i);
StatusViewData viewData = statuses.getPairedItem(i);
if(viewData.getCard() == null && card != null) {
StatusViewData.Concrete viewData = statuses.getPairedItem(i);
if (viewData.getCard() == null && card != null) {
viewData = new StatusViewData.Builder(viewData)
.setCard(card)
.createStatusViewData();
@ -405,7 +412,7 @@ public class ViewThreadFragment extends SFragment implements
statusIndex = ancestors.size();
adapter.setDetailedStatusPosition(statusIndex);
statuses.addAll(0, ancestors);
List<StatusViewData> ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex);
List<StatusViewData.Concrete> ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex);
if (BuildConfig.DEBUG && ancestors.size() != ancestorsViewDatas.size()) {
String error = String.format(Locale.getDefault(),
"Incorrectly got statusViewData sublist." +
@ -420,8 +427,8 @@ public class ViewThreadFragment extends SFragment implements
// In case we needed to delete everything (which is way easier than deleting
// everything except one), re-insert the remaining status here.
statuses.add(statusIndex, mainStatus);
StatusViewData viewData = statuses.getPairedItem(statusIndex);
if(viewData.getCard() == null && card != null) {
StatusViewData.Concrete viewData = statuses.getPairedItem(statusIndex);
if (viewData.getCard() == null && card != null) {
viewData = new StatusViewData.Builder(viewData)
.setCard(card)
.createStatusViewData();
@ -431,9 +438,9 @@ public class ViewThreadFragment extends SFragment implements
// Insert newly fetched descendants
statuses.addAll(descendants);
List<StatusViewData> descendantsViewData;
descendantsViewData = statuses.getPairedCopy()
.subList(statuses.size() - descendants.size(), statuses.size());
List<StatusViewData.Concrete> 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." +
@ -447,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();
statuses.setPairedItem(statusIndex, newViewData);
adapter.setItem(statusIndex, newViewData, true);
}
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);
}
}

@ -29,4 +29,5 @@ public interface StatusActionListener extends LinkListener {
void onOpenReblog(int position);
void onExpandedChange(boolean expanded, int position);
void onContentHiddenChange(boolean isShowing, int position);
void onLoadMore(int position);
}

@ -0,0 +1,38 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.arch.core.util.Function;
import java.util.ArrayList;
import java.util.List;
/**
* Created by charlag on 05/11/17.
*/
public final class CollectionUtil {
private CollectionUtil() {
throw new AssertionError();
}
public static <E, R> List<R> map(List<E> list, Function<E, R> mapper) {
final List<R> newList = new ArrayList<>(list.size());
for (E el : list) {
newList.add(mapper.apply(el));
}
return newList;
}
}

@ -0,0 +1,125 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Created by charlag on 05/11/17.
*
* Class to represent sum type/tagged union/variant/ADT e.t.c.
* It is either Left or Right.
*/
public final class Either<L, R> {
/**
* Constructs Left instance of either
* @param left Object to be considered Left
* @param <L> Left type
* @param <R> Right type
* @return new instance of Either which contains left.
*/
public static <L, R> Either<L, R> left(L left) {
return new Either<>(left, false);
}
/**
* Constructs Right instance of either
* @param right Object to be considered Right
* @param <L> Left type
* @param <R> Right type
* @return new instance of Either which contains right.
*/
public static <L, R> Either<L, R> right(R right) {
return new Either<>(right, true);
}
private final Object value;
// we need it because of the types erasure
private boolean isRight;
private Either(Object value, boolean isRight) {
this.value = value;
this.isRight = isRight;
}
public boolean isRight() {
return isRight;
}
/**
* Try to get contained object as a Left or throw an exception.
* @throws AssertionError If contained value is Right
* @return contained value as Right
*/
public @NonNull L getAsLeft() {
if (isRight) {
throw new AssertionError("Tried to get the Either as Left while it is Right");
}
//noinspection unchecked
return (L) value;
}
/**
* Try to get contained object as a Right or throw an exception.
* @throws AssertionError If contained value is Left
* @return contained value as Right
*/
public @NonNull R getAsRight() {
if (!isRight) {
throw new AssertionError("Tried to get the Either as Right while it is Left");
}
//noinspection unchecked
return (R) value;
}
/**
* Same as {@link #getAsLeft()} but returns {@code null} is the value if Right instead of
* throwing an exception.
* @return contained value as Left or null
*/
public @Nullable L getAsLeftOrNull() {
if (isRight) {
return null;
}
//noinspection unchecked
return (L) value;
}
/**
* Same as {@link #getAsRightOrNull()} but returns {@code null} is the value if Left instead of
* throwing an exception.
* @return contained value as Right or null
*/
public @Nullable R getAsRightOrNull() {
if (!isRight) {
return null;
}
//noinspection unchecked
return (R) value;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (!(obj instanceof Either)) return false;
Either that = (Either) obj;
return this.isRight == that.isRight &&
(this.value == that.value ||
this.value != null && this.value.equals(that.value));
}
}

@ -1,3 +1,18 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.arch.core.util.Function;
@ -17,7 +32,7 @@ import java.util.List;
public final class ViewDataUtils {
@Nullable
public static StatusViewData statusToViewData(@Nullable Status status) {
public static StatusViewData.Concrete statusToViewData(@Nullable Status status) {
if (status == null) return null;
Status visibleStatus = status.reblog == null ? status : status.reblog;
return new StatusViewData.Builder().setId(status.id)
@ -55,12 +70,12 @@ public final class ViewDataUtils {
return viewDatas;
}
public static Function<Status, StatusViewData> statusMapper() {
public static Function<Status, StatusViewData.Concrete> statusMapper() {
return statusMapper;
}
public static NotificationViewData notificationToViewData(Notification notification) {
return new NotificationViewData(notification.type, notification.id, notification.account,
public static NotificationViewData.Concrete notificationToViewData(Notification notification) {
return new NotificationViewData.Concrete(notification.type, notification.id, notification.account,
statusToViewData(notification.status));
}
@ -73,10 +88,10 @@ public final class ViewDataUtils {
return viewDatas;
}
private static final Function<Status, StatusViewData> statusMapper =
new Function<Status, StatusViewData>() {
private static final Function<Status, StatusViewData.Concrete> statusMapper =
new Function<Status, StatusViewData.Concrete>() {
@Override
public StatusViewData apply(Status input) {
public StatusViewData.Concrete apply(Status input) {
return ViewDataUtils.statusToViewData(input);
}
};

@ -49,16 +49,16 @@ public class ConversationLineItemDecoration extends RecyclerView.ItemDecoration
int position = parent.getChildAdapterPosition(child);
ThreadAdapter adapter = (ThreadAdapter) parent.getAdapter();
StatusViewData current = adapter.getItem(position);
StatusViewData.Concrete current = adapter.getItem(position);
int dividerTop, dividerBottom;
if (current != null) {
StatusViewData above = adapter.getItem(position - 1);
StatusViewData.Concrete above = adapter.getItem(position - 1);
if (above != null && above.getId().equals(current.getInReplyToId())) {
dividerTop = child.getTop();
} else {
dividerTop = child.getTop() + avatarMargin;
}
StatusViewData below = adapter.getItem(position + 1);
StatusViewData.Concrete below = adapter.getItem(position + 1);
if (below != null && current.getId().equals(below.getInReplyToId())) {
dividerBottom = child.getBottom();
} else {

@ -1,3 +1,18 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.viewdata;
import com.keylesspalace.tusky.entity.Account;
@ -5,35 +20,59 @@ import com.keylesspalace.tusky.entity.Notification;
/**
* Created by charlag on 12/07/2017.
*
* Class to represent data required to display either a notification or a placeholder.
* It is either a {@link Placeholder} or a {@link Concrete}.
* It is modelled this way because close relationship between placeholder and concrete notification
* is fine in this case. Placeholder case is not modelled as a type of notification because
* invariants would be violated and because it would model domain incorrectly. It is prefereable to
* {@link com.keylesspalace.tusky.util.Either} because class hierarchy is cheaper, faster and
* more native.
*/
public 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 abstract class NotificationViewData {
private NotificationViewData() {
}
public Notification.Type getType() {
return type;
}
public static final class Concrete extends NotificationViewData {
private final Notification.Type type;
private final String id;
private final Account account;
private final StatusViewData.Concrete statusViewData;
public String getId() {
return id;
}
public Concrete(Notification.Type type, String id, Account account,
StatusViewData.Concrete statusViewData) {
this.type = type;
this.id = id;
this.account = account;
this.statusViewData = statusViewData;
}
public Notification.Type getType() {
return type;
}
public Account getAccount() {
return account;
public String getId() {
return id;
}
public Account getAccount() {
return account;
}
public StatusViewData.Concrete getStatusViewData() {
return statusViewData;
}
}
public StatusViewData getStatusViewData() {
return statusViewData;
public static final class Placeholder extends NotificationViewData {
private final boolean isLoading;
public Placeholder(boolean isLoading) {
this.isLoading = isLoading;
}
public boolean isLoading() {
return isLoading;
}
}
}

@ -1,3 +1,18 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.viewdata;
import android.support.annotation.Nullable;
@ -12,179 +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<Status.Emoji> emojis;
@Nullable
private final Card card;
public StatusViewData(String id, Spanned content, boolean reblogged, boolean favourited,
String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments,
String rebloggedByUsername, String rebloggedAvatar, boolean sensitive, boolean isExpanded,
boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar,
Date createdAt, String reblogsCount, String favouritesCount, String inReplyToId,
Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
Status.Application application, List<Status.Emoji> emojis, Card card) {
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 abstract class StatusViewData {
public String getId() {
return id;
private StatusViewData() {
}
public Spanned getContent() {
return content;
}
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<Status.Emoji> 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<Status.Emoji> 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 boolean isReblogged() {
return reblogged;
}
public String getId() {
return id;
}
public boolean isFavourited() {
return favourited;
}
public Spanned getContent() {
return content;
}
@Nullable
public String getSpoilerText() {
return spoilerText;
}
public boolean isReblogged() {
return reblogged;
}
public Status.Visibility getVisibility() {
return visibility;
}
public boolean isFavourited() {
return favourited;
}
public Status.MediaAttachment[] getAttachments() {
return attachments;
}
@Nullable
public String getSpoilerText() {
return spoilerText;
}
@Nullable
public String getRebloggedByUsername() {
return rebloggedByUsername;
}
public Status.Visibility getVisibility() {
return visibility;
}
public boolean isSensitive() {
return isSensitive;
}
public Status.MediaAttachment[] getAttachments() {
return attachments;
}
public boolean isExpanded() {
return isExpanded;
}
@Nullable
public String getRebloggedByUsername() {
return rebloggedByUsername;
}
public boolean isShowingSensitiveContent() {
return isShowingSensitiveContent;
}
public boolean isSensitive() {
return isSensitive;
}
@Nullable
public String getRebloggedAvatar() {
return rebloggedAvatar;
}
public boolean isExpanded() {
return isExpanded;
}
public String getUserFullName() {
return userFullName;
}
public boolean isShowingSensitiveContent() {
return isShowingSensitiveContent;
}
public String getNickname() {
return nickname;
}
@Nullable
public String getRebloggedAvatar() {
return rebloggedAvatar;
}
public String getAvatar() {
return avatar;
}
public String getUserFullName() {
return userFullName;
}
public Date getCreatedAt() {
return createdAt;
}
public String getNickname() {
return nickname;
}
public String getReblogsCount() {
return reblogsCount;
}
public String getAvatar() {
return avatar;
}
public String getFavouritesCount() {
return favouritesCount;
}
public Date getCreatedAt() {
return createdAt;
}
@Nullable
public String getInReplyToId() {
return inReplyToId;
}
public String getReblogsCount() {
return reblogsCount;
}
public String getSenderId() {
return senderId;
}
public String getFavouritesCount() {
return favouritesCount;
}
public Boolean getRebloggingEnabled() {
return rebloggingEnabled;
}
@Nullable
public String getInReplyToId() {
return inReplyToId;
}
@Nullable
public Status.Mention[] getMentions() {
return mentions;
}
public String getSenderId() {
return senderId;
}
public Status.Application getApplication() {
return application;
}
public Boolean getRebloggingEnabled() {
return rebloggingEnabled;
}
@Nullable
public Status.Mention[] getMentions() {
return mentions;
}
public Status.Application getApplication() {
return application;
}
public List<Status.Emoji> getEmojis() {
return emojis;
}
@Nullable
public Card getCard() {
return card;
}
public List<Status.Emoji> getEmojis() {
return emojis;
}
public Card getCard() {
return card;
public static final class Placeholder extends StatusViewData {
private final boolean isLoading;
public Placeholder(boolean isLoading) {
this.isLoading = isLoading;
}
public boolean isLoading() {
return isLoading;
}
}
public static class Builder {
@ -217,7 +255,7 @@ public final class StatusViewData {
public Builder() {
}
public Builder(final StatusViewData viewData) {
public Builder(final StatusViewData.Concrete viewData) {
id = viewData.id;
content = viewData.content;
reblogged = viewData.reblogged;
@ -243,7 +281,6 @@ public final class StatusViewData {
application = viewData.application;
emojis = viewData.getEmojis();
card = viewData.getCard();
}
public Builder setId(String id) {
@ -371,12 +408,15 @@ public final class StatusViewData {
return this;
}
public StatusViewData createStatusViewData() {
public StatusViewData.Concrete createStatusViewData() {
if (this.emojis == null) emojis = Collections.emptyList();
return new StatusViewData(id, content, reblogged, favourited, spoilerText, visibility,
if (this.createdAt == null) createdAt = new Date();
return new StatusViewData.Concrete(id, content, reblogged, favourited, spoilerText, visibility,
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, reblogsCount,
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, emojis, card);
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,
emojis, card);
}
}
}

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/button_load_more"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="match_parent"
android:layout_height="72dp"
android:text="@string/load_more_placeholder_text"
android:textColor="?attr/colorAccent" />

@ -237,5 +237,6 @@
<string name="pref_title_alway_show_sensitive_media">Always show all nsfw content</string>
<string name="title_media">Media</string>
<string name="replying_to">Replying to @%s</string>
<string name="load_more_placeholder_text">load more</string>
</resources>

Loading…
Cancel
Save