implement "load more" placeholder

This commit is contained in:
Conny Duck 2017-11-03 22:17:31 +01:00
parent fcb8a23343
commit 80a10c1ac1
16 changed files with 398 additions and 95 deletions

View file

@ -47,6 +47,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_FOOTER = 1; private static final int VIEW_TYPE_FOOTER = 1;
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2; private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
private static final int VIEW_TYPE_FOLLOW = 3; private static final int VIEW_TYPE_FOLLOW = 3;
private static final int VIEW_TYPE_PLACEHOLDER = 4;
private List<NotificationViewData> notifications; private List<NotificationViewData> notifications;
private StatusActionListener statusListener; private StatusActionListener statusListener;
@ -88,6 +89,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.inflate(R.layout.item_follow, parent, false); .inflate(R.layout.item_follow, parent, false);
return new FollowViewHolder(view); 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);
}
} }
} }
@ -121,6 +127,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
holder.setupButtons(notificationActionListener, notification.getAccount().id); holder.setupButtons(notificationActionListener, notification.getAccount().id);
break; break;
} }
case PLACEHOLDER: {
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
holder.setup(!notification.isPlaceholderLoading(), statusListener);
break;
}
} }
} else { } else {
FooterViewHolder holder = (FooterViewHolder) viewHolder; FooterViewHolder holder = (FooterViewHolder) viewHolder;
@ -151,6 +162,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case FOLLOW: { case FOLLOW: {
return VIEW_TYPE_FOLLOW; return VIEW_TYPE_FOLLOW;
} }
case PLACEHOLDER: {
return VIEW_TYPE_PLACEHOLDER;
}
} }
} }
} }

View file

@ -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());
}
});
}
}
}

View file

@ -271,6 +271,9 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
sensitiveMediaShow.setOnClickListener(new View.OnClickListener() { sensitiveMediaShow.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(false, getAdapterPosition());
}
v.setVisibility(View.GONE); v.setVisibility(View.GONE);
sensitiveMediaWarning.setVisibility(View.VISIBLE); sensitiveMediaWarning.setVisibility(View.VISIBLE);
} }

View file

@ -31,6 +31,7 @@ import java.util.List;
public class TimelineAdapter extends RecyclerView.Adapter { public class TimelineAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_STATUS = 0; private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_FOOTER = 1; private static final int VIEW_TYPE_FOOTER = 1;
private static final int VIEW_TYPE_PLACEHOLDER = 2;
private List<StatusViewData> statuses; private List<StatusViewData> statuses;
private StatusActionListener statusListener; private StatusActionListener statusListener;
@ -59,15 +60,26 @@ public class TimelineAdapter extends RecyclerView.Adapter {
.inflate(R.layout.item_footer, viewGroup, false); .inflate(R.layout.item_footer, viewGroup, false);
return new FooterViewHolder(view); 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 @Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < statuses.size()) { if (position < statuses.size()) {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
StatusViewData status = statuses.get(position); StatusViewData status = statuses.get(position);
holder.setupWithStatus(status, statusListener, mediaPreviewEnabled); if(status.isPlaceholder()) {
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
holder.setup(!status.isPlaceholderLoading(), statusListener);
} else {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.setupWithStatus(status, statusListener, mediaPreviewEnabled);
}
} else { } else {
FooterViewHolder holder = (FooterViewHolder) viewHolder; FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState); holder.setState(footerState);
@ -84,7 +96,11 @@ public class TimelineAdapter extends RecyclerView.Adapter {
if (position == statuses.size()) { if (position == statuses.size()) {
return VIEW_TYPE_FOOTER; return VIEW_TYPE_FOOTER;
} else { } else {
return VIEW_TYPE_STATUS; if(statuses.get(position).isPlaceholder()) {
return VIEW_TYPE_PLACEHOLDER;
} else {
return VIEW_TYPE_STATUS;
}
} }
} }

View file

@ -27,6 +27,7 @@ public class Notification {
FAVOURITE, FAVOURITE,
@SerializedName("follow") @SerializedName("follow")
FOLLOW, FOLLOW,
PLACEHOLDER
} }
public Type type; public Type type;

View file

@ -27,6 +27,10 @@ import java.util.Date;
import java.util.List; import java.util.List;
public class Status { 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; public String url;
@SerializedName("reblogs_count") @SerializedName("reblogs_count")
@ -106,19 +110,21 @@ public class Status {
public static final int MAX_MEDIA_ATTACHMENTS = 4; public static final int MAX_MEDIA_ATTACHMENTS = 4;
@Override @Override
public int hashCode() { public boolean equals(Object o) {
return id.hashCode(); if (this == o) return true;
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 @Override
public boolean equals(Object other) { public int hashCode() {
if (this.id == null) { int result = (placeholder ? 1 : 0);
return this == other; result = 31 * result + (id != null ? id.hashCode() : 0);
} else if (!(other instanceof Status)) { return result;
return false;
}
Status status = (Status) other;
return status.id.equals(this.id);
} }
public static class MediaAttachment { public static class MediaAttachment {

View file

@ -54,7 +54,6 @@ import com.keylesspalace.tusky.viewdata.NotificationViewData;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import retrofit2.Call; import retrofit2.Call;
@ -65,11 +64,14 @@ public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, SwipeRefreshLayout.OnRefreshListener, StatusActionListener,
NotificationsAdapter.NotificationActionListener, NotificationsAdapter.NotificationActionListener,
SharedPreferences.OnSharedPreferenceChangeListener { 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 { private enum FetchEnd {
TOP, TOP,
BOTTOM BOTTOM,
MIDDLE
} }
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
@ -156,12 +158,10 @@ public class NotificationsFragment extends SFragment implements
TabLayout layout = activity.findViewById(R.id.tab_layout); TabLayout layout = activity.findViewById(R.id.tab_layout);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() { onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override @Override
public void onTabSelected(TabLayout.Tab tab) { public void onTabSelected(TabLayout.Tab tab) {}
}
@Override @Override
public void onTabUnselected(TabLayout.Tab tab) { public void onTabUnselected(TabLayout.Tab tab) {}
}
@Override @Override
public void onTabReselected(TabLayout.Tab tab) { public void onTabReselected(TabLayout.Tab tab) {
@ -220,7 +220,7 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public void onRefresh() { public void onRefresh() {
sendFetchNotificationsRequest(null, topId, FetchEnd.TOP); sendFetchNotificationsRequest(null, topId, FetchEnd.TOP, -1);
} }
@Override @Override
@ -252,8 +252,7 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) { public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
Log.d(getClass().getSimpleName(), "Failed to reblog status: " + status.id); Log.d(getClass().getSimpleName(), "Failed to reblog status: " + status.id, t);
t.printStackTrace();
} }
}); });
} }
@ -283,8 +282,7 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) { public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
Log.d(getClass().getSimpleName(), "Failed to favourite status: " + status.id); Log.d(getClass().getSimpleName(), "Failed to favourite status: " + status.id, t);
t.printStackTrace();
} }
}); });
} }
@ -321,7 +319,7 @@ public class NotificationsFragment extends SFragment implements
.setIsExpanded(expanded) .setIsExpanded(expanded)
.createStatusViewData(); .createStatusViewData();
NotificationViewData notificationViewData = new NotificationViewData(old.getType(), NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
old.getId(), old.getAccount(), statusViewData); old.getId(), old.getAccount(), statusViewData, false);
notifications.setPairedItem(position, notificationViewData); notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, false); adapter.updateItemWithNotify(position, notificationViewData, false);
} }
@ -334,11 +332,29 @@ public class NotificationsFragment extends SFragment implements
.setIsShowingSensitiveContent(isShowing) .setIsShowingSensitiveContent(isShowing)
.createStatusViewData(); .createStatusViewData();
NotificationViewData notificationViewData = new NotificationViewData(old.getType(), NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
old.getId(), old.getAccount(), statusViewData); old.getId(), old.getAccount(), statusViewData, false);
notifications.setPairedItem(position, notificationViewData); notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, false); adapter.updateItemWithNotify(position, notificationViewData, false);
} }
@Override
public void onLoadMore(int position) {
//check bounds before accessing list,
if (notifications.size() >= position && position > 0) {
String fromId = notifications.get(position - 1).id;
String toId = notifications.get(position + 1).id;
sendFetchNotificationsRequest(fromId, toId, FetchEnd.MIDDLE, position);
NotificationViewData old = notifications.getPairedItem(position);
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
old.getId(), old.getAccount(), old.getStatusViewData(), true);
notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, false);
} else {
Log.d(TAG, "error loading more");
}
}
@Override @Override
public void onViewTag(String tag) { public void onViewTag(String tag) {
super.viewTag(tag); super.viewTag(tag);
@ -385,7 +401,7 @@ public class NotificationsFragment extends SFragment implements
} }
private void onLoadMore() { private void onLoadMore() {
sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM); sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM, -1);
} }
private void jumpToTop() { private void jumpToTop() {
@ -394,7 +410,7 @@ public class NotificationsFragment extends SFragment implements
} }
private void sendFetchNotificationsRequest(String fromId, String uptoId, private void sendFetchNotificationsRequest(String fromId, String uptoId,
final FetchEnd fetchEnd) { final FetchEnd fetchEnd, final int pos) {
/* If there is a fetch already ongoing, record however many fetches are requested and /* If there is a fetch already ongoing, record however many fetches are requested and
* fulfill them after it's complete. */ * fulfill them after it's complete. */
if (fetchEnd == FetchEnd.TOP && topLoading) { if (fetchEnd == FetchEnd.TOP && topLoading) {
@ -418,7 +434,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>>() { call.enqueue(new Callback<List<Notification>>() {
@Override @Override
@ -426,22 +442,22 @@ public class NotificationsFragment extends SFragment implements
@NonNull Response<List<Notification>> response) { @NonNull Response<List<Notification>> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String linkHeader = response.headers().get("Link"); String linkHeader = response.headers().get("Link");
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd); onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos);
} else { } else {
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd); onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos);
} }
} }
@Override @Override
public void onFailure(@NonNull Call<List<Notification>> call, @NonNull Throwable t) { public void onFailure(@NonNull Call<List<Notification>> call, @NonNull Throwable t) {
onFetchNotificationsFailure((Exception) t, fetchEnd); onFetchNotificationsFailure((Exception) t, fetchEnd, pos);
} }
}); });
callList.add(call); callList.add(call);
} }
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader, private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
FetchEnd fetchEnd) { FetchEnd fetchEnd, int pos) {
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader); List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
switch (fetchEnd) { switch (fetchEnd) {
case TOP: { case TOP: {
@ -453,6 +469,10 @@ public class NotificationsFragment extends SFragment implements
update(notifications, null, uptoId); update(notifications, null, uptoId);
break; break;
} }
case MIDDLE: {
insert(notifications, pos);
break;
}
case BOTTOM: { case BOTTOM: {
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next"); HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
String fromId = null; String fromId = null;
@ -489,8 +509,8 @@ public class NotificationsFragment extends SFragment implements
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
} }
public void update(@Nullable List<Notification> newNotifications, @Nullable String fromId, private void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
@Nullable String uptoId) { @Nullable String uptoId) {
if (ListUtils.isEmpty(newNotifications)) { if (ListUtils.isEmpty(newNotifications)) {
return; return;
} }
@ -501,8 +521,7 @@ public class NotificationsFragment extends SFragment implements
topId = uptoId; topId = uptoId;
} }
if (notifications.isEmpty()) { if (notifications.isEmpty()) {
// This construction removes duplicates while preserving order. notifications.addAll(newNotifications);
notifications.addAll(new LinkedHashSet<>(newNotifications));
} else { } else {
int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1)); int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1));
for (int i = 0; i < index; i++) { for (int i = 0; i < index; i++) {
@ -510,6 +529,11 @@ public class NotificationsFragment extends SFragment implements
} }
int newIndex = newNotifications.indexOf(notifications.get(0)); int newIndex = newNotifications.indexOf(notifications.get(0));
if (newIndex == -1) { if (newIndex == -1) {
if(index == -1 && newNotifications.size() >= LOAD_AT_ONCE) {
Notification placeholder = new Notification();
placeholder.type = Notification.Type.PLACEHOLDER;
newNotifications.add(placeholder);
}
notifications.addAll(0, newNotifications); notifications.addAll(0, newNotifications);
} else { } else {
List<Notification> sublist = newNotifications.subList(0, newIndex); List<Notification> sublist = newNotifications.subList(0, newIndex);
@ -519,7 +543,7 @@ public class NotificationsFragment extends SFragment implements
adapter.update(notifications.getPairedCopy()); 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)) { if (ListUtils.isEmpty(newNotifications)) {
return; return;
} }
@ -532,7 +556,7 @@ public class NotificationsFragment extends SFragment implements
notifications.addAll(newNotifications); notifications.addAll(newNotifications);
List<NotificationViewData> newViewDatas = notifications.getPairedCopy() List<NotificationViewData> newViewDatas = notifications.getPairedCopy()
.subList(notifications.size() - newNotifications.size(), .subList(notifications.size() - newNotifications.size(),
notifications.size() - 1); notifications.size());
adapter.addItems(newViewDatas); adapter.addItems(newViewDatas);
} }
} }
@ -546,8 +570,15 @@ public class NotificationsFragment extends SFragment implements
return false; return false;
} }
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd) { private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) {
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
if(fetchEnd == FetchEnd.MIDDLE && notifications.getPairedItem(position).getType() == Notification.Type.PLACEHOLDER) {
NotificationViewData old = notifications.getPairedItem(position);
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
old.getId(), old.getAccount(), old.getStatusViewData(), false);
notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, true);
}
Log.e(TAG, "Fetch failure: " + exception.getMessage()); Log.e(TAG, "Fetch failure: " + exception.getMessage());
fulfillAnyQueuedFetches(fetchEnd); fulfillAnyQueuedFetches(fetchEnd);
} }
@ -573,9 +604,29 @@ public class NotificationsFragment extends SFragment implements
} }
} }
private void insert(List<Notification> newNotifications, int pos) {
notifications.remove(pos);
if (ListUtils.isEmpty(newNotifications)) {
adapter.update(notifications.getPairedCopy());
return;
}
if(newNotifications.size() >= LOAD_AT_ONCE) {
Notification placeholder = new Notification();
placeholder.type = Notification.Type.PLACEHOLDER;
newNotifications.add(placeholder);
}
notifications.addAll(pos, newNotifications);
adapter.update(notifications.getPairedCopy());
}
private void fullyRefresh() { private void fullyRefresh() {
adapter.clear(); adapter.clear();
notifications.clear(); notifications.clear();
sendFetchNotificationsRequest(null, null, FetchEnd.TOP); sendFetchNotificationsRequest(null, null, FetchEnd.TOP, -1);
} }
} }

View file

@ -21,6 +21,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.LocalBroadcastManager; 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<Relationship> call = mastodonApi.muteAccount(id);
call.enqueue(new Callback<Relationship>() { call.enqueue(new Callback<Relationship>() {
@Override @Override
public void onResponse(Call<Relationship> call, Response<Relationship> response) {} public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {}
@Override @Override
public void onFailure(Call<Relationship> call, Throwable t) {} public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {}
}); });
callList.add(call); callList.add(call);
Intent intent = new Intent(TimelineReceiver.Types.MUTE_ACCOUNT); 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<Relationship> call = mastodonApi.blockAccount(id);
call.enqueue(new Callback<Relationship>() { call.enqueue(new Callback<Relationship>() {
@Override @Override
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {} public void onResponse(@NonNull Call<Relationship> call, @NonNull retrofit2.Response<Relationship> response) {}
@Override @Override
public void onFailure(Call<Relationship> call, Throwable t) {} public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {}
}); });
callList.add(call); callList.add(call);
Intent intent = new Intent(TimelineReceiver.Types.BLOCK_ACCOUNT); 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<ResponseBody> call = mastodonApi.deleteStatus(id);
call.enqueue(new Callback<ResponseBody>() { call.enqueue(new Callback<ResponseBody>() {
@Override @Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {} public void onResponse(@NonNull Call<ResponseBody> call, @NonNull retrofit2.Response<ResponseBody> response) {}
@Override @Override
public void onFailure(Call<ResponseBody> call, Throwable t) {} public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {}
}); });
callList.add(call); callList.add(call);
} }

View file

@ -20,6 +20,7 @@ import android.content.SharedPreferences;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.TabLayout; import android.support.design.widget.TabLayout;
@ -51,7 +52,6 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -63,10 +63,12 @@ public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, SwipeRefreshLayout.OnRefreshListener,
StatusActionListener, StatusActionListener,
SharedPreferences.OnSharedPreferenceChangeListener { 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 KIND_ARG = "kind";
private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id"; private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id";
private static final int LOAD_AT_ONCE = 30;
public enum Kind { public enum Kind {
HOME, HOME,
PUBLIC_LOCAL, PUBLIC_LOCAL,
@ -79,6 +81,7 @@ public class TimelineFragment extends SFragment implements
private enum FetchEnd { private enum FetchEnd {
TOP, TOP,
BOTTOM, BOTTOM,
MIDDLE
} }
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
@ -178,12 +181,10 @@ public class TimelineFragment extends SFragment implements
TabLayout layout = getActivity().findViewById(R.id.tab_layout); TabLayout layout = getActivity().findViewById(R.id.tab_layout);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() { onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override @Override
public void onTabSelected(TabLayout.Tab tab) { public void onTabSelected(TabLayout.Tab tab) {}
}
@Override @Override
public void onTabUnselected(TabLayout.Tab tab) { public void onTabUnselected(TabLayout.Tab tab) {}
}
@Override @Override
public void onTabReselected(TabLayout.Tab tab) { public void onTabReselected(TabLayout.Tab tab) {
@ -218,7 +219,7 @@ public class TimelineFragment extends SFragment implements
} else if (!composeButton.isShown()) { } else if (!composeButton.isShown()) {
composeButton.show(); composeButton.show();
} }
} }
} }
@Override @Override
@ -250,7 +251,7 @@ public class TimelineFragment extends SFragment implements
@Override @Override
public void onRefresh() { public void onRefresh() {
sendFetchTimelineRequest(null, topId, FetchEnd.TOP); sendFetchTimelineRequest(null, topId, FetchEnd.TOP, -1);
} }
@Override @Override
@ -263,7 +264,7 @@ public class TimelineFragment extends SFragment implements
final Status status = statuses.get(position); final Status status = statuses.get(position);
super.reblogWithCallback(status, reblog, new Callback<Status>() { super.reblogWithCallback(status, reblog, new Callback<Status>() {
@Override @Override
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) { public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
status.reblogged = reblog; status.reblogged = reblog;
@ -280,9 +281,8 @@ public class TimelineFragment extends SFragment implements
} }
@Override @Override
public void onFailure(Call<Status> call, Throwable t) { public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
Log.d(TAG, "Failed to reblog status " + status.id); Log.d(TAG, "Failed to reblog status " + status.id, t);
t.printStackTrace();
} }
}); });
} }
@ -293,7 +293,7 @@ public class TimelineFragment extends SFragment implements
super.favouriteWithCallback(status, favourite, new Callback<Status>() { super.favouriteWithCallback(status, favourite, new Callback<Status>() {
@Override @Override
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) { public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
status.favourited = favourite; status.favourited = favourite;
@ -310,9 +310,8 @@ public class TimelineFragment extends SFragment implements
} }
@Override @Override
public void onFailure(Call<Status> call, Throwable t) { public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
Log.d(TAG, "Failed to favourite status " + status.id); Log.d(TAG, "Failed to favourite status " + status.id, t);
t.printStackTrace();
} }
}); });
} }
@ -343,6 +342,24 @@ public class TimelineFragment extends SFragment implements
adapter.changeItem(position, newViewData, false); adapter.changeItem(position, newViewData, false);
} }
@Override
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);
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
.setPlaceholderLoading(true).createStatusViewData();
statuses.setPairedItem(position, newViewData);
adapter.changeItem(position, newViewData, false);
} else {
Log.d(TAG, "error loading more");
}
}
@Override @Override
public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type, public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type,
View view) { View view) {
@ -427,12 +444,12 @@ public class TimelineFragment extends SFragment implements
} }
private void onLoadMore() { private void onLoadMore() {
sendFetchTimelineRequest(bottomId, null, FetchEnd.BOTTOM); sendFetchTimelineRequest(bottomId, null, FetchEnd.BOTTOM, -1);
} }
private void fullyRefresh() { private void fullyRefresh() {
adapter.clear(); adapter.clear();
sendFetchTimelineRequest(null, null, FetchEnd.TOP); sendFetchTimelineRequest(null, null, FetchEnd.TOP, -1);
} }
private boolean jumpToTopAllowed() { private boolean jumpToTopAllowed() {
@ -456,20 +473,20 @@ public class TimelineFragment extends SFragment implements
case HOME: case HOME:
return api.homeTimeline(fromId, uptoId, null); return api.homeTimeline(fromId, uptoId, null);
case PUBLIC_FEDERATED: case PUBLIC_FEDERATED:
return api.publicTimeline(null, fromId, uptoId, null); return api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE);
case PUBLIC_LOCAL: case PUBLIC_LOCAL:
return api.publicTimeline(true, fromId, uptoId, null); return api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE);
case TAG: case TAG:
return api.hashtagTimeline(tagOrId, null, fromId, uptoId, null); return api.hashtagTimeline(tagOrId, null, fromId, uptoId, LOAD_AT_ONCE);
case USER: case USER:
return api.accountStatuses(tagOrId, fromId, uptoId, null); return api.accountStatuses(tagOrId, fromId, uptoId, LOAD_AT_ONCE);
case FAVOURITES: 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, 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 /* If there is a fetch already ongoing, record however many fetches are requested and
* fulfill them after it's complete. */ * fulfill them after it's complete. */
if (fetchEnd == FetchEnd.TOP && topLoading) { if (fetchEnd == FetchEnd.TOP && topLoading) {
@ -495,18 +512,18 @@ public class TimelineFragment extends SFragment implements
Callback<List<Status>> callback = new Callback<List<Status>>() { Callback<List<Status>> callback = new Callback<List<Status>>() {
@Override @Override
public void onResponse(Call<List<Status>> call, Response<List<Status>> response) { public void onResponse(@NonNull Call<List<Status>> call, @NonNull Response<List<Status>> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String linkHeader = response.headers().get("Link"); String linkHeader = response.headers().get("Link");
onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd); onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd, pos);
} else { } else {
onFetchTimelineFailure(new Exception(response.message()), fetchEnd); onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos);
} }
} }
@Override @Override
public void onFailure(Call<List<Status>> call, Throwable t) { public void onFailure(@NonNull Call<List<Status>> call, @NonNull Throwable t) {
onFetchTimelineFailure((Exception) t, fetchEnd); onFetchTimelineFailure((Exception) t, fetchEnd, pos);
} }
}; };
@ -515,8 +532,9 @@ public class TimelineFragment extends SFragment implements
listCall.enqueue(callback); listCall.enqueue(callback);
} }
public void onFetchTimelineSuccess(List<Status> statuses, String linkHeader, private void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
FetchEnd fetchEnd) { FetchEnd fetchEnd, int pos) {
boolean fullFetch = statuses.size() >= LOAD_AT_ONCE;
filterStatuses(statuses); filterStatuses(statuses);
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader); List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
switch (fetchEnd) { switch (fetchEnd) {
@ -526,7 +544,11 @@ public class TimelineFragment extends SFragment implements
if (previous != null) { if (previous != null) {
uptoId = previous.uri.getQueryParameter("since_id"); uptoId = previous.uri.getQueryParameter("since_id");
} }
updateStatuses(statuses, null, uptoId); updateStatuses(statuses, null, uptoId, fullFetch);
break;
}
case MIDDLE: {
insertStatuses(statuses,fullFetch, pos);
break; break;
} }
case BOTTOM: { case BOTTOM: {
@ -546,7 +568,7 @@ public class TimelineFragment extends SFragment implements
if (previous != null) { if (previous != null) {
uptoId = previous.uri.getQueryParameter("since_id"); uptoId = previous.uri.getQueryParameter("since_id");
} }
updateStatuses(statuses, fromId, uptoId); updateStatuses(statuses, fromId, uptoId, fullFetch);
} }
break; break;
} }
@ -560,8 +582,17 @@ public class TimelineFragment extends SFragment implements
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
} }
public void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd) { private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
if(fetchEnd == FetchEnd.MIDDLE && statuses.getPairedItem(position).isPlaceholder()) {
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(position))
.setPlaceholderLoading(false).createStatusViewData();
statuses.setPairedItem(position, newViewData);
adapter.changeItem(position, newViewData, true);
}
Log.e(TAG, "Fetch Failure: " + exception.getMessage()); Log.e(TAG, "Fetch Failure: " + exception.getMessage());
fulfillAnyQueuedFetches(fetchEnd); fulfillAnyQueuedFetches(fetchEnd);
} }
@ -587,7 +618,7 @@ public class TimelineFragment extends SFragment implements
} }
} }
protected void filterStatuses(List<Status> statuses) { private void filterStatuses(List<Status> statuses) {
Iterator<Status> it = statuses.iterator(); Iterator<Status> it = statuses.iterator();
while (it.hasNext()) { while (it.hasNext()) {
Status status = it.next(); Status status = it.next();
@ -599,7 +630,7 @@ public class TimelineFragment extends SFragment implements
} }
private void updateStatuses(List<Status> newStatuses, @Nullable String fromId, private void updateStatuses(List<Status> newStatuses, @Nullable String fromId,
@Nullable String toId) { @Nullable String toId, boolean fullFetch) {
if (ListUtils.isEmpty(newStatuses)) { if (ListUtils.isEmpty(newStatuses)) {
return; return;
} }
@ -610,16 +641,21 @@ public class TimelineFragment extends SFragment implements
topId = toId; topId = toId;
} }
if (statuses.isEmpty()) { if (statuses.isEmpty()) {
// This construction removes duplicates while preserving order. statuses.addAll(newStatuses);
statuses.addAll(new LinkedHashSet<>(newStatuses));
} else { } else {
Status lastOfNew = newStatuses.get(newStatuses.size() - 1); Status lastOfNew = newStatuses.get(newStatuses.size() - 1);
int index = statuses.indexOf(lastOfNew); int index = statuses.indexOf(lastOfNew);
for (int i = 0; i < index; i++) { for (int i = 0; i < index; i++) {
statuses.remove(0); statuses.remove(0);
} }
int newIndex = newStatuses.indexOf(statuses.get(0)); int newIndex = newStatuses.indexOf(statuses.get(0));
if (newIndex == -1) { if (newIndex == -1) {
if(index == -1 && fullFetch) {
Status placeholder = new Status();
placeholder.placeholder = true;
newStatuses.add(placeholder);
}
statuses.addAll(0, newStatuses); statuses.addAll(0, newStatuses);
} else { } else {
statuses.addAll(0, newStatuses.subList(0, newIndex)); statuses.addAll(0, newStatuses.subList(0, newIndex));
@ -641,7 +677,7 @@ public class TimelineFragment extends SFragment implements
if (BuildConfig.DEBUG && newStatuses.size() != newViewDatas.size()) { if (BuildConfig.DEBUG && newStatuses.size() != newViewDatas.size()) {
String error = String.format(Locale.getDefault(), String error = String.format(Locale.getDefault(),
"Incorrectly got statusViewData sublist." + "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()); newStatuses.size(), newViewDatas.size(), statuses.size());
throw new AssertionError(error); throw new AssertionError(error);
} }
@ -652,6 +688,28 @@ public class TimelineFragment extends SFragment implements
} }
} }
private void insertStatuses(List<Status> newStatuses, boolean fullFetch, int pos) {
if(statuses.get(pos).placeholder) {
statuses.remove(pos);
}
if (ListUtils.isEmpty(newStatuses)) {
adapter.update(statuses.getPairedCopy());
return;
}
if(fullFetch) {
Status placeholder = new Status();
placeholder.placeholder = true;
newStatuses.add(placeholder);
}
statuses.addAll(pos, newStatuses);
adapter.update(statuses.getPairedCopy());
}
private static boolean findStatus(List<Status> statuses, String id) { private static boolean findStatus(List<Status> statuses, String id) {
for (Status status : statuses) { for (Status status : statuses) {
if (status.id.equals(id)) { if (status.id.equals(id)) {

View file

@ -243,6 +243,11 @@ public class ViewThreadFragment extends SFragment implements
adapter.setItem(position, newViewData, false); adapter.setItem(position, newViewData, false);
} }
@Override
public void onLoadMore(int pos) {
}
@Override @Override
public void onViewTag(String tag) { public void onViewTag(String tag) {
super.viewTag(tag); super.viewTag(tag);

View file

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

View file

@ -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; package com.keylesspalace.tusky.util;
import android.arch.core.util.Function; import android.arch.core.util.Function;
@ -19,6 +34,11 @@ public final class ViewDataUtils {
@Nullable @Nullable
public static StatusViewData statusToViewData(@Nullable Status status) { public static StatusViewData statusToViewData(@Nullable Status status) {
if (status == null) return null; 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; Status visibleStatus = status.reblog == null ? status : status.reblog;
return new StatusViewData.Builder().setId(status.id) return new StatusViewData.Builder().setId(status.id)
.setAttachments(visibleStatus.attachments) .setAttachments(visibleStatus.attachments)
@ -61,7 +81,7 @@ public final class ViewDataUtils {
public static NotificationViewData notificationToViewData(Notification notification) { public static NotificationViewData notificationToViewData(Notification notification) {
return new NotificationViewData(notification.type, notification.id, notification.account, return new NotificationViewData(notification.type, notification.id, notification.account,
statusToViewData(notification.status)); statusToViewData(notification.status), false);
} }
public static List<NotificationViewData> notificationListToViewDataList( public static List<NotificationViewData> notificationListToViewDataList(

View file

@ -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; package com.keylesspalace.tusky.viewdata;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
@ -12,13 +27,15 @@ public final class NotificationViewData {
private final String id; private final String id;
private final Account account; private final Account account;
private final StatusViewData statusViewData; private final StatusViewData statusViewData;
private final boolean placeholderLoading;
public NotificationViewData(Notification.Type type, String id, Account account, public NotificationViewData(Notification.Type type, String id, Account account,
StatusViewData statusViewData) { StatusViewData statusViewData, boolean placeholderLoading) {
this.type = type; this.type = type;
this.id = id; this.id = id;
this.account = account; this.account = account;
this.statusViewData = statusViewData; this.statusViewData = statusViewData;
this.placeholderLoading = placeholderLoading;
} }
public Notification.Type getType() { public Notification.Type getType() {
@ -36,4 +53,8 @@ public final class NotificationViewData {
public StatusViewData getStatusViewData() { public StatusViewData getStatusViewData() {
return statusViewData; return statusViewData;
} }
public boolean isPlaceholderLoading() {
return placeholderLoading;
}
} }

View file

@ -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; package com.keylesspalace.tusky.viewdata;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -48,13 +63,18 @@ public final class StatusViewData {
@Nullable @Nullable
private final Card card; private final Card card;
private final boolean placeholder;
private final boolean placeholderLoading;
public StatusViewData(String id, Spanned content, boolean reblogged, boolean favourited, public StatusViewData(String id, Spanned content, boolean reblogged, boolean favourited,
String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments, @Nullable String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments,
String rebloggedByUsername, String rebloggedAvatar, boolean sensitive, boolean isExpanded, @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded,
boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar, boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar,
Date createdAt, String reblogsCount, String favouritesCount, String inReplyToId, Date createdAt, String reblogsCount, String favouritesCount, @Nullable String inReplyToId,
Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
Status.Application application, List<Status.Emoji> emojis, Card card) { Status.Application application, List<Status.Emoji> emojis, @Nullable Card card,
boolean placeholder, boolean placeholderLoading) {
this.id = id; this.id = id;
this.content = content; this.content = content;
this.reblogged = reblogged; this.reblogged = reblogged;
@ -80,6 +100,8 @@ public final class StatusViewData {
this.application = application; this.application = application;
this.emojis = emojis; this.emojis = emojis;
this.card = card; this.card = card;
this.placeholder = placeholder;
this.placeholderLoading = placeholderLoading;
} }
public String getId() { public String getId() {
@ -183,10 +205,19 @@ public final class StatusViewData {
return emojis; return emojis;
} }
@Nullable
public Card getCard() { public Card getCard() {
return card; return card;
} }
public boolean isPlaceholder() {
return placeholder;
}
public boolean isPlaceholderLoading() {
return placeholderLoading;
}
public static class Builder { public static class Builder {
private String id; private String id;
private Spanned content; private Spanned content;
@ -213,6 +244,8 @@ public final class StatusViewData {
private Status.Application application; private Status.Application application;
private List<Status.Emoji> emojis; private List<Status.Emoji> emojis;
private Card card; private Card card;
private boolean placeholder;
private boolean placeholderLoading;
public Builder() { public Builder() {
} }
@ -243,6 +276,8 @@ public final class StatusViewData {
application = viewData.application; application = viewData.application;
emojis = viewData.getEmojis(); emojis = viewData.getEmojis();
card = viewData.getCard(); card = viewData.getCard();
placeholder = viewData.isPlaceholder();
placeholderLoading = viewData.isPlaceholderLoading();
} }
@ -371,12 +406,25 @@ public final class StatusViewData {
return this; 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 createStatusViewData() {
if (this.emojis == null) emojis = Collections.emptyList(); 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(id, content, reblogged, favourited, spoilerText, visibility,
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, reblogsCount, isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, reblogsCount,
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, emojis, card); favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,
emojis, card, placeholder, placeholderLoading);
} }
} }
} }

View file

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

View file

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