Make more clear representation of placeholder in notifications

main
charlag 7 years ago committed by Konrad Pozniak
parent cbf6062bce
commit 33ece0410d
  1. 65
      app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
  2. 1
      app/src/main/java/com/keylesspalace/tusky/entity/Notification.java
  3. 164
      app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
  4. 38
      app/src/main/java/com/keylesspalace/tusky/util/CollectionUtil.java
  5. 125
      app/src/main/java/com/keylesspalace/tusky/util/Either.java
  6. 6
      app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java
  7. 74
      app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java

@ -101,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 status = concreteNotificaton.getStatusViewData();
holder.setupWithStatus(status,
statusListener, mediaPreviewEnabled);
break;
@ -113,23 +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);
break;
}
case PLACEHOLDER: {
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
holder.setup(!notification.isPlaceholderLoading(), statusListener);
holder.setMessage(concreteNotificaton.getAccount().getDisplayName(),
concreteNotificaton.getAccount().username, concreteNotificaton.getAccount().avatar);
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().id);
break;
}
}
@ -150,21 +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;
}
case PLACEHOLDER: {
return VIEW_TYPE_PLACEHOLDER;
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");
}
}
}

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

@ -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;
@ -74,6 +76,21 @@ public class NotificationsFragment extends SFragment implements
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;
private LinearLayoutManager layoutManager;
private RecyclerView recyclerView;
@ -89,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);
}
}
});
@ -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
@ -225,13 +248,12 @@ public class NotificationsFragment extends SFragment implements
@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);
@ -260,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
@ -272,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);
@ -289,7 +314,7 @@ public class NotificationsFragment extends SFragment implements
@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);
}
@ -301,38 +326,40 @@ 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);
NotificationViewData.Concrete old =
(NotificationViewData.Concrete) notifications.getPairedItem(position);
StatusViewData statusViewData =
new StatusViewData.Builder(old.getStatusViewData())
.setIsExpanded(expanded)
.createStatusViewData();
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
old.getId(), old.getAccount(), statusViewData, false);
NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(),
old.getId(), old.getAccount(), statusViewData);
notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, false);
}
@Override
public void onContentHiddenChange(boolean isShowing, int position) {
NotificationViewData old = notifications.getPairedItem(position);
NotificationViewData.Concrete old =
(NotificationViewData.Concrete) notifications.getPairedItem(position);
StatusViewData statusViewData =
new StatusViewData.Builder(old.getStatusViewData())
.setIsShowingSensitiveContent(isShowing)
.createStatusViewData();
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
old.getId(), old.getAccount(), statusViewData, false);
NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(),
old.getId(), old.getAccount(), statusViewData);
notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, false);
}
@ -341,13 +368,12 @@ public class NotificationsFragment extends SFragment implements
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;
// is it safe?
String fromId = notifications.get(position - 1).getAsRight().id;
String toId = notifications.get(position + 1).getAsRight().id;
sendFetchNotificationsRequest(fromId, toId, FetchEnd.MIDDLE, position);
NotificationViewData old = notifications.getPairedItem(position);
NotificationViewData notificationViewData = new NotificationViewData(old.getType(),
old.getId(), old.getAccount(), old.getStatusViewData(), true);
NotificationViewData notificationViewData =
new NotificationViewData.Placeholder(true);
notifications.setPairedItem(position, notificationViewData);
adapter.updateItemWithNotify(position, notificationViewData, false);
} else {
@ -390,10 +416,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();
}
}
@ -470,7 +497,7 @@ public class NotificationsFragment extends SFragment implements
break;
}
case MIDDLE: {
insert(notifications, pos);
replacePlaceholderWithNotifications(notifications, pos);
break;
}
case BOTTOM: {
@ -520,24 +547,25 @@ public class NotificationsFragment extends SFragment implements
if (uptoId != null) {
topId = uptoId;
}
List<Either<Placeholder, Notification>> liftedNew =
liftNotificationList(newNotifications);
if (notifications.isEmpty()) {
notifications.addAll(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) {
if(index == -1 && newNotifications.size() >= LOAD_AT_ONCE) {
Notification placeholder = new Notification();
placeholder.type = Notification.Type.PLACEHOLDER;
newNotifications.add(placeholder);
if (index == -1 && liftedNew.size() >= LOAD_AT_ONCE) {
liftedNew.add(Either.<Placeholder, Notification>left(Placeholder.getInstance()));
}
notifications.addAll(0, newNotifications);
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());
@ -551,9 +579,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());
@ -561,23 +590,14 @@ 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, int position) {
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);
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);
@ -604,8 +624,8 @@ public class NotificationsFragment extends SFragment implements
}
}
private void insert(List<Notification> newNotifications, int pos) {
private void replacePlaceholderWithNotifications(List<Notification> newNotifications, int pos) {
// Remove placeholder
notifications.remove(pos);
if (ListUtils.isEmpty(newNotifications)) {
@ -613,15 +633,29 @@ public class NotificationsFragment extends SFragment implements
return;
}
if(newNotifications.size() >= LOAD_AT_ONCE) {
Notification placeholder = new Notification();
placeholder.type = Notification.Type.PLACEHOLDER;
newNotifications.add(placeholder);
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, newNotifications);
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() {

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

@ -34,7 +34,7 @@ public final class ViewDataUtils {
@Nullable
public static StatusViewData statusToViewData(@Nullable Status status) {
if (status == null) return null;
if(status.placeholder) {
if (status.placeholder) {
return new StatusViewData.Builder().setId(status.id)
.setPlaceholder(true)
.createStatusViewData();
@ -80,8 +80,8 @@ public final class ViewDataUtils {
}
public static NotificationViewData notificationToViewData(Notification notification) {
return new NotificationViewData(notification.type, notification.id, notification.account,
statusToViewData(notification.status), false);
return new NotificationViewData.Concrete(notification.type, notification.id, notification.account,
statusToViewData(notification.status));
}
public static List<NotificationViewData> notificationListToViewDataList(

@ -20,41 +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;
private final boolean placeholderLoading;
public NotificationViewData(Notification.Type type, String id, Account account,
StatusViewData statusViewData, boolean placeholderLoading) {
this.type = type;
this.id = id;
this.account = account;
this.statusViewData = statusViewData;
this.placeholderLoading = placeholderLoading;
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 statusViewData;
public String getId() {
return id;
}
public Concrete(Notification.Type type, String id, Account account,
StatusViewData statusViewData) {
this.type = type;
this.id = id;
this.account = account;
this.statusViewData = statusViewData;
}
public Account getAccount() {
return account;
}
public Notification.Type getType() {
return type;
}
public StatusViewData getStatusViewData() {
return statusViewData;
public String getId() {
return id;
}
public Account getAccount() {
return account;
}
public StatusViewData getStatusViewData() {
return statusViewData;
}
}
public boolean isPlaceholderLoading() {
return placeholderLoading;
public static final class Placeholder extends NotificationViewData {
private final boolean isLoading;
public Placeholder(boolean isLoading) {
this.isLoading = isLoading;
}
public boolean isLoading() {
return isLoading;
}
}
}

Loading…
Cancel
Save