Merge branch '879-remove-thread-collapsing-unread-count' into 'master'

Remove code for collapsing threads and for reply count

Besides removing lots of code, this MR also improves the encapsulation between adapter and view holders.

Closes #478, #502, #526, #682,  #683,  #835,  #836

See merge request !477
This commit is contained in:
Torsten Grote
2017-01-04 13:00:16 +00:00
21 changed files with 160 additions and 479 deletions

View File

@@ -100,7 +100,7 @@ public class ForumActivity extends
super.onActivityResult(request, result, data); super.onActivityResult(request, result, data);
if (request == REQUEST_SHARE_FORUM && result == RESULT_OK) { if (request == REQUEST_SHARE_FORUM && result == RESULT_OK) {
displaySnackbarShort(R.string.forum_shared_snackbar); displaySnackbar(R.string.forum_shared_snackbar);
} }
} }

View File

@@ -183,7 +183,7 @@ public class GroupActivity extends
@Override @Override
protected void onActivityResult(int request, int result, Intent data) { protected void onActivityResult(int request, int result, Intent data) {
if (request == REQUEST_GROUP_INVITE && result == RESULT_OK) { if (request == REQUEST_GROUP_INVITE && result == RESULT_OK) {
displaySnackbarShort(R.string.groups_invitation_sent); displaySnackbar(R.string.groups_invitation_sent);
} else super.onActivityResult(request, result, data); } else super.onActivityResult(request, result, data);
} }

View File

@@ -31,9 +31,8 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
@LayoutRes @LayoutRes
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
GroupMessageItem item = getVisibleItem(position); GroupMessageItem item = items.get(position);
if (item != null) return item.getLayout(); return item.getLayout();
return R.layout.list_item_thread;
} }
@Override @Override
@@ -58,7 +57,7 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
GroupMessageItem item = items.get(position); GroupMessageItem item = items.get(position);
if (item instanceof JoinMessageItem) { if (item instanceof JoinMessageItem) {
((JoinMessageItem) item).setVisibility(v); ((JoinMessageItem) item).setVisibility(v);
notifyItemChanged(getVisiblePos(item), item); notifyItemChanged(findItemPosition(item), item);
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.privategroup.conversation; package org.briarproject.briar.android.privategroup.conversation;
import android.support.annotation.LayoutRes; import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Author;
@@ -20,9 +21,8 @@ class GroupMessageItem extends ThreadItem {
private final GroupId groupId; private final GroupId groupId;
private GroupMessageItem(MessageId messageId, GroupId groupId, private GroupMessageItem(MessageId messageId, GroupId groupId,
MessageId parentId, @Nullable MessageId parentId, String text, long timestamp,
String text, long timestamp, Author author, Status status, Author author, Status status, boolean isRead) {
boolean isRead) {
super(messageId, parentId, text, timestamp, author, status, isRead); super(messageId, parentId, text, timestamp, author, status, isRead);
this.groupId = groupId; this.groupId = groupId;
} }

View File

@@ -27,11 +27,6 @@ class JoinMessageItem extends GroupMessageItem {
return 0; return 0;
} }
@Override
public boolean hasDescendants() {
return false;
}
@Override @Override
@LayoutRes @LayoutRes
public int getLayout() { public int getLayout() {

View File

@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity; import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
import org.briarproject.briar.android.threaded.BaseThreadItemViewHolder; import org.briarproject.briar.android.threaded.BaseThreadItemViewHolder;
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener; import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
import static org.briarproject.bramble.api.identity.Author.Status.OURSELVES; import static org.briarproject.bramble.api.identity.Author.Status.OURSELVES;
@@ -41,10 +40,9 @@ class JoinMessageItemViewHolder
} }
@Override @Override
public void bind(ThreadItemAdapter<GroupMessageItem> adapter, public void bind(GroupMessageItem item,
ThreadItemListener<GroupMessageItem> listener, ThreadItemListener<GroupMessageItem> listener) {
GroupMessageItem item, int pos) { super.bind(item, listener);
super.bind(adapter, listener, item, pos);
if (isCreator) bindForCreator((JoinMessageItem) item); if (isCreator) bindForCreator((JoinMessageItem) item);
else bind((JoinMessageItem) item); else bind((JoinMessageItem) item);

View File

@@ -20,9 +20,6 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener; import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
import org.briarproject.briar.android.view.AuthorView; import org.briarproject.briar.android.view.AuthorView;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@UiThread @UiThread
@NotNullByDefault @NotNullByDefault
public abstract class BaseThreadItemViewHolder<I extends ThreadItem> public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
@@ -33,7 +30,6 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
protected final TextView textView; protected final TextView textView;
private final ViewGroup layout; private final ViewGroup layout;
private final AuthorView author; private final AuthorView author;
private final View topDivider;
public BaseThreadItemViewHolder(View v) { public BaseThreadItemViewHolder(View v) {
super(v); super(v);
@@ -41,43 +37,30 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
layout = (ViewGroup) v.findViewById(R.id.layout); layout = (ViewGroup) v.findViewById(R.id.layout);
textView = (TextView) v.findViewById(R.id.text); textView = (TextView) v.findViewById(R.id.text);
author = (AuthorView) v.findViewById(R.id.author); author = (AuthorView) v.findViewById(R.id.author);
topDivider = v.findViewById(R.id.top_divider);
} }
// TODO improve encapsulation, so we don't need to pass the adapter here
@CallSuper @CallSuper
public void bind(final ThreadItemAdapter<I> adapter, public void bind(final I item, final ThreadItemListener<I> listener) {
final ThreadItemListener<I> listener, final I item, int pos) {
textView.setText(StringUtils.trim(item.getText())); textView.setText(StringUtils.trim(item.getText()));
if (pos == 0) {
topDivider.setVisibility(INVISIBLE);
} else {
topDivider.setVisibility(VISIBLE);
}
author.setAuthor(item.getAuthor()); author.setAuthor(item.getAuthor());
author.setDate(item.getTimestamp()); author.setDate(item.getTimestamp());
author.setAuthorStatus(item.getStatus()); author.setAuthorStatus(item.getStatus());
if (item.equals(adapter.getReplyItem())) { if (item.isHighlighted()) {
layout.setActivated(true); layout.setActivated(true);
} else if (!item.isRead()) { } else if (!item.isRead()) {
layout.setActivated(true); layout.setActivated(true);
animateFadeOut(adapter, item); animateFadeOut();
listener.onUnreadItemVisible(item); listener.onUnreadItemVisible(item);
} else { } else {
layout.setActivated(false); layout.setActivated(false);
} }
} }
private void animateFadeOut(final ThreadItemAdapter<I> adapter, private void animateFadeOut() {
final I addedItem) {
setIsRecyclable(false); setIsRecyclable(false);
ValueAnimator anim = new ValueAnimator(); ValueAnimator anim = new ValueAnimator();
adapter.addAnimatingItem(addedItem, anim);
ColorDrawable viewColor = new ColorDrawable(ContextCompat ColorDrawable viewColor = new ColorDrawable(ContextCompat
.getColor(getContext(), R.color.forum_cell_highlight)); .getColor(getContext(), R.color.forum_cell_highlight));
anim.setIntValues(viewColor.getColor(), ContextCompat anim.setIntValues(viewColor.getColor(), ContextCompat
@@ -94,7 +77,6 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
R.drawable.list_item_thread_background); R.drawable.list_item_thread_background);
layout.setActivated(false); layout.setActivated(false);
setIsRecyclable(true); setIsRecyclable(true);
adapter.removeAnimatingItem(addedItem);
} }
@Override @Override
public void onAnimationCancel(Animator animation) { public void onAnimationCancel(Animator animation) {

View File

@@ -23,9 +23,7 @@ public abstract class ThreadItem implements MessageNode {
private final Author author; private final Author author;
private final Status status; private final Status status;
private int level = UNDEFINED; private int level = UNDEFINED;
private boolean isShowingDescendants = true; private boolean isRead, highlighted;
private int descendantCount = 0;
private boolean isRead;
public ThreadItem(MessageId messageId, @Nullable MessageId parentId, public ThreadItem(MessageId messageId, @Nullable MessageId parentId,
String text, long timestamp, Author author, Status status, String text, long timestamp, Author author, Status status,
@@ -37,6 +35,7 @@ public abstract class ThreadItem implements MessageNode {
this.author = author; this.author = author;
this.status = status; this.status = status;
this.isRead = isRead; this.isRead = isRead;
this.highlighted = false;
} }
public String getText() { public String getText() {
@@ -71,19 +70,11 @@ public abstract class ThreadItem implements MessageNode {
return status; return status;
} }
public boolean isShowingDescendants() {
return isShowingDescendants;
}
@Override @Override
public void setLevel(int level) { public void setLevel(int level) {
this.level = level; this.level = level;
} }
public void setShowingDescendants(boolean showingDescendants) {
this.isShowingDescendants = showingDescendants;
}
public boolean isRead() { public boolean isRead() {
return isRead; return isRead;
} }
@@ -92,13 +83,12 @@ public abstract class ThreadItem implements MessageNode {
isRead = read; isRead = read;
} }
public boolean hasDescendants() { public void setHighlighted(boolean highlighted) {
return descendantCount > 0; this.highlighted = highlighted;
} }
@Override public boolean isHighlighted() {
public void setDescendantCount(int descendantCount) { return highlighted;
this.descendantCount = descendantCount;
} }
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.threaded; package org.briarproject.briar.android.threaded;
import android.animation.ValueAnimator;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
@@ -13,14 +12,11 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.util.VersionedAdapter; import org.briarproject.briar.android.util.VersionedAdapter;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static android.support.v7.widget.RecyclerView.NO_POSITION; import static android.support.v7.widget.RecyclerView.NO_POSITION;
@UiThread
public class ThreadItemAdapter<I extends ThreadItem> public class ThreadItemAdapter<I extends ThreadItem>
extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>> extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>>
implements VersionedAdapter { implements VersionedAdapter {
@@ -28,13 +24,9 @@ public class ThreadItemAdapter<I extends ThreadItem>
static final int UNDEFINED = -1; static final int UNDEFINED = -1;
protected final NestedTreeList<I> items = new NestedTreeList<>(); protected final NestedTreeList<I> items = new NestedTreeList<>();
private final Map<I, ValueAnimator> animatingItems = new HashMap<>();
private final ThreadItemListener<I> listener; private final ThreadItemListener<I> listener;
private final LinearLayoutManager layoutManager; private final LinearLayoutManager layoutManager;
@Nullable
private I replyItem;
private volatile int revision = 0; private volatile int revision = 0;
public ThreadItemAdapter(ThreadItemListener<I> listener, public ThreadItemAdapter(ThreadItemListener<I> listener,
@@ -53,24 +45,23 @@ public class ThreadItemAdapter<I extends ThreadItem>
@Override @Override
public void onBindViewHolder(BaseThreadItemViewHolder<I> ui, int position) { public void onBindViewHolder(BaseThreadItemViewHolder<I> ui, int position) {
I item = getVisibleItem(position); I item = items.get(position);
if (item == null) return; ui.bind(item, listener);
ui.bind(this, listener, item, position);
} }
/**
* Contrary to the super class adapter,
* this returns the number of <b>visible</b> items,
* not all items in the dataset.
*/
@Override @Override
public int getItemCount() { public int getItemCount() {
return getVisiblePos(null); return items.size();
} }
@Nullable @Override
I getReplyItem() { public int getRevision() {
return replyItem; return revision;
}
@Override
public void incrementRevision() {
revision++;
} }
public void setItems(Collection<I> items) { public void setItems(Collection<I> items) {
@@ -81,233 +72,51 @@ public class ThreadItemAdapter<I extends ThreadItem>
public void add(I item) { public void add(I item) {
items.add(item); items.add(item);
if (item.getParentId() == null) { notifyItemInserted(findItemPosition(item));
notifyItemInserted(getVisiblePos(item));
} else {
// Try to find the item's parent and perform the proper ui update if
// it's present and visible.
for (int i = items.indexOf(item) - 1; i >= 0; i--) {
I higherItem = items.get(i);
if (higherItem.getLevel() < item.getLevel()) {
// parent found
if (higherItem.isShowingDescendants()) {
int parentVisiblePos = getVisiblePos(higherItem);
if (parentVisiblePos != NO_POSITION) {
// parent is visible, we need to update its ui
notifyItemChanged(parentVisiblePos);
// new item insert ui
int visiblePos = getVisiblePos(item);
notifyItemInserted(visiblePos);
break;
}
} else {
// do not show the new item if its parent is not showing
// descendants (this can be overridden by the user by
// pressing the snack bar)
break;
}
}
}
}
} }
void scrollTo(I item) { @Nullable
int visiblePos = getVisiblePos(item); public I getItemAt(int position) {
MessageId parentId = item.getParentId(); if (position == NO_POSITION || position >= items.size()) {
if (visiblePos == NO_POSITION && parentId != null) { return null;
// The item is not visible due to being hidden by its parent item.
// Find the parent and make it visible and traverse up the parent
// chain if necessary to make the item visible
for (int i = items.indexOf(item) - 1; i >= 0; i--) {
I higherItem = items.get(i);
if (higherItem.getId().equals(parentId)) {
// parent found
showDescendants(higherItem);
int parentPos = getVisiblePos(higherItem);
if (parentPos != NO_POSITION) {
// parent or ancestor is visible, item's visibility
// is ensured
notifyItemChanged(parentPos);
visiblePos = parentPos;
break;
}
// parent or ancestor is hidden, we need to continue up the
// dependency chain
parentId = higherItem.getParentId();
if (parentId == null) throw new AssertionError();
}
}
} }
if (visiblePos != NO_POSITION) return items.get(position);
layoutManager.scrollToPositionWithOffset(visiblePos, 0);
} }
int getReplyCount(I item) { protected int findItemPosition(@Nullable I item) {
int counter = 0; for (int i = 0; i < items.size(); i++) {
int pos = items.indexOf(item); if (items.get(i).equals(item)) return i;
if (pos >= 0) {
int ancestorLvl = item.getLevel();
for (int i = pos + 1; i < items.size(); i++) {
int descendantLvl = items.get(i).getLevel();
if (descendantLvl <= ancestorLvl)
break;
if (descendantLvl == ancestorLvl + 1)
counter++;
}
} }
return counter; return NO_POSITION; // Not found
} }
void setReplyItem(@Nullable I item) {
if (replyItem != null) {
notifyItemChanged(getVisiblePos(replyItem));
}
replyItem = item;
if (replyItem != null) {
notifyItemChanged(getVisiblePos(replyItem));
}
}
void setReplyItemById(MessageId id) {
for (I item : items) {
if (item.getId().equals(id)) {
setReplyItem(item);
break;
}
}
}
private List<Integer> getSubTreeIndexes(int pos, int levelLimit) {
List<Integer> indexList = new ArrayList<>();
for (int i = pos + 1; i < getItemCount(); i++) {
I item = getVisibleItem(i);
if (item != null && item.getLevel() > levelLimit) {
indexList.add(i);
} else {
break;
}
}
return indexList;
}
public void showDescendants(I item) {
item.setShowingDescendants(true);
int visiblePos = getVisiblePos(item);
List<Integer> indexList =
getSubTreeIndexes(visiblePos, item.getLevel());
if (!indexList.isEmpty()) {
if (indexList.size() == 1) {
notifyItemInserted(indexList.get(0));
} else {
notifyItemRangeInserted(indexList.get(0),
indexList.size());
}
}
}
public void hideDescendants(I item) {
int visiblePos = getVisiblePos(item);
List<Integer> indexList =
getSubTreeIndexes(visiblePos, item.getLevel());
if (!indexList.isEmpty()) {
// stop animating children
for (int index : indexList) {
ValueAnimator anim = animatingItems.get(items.get(index));
if (anim != null && anim.isRunning()) {
anim.cancel();
}
}
if (indexList.size() == 1) {
notifyItemRemoved(indexList.get(0));
} else {
notifyItemRangeRemoved(indexList.get(0),
indexList.size());
}
}
item.setShowingDescendants(false);
}
/** /**
* Returns the visible item at the given position * Highlights the item with the given {@link MessageId}
* and disables the highlight for a previously highlighted item, if any.
* *
* @param position is visible item index * Only one item can be highlighted at a time.
* @return the visible item at index 'position' from an ordered list of
* visible items, or null if 'position' is larger than the number of
* visible items.
*/ */
void setHighlightedItem(@Nullable MessageId id) {
for (int i = 0; i < items.size(); i++) {
I item = items.get(i);
if (id != null && item.getId().equals(id)) {
item.setHighlighted(true);
notifyItemChanged(i, item);
} else if (item.isHighlighted()) {
item.setHighlighted(false);
notifyItemChanged(i, item);
}
}
}
@Nullable @Nullable
public I getVisibleItem(int position) { I getHighlightedItem() {
int levelLimit = UNDEFINED; for (I i : items) {
for (I item : items) { if (i.isHighlighted()) return i;
if (levelLimit >= 0) {
// skip hidden items that their parent is hiding
if (item.getLevel() > levelLimit) {
continue;
}
levelLimit = UNDEFINED;
}
if (!item.isShowingDescendants()) {
levelLimit = item.getLevel();
}
if (position-- == 0) {
return item;
}
} }
return null; return null;
} }
/**
* Returns the visible position of the given item.
*
* @param item the item to find the visible position of, or null to
* return the total count of visible items.
* @return the visible position of 'item', or the total number of visible
* items if 'item' is null. If 'item' is not visible, NO_POSITION is
* returned.
*/
protected int getVisiblePos(@Nullable I item) {
int visibleCounter = 0;
int levelLimit = UNDEFINED;
for (I i : items) {
if (levelLimit >= 0) {
if (i.getLevel() > levelLimit) {
// skip all the items below a non visible branch
continue;
}
levelLimit = UNDEFINED;
}
if (item != null && item.equals(i)) {
return visibleCounter;
} else if (!i.isShowingDescendants()) {
levelLimit = i.getLevel();
}
visibleCounter++;
}
return item == null ? visibleCounter : NO_POSITION;
}
void addAnimatingItem(I item, ValueAnimator anim) {
animatingItems.put(item, anim);
}
void removeAnimatingItem(I item) {
animatingItems.remove(item);
}
@Override
public int getRevision() {
return revision;
}
@UiThread
@Override
public void incrementRevision() {
revision++;
}
/** /**
* Gets the number of unread items above and below the current view port. * Gets the number of unread items above and below the current view port.
* *
@@ -321,24 +130,13 @@ public class ThreadItemAdapter<I extends ThreadItem>
return new UnreadCount(0, 0); return new UnreadCount(0, 0);
int unreadCounterTop = 0, unreadCounterBottom = 0; int unreadCounterTop = 0, unreadCounterBottom = 0;
int visibleCounter = 0; for (int i = 0; i < items.size(); i++) {
int levelLimit = UNDEFINED; I item = items.get(i);
for (I i : items) { if (i < positionTop && !item.isRead()) {
if (levelLimit >= 0) {
if (i.getLevel() > levelLimit) {
// skip all the items below a non visible branch
continue;
}
levelLimit = UNDEFINED;
}
if (visibleCounter > positionBottom && !i.isRead()) {
unreadCounterBottom++;
} else if (visibleCounter < positionTop && !i.isRead()) {
unreadCounterTop++; unreadCounterTop++;
} else if (!i.isShowingDescendants()) { } else if (i > positionBottom && !item.isRead()) {
levelLimit = i.getLevel(); unreadCounterBottom++;
} }
visibleCounter++;
} }
return new UnreadCount(unreadCounterTop, unreadCounterBottom); return new UnreadCount(unreadCounterTop, unreadCounterBottom);
} }
@@ -348,22 +146,9 @@ public class ThreadItemAdapter<I extends ThreadItem>
*/ */
public int getVisibleUnreadPosBottom() { public int getVisibleUnreadPosBottom() {
final int positionBottom = layoutManager.findLastVisibleItemPosition(); final int positionBottom = layoutManager.findLastVisibleItemPosition();
int visibleCounter = 0; if (positionBottom == NO_POSITION) return NO_POSITION;
int levelLimit = UNDEFINED; for (int i = positionBottom + 1; i < items.size(); i++) {
for (I i : items) { if (!items.get(i).isRead()) return i;
if (levelLimit >= 0) {
if (i.getLevel() > levelLimit) {
// skip all the items below a non visible branch
continue;
}
levelLimit = UNDEFINED;
}
if (visibleCounter > positionBottom && !i.isRead()) {
return visibleCounter;
} else if (!i.isShowingDescendants()) {
levelLimit = i.getLevel();
}
visibleCounter++;
} }
return NO_POSITION; return NO_POSITION;
} }
@@ -374,24 +159,12 @@ public class ThreadItemAdapter<I extends ThreadItem>
public int getVisibleUnreadPosTop() { public int getVisibleUnreadPosTop() {
final int positionTop = layoutManager.findFirstVisibleItemPosition(); final int positionTop = layoutManager.findFirstVisibleItemPosition();
int position = NO_POSITION; int position = NO_POSITION;
int visibleCounter = 0; for (int i = 0; i < items.size(); i++) {
int levelLimit = UNDEFINED; if (i < positionTop && !items.get(i).isRead()) {
for (I i : items) { position = i;
if (levelLimit >= 0) { } else if (i >= positionTop) {
if (i.getLevel() > levelLimit) {
// skip all the items below a non visible branch
continue;
}
levelLimit = UNDEFINED;
}
if (visibleCounter < positionTop && !i.isRead()) {
position = visibleCounter;
} if (visibleCounter >= positionTop) {
return position; return position;
} else if (!i.isShowingDescendants()) {
levelLimit = i.getLevel();
} }
visibleCounter++;
} }
return NO_POSITION; return NO_POSITION;
} }

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.threaded;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.CallSuper; import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
@@ -32,11 +33,11 @@ import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import org.briarproject.briar.android.view.UnreadMessageButton; import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.api.client.NamedGroup; import org.briarproject.briar.api.client.NamedGroup;
import org.briarproject.briar.api.client.PostHeader; import org.briarproject.briar.api.client.PostHeader;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
import java.util.Collection; import java.util.Collection;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static android.support.design.widget.Snackbar.make; import static android.support.design.widget.Snackbar.make;
@@ -62,9 +63,11 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
protected A adapter; protected A adapter;
protected BriarRecyclerView list; protected BriarRecyclerView list;
private LinearLayoutManager layoutManager;
protected TextInputView textInput; protected TextInputView textInput;
protected GroupId groupId; protected GroupId groupId;
private UnreadMessageButton upButton, downButton; private UnreadMessageButton upButton, downButton;
@Nullable
private MessageId replyId; private MessageId replyId;
protected abstract ThreadListController<G, I, H> getController(); protected abstract ThreadListController<G, I, H> getController();
@@ -89,9 +92,9 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
textInput.setVisibility(GONE); textInput.setVisibility(GONE);
textInput.setListener(this); textInput.setListener(this);
list = (BriarRecyclerView) findViewById(R.id.list); list = (BriarRecyclerView) findViewById(R.id.list);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); layoutManager = new LinearLayoutManager(this);
list.setLayoutManager(linearLayoutManager); list.setLayoutManager(layoutManager);
adapter = createAdapter(linearLayoutManager); adapter = createAdapter(layoutManager);
list.setAdapter(adapter); list.setAdapter(adapter);
list.getRecyclerView().addOnScrollListener( list.getRecyclerView().addOnScrollListener(
@@ -179,7 +182,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
adapter.setItems(items); adapter.setItems(items);
list.showData(); list.showData();
if (replyId != null) if (replyId != null)
adapter.setReplyItemById(replyId); adapter.setHighlightedItem(replyId);
} }
} else { } else {
LOG.info("Concurrent update, reloading"); LOG.info("Concurrent update, reloading");
@@ -240,7 +243,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
boolean visible = textInput.getVisibility() == VISIBLE; boolean visible = textInput.getVisibility() == VISIBLE;
outState.putBoolean(KEY_INPUT_VISIBILITY, visible); outState.putBoolean(KEY_INPUT_VISIBILITY, visible);
ThreadItem replyItem = adapter.getReplyItem(); ThreadItem replyItem = adapter.getHighlightedItem();
if (replyItem != null) { if (replyItem != null) {
outState.putByteArray(KEY_REPLY_ID, replyItem.getId().getBytes()); outState.putByteArray(KEY_REPLY_ID, replyItem.getId().getBytes());
} }
@@ -261,7 +264,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
public void onBackPressed() { public void onBackPressed() {
if (textInput.getVisibility() == VISIBLE) { if (textInput.getVisibility() == VISIBLE) {
textInput.setVisibility(GONE); textInput.setVisibility(GONE);
adapter.setReplyItem(null); adapter.setHighlightedItem(null);
} else { } else {
super.onBackPressed(); super.onBackPressed();
} }
@@ -276,8 +279,21 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
} }
@Override @Override
public void onReplyClick(I item) { public void onReplyClick(final I item) {
showTextInput(item); showTextInput(item);
if (textInput.isKeyboardOpen()) {
scrollToItemAtTop(item);
} else {
// wait with scrolling until keyboard opened
textInput.addOnKeyboardShownListener(
new KeyboardAwareLinearLayout.OnKeyboardShownListener() {
@Override
public void onKeyboardShown() {
scrollToItemAtTop(item);
textInput.removeOnKeyboardShownListener(this);
}
});
}
} }
@Override @Override
@@ -300,7 +316,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
} }
} }
protected void displaySnackbarShort(@StringRes int stringId) { private void scrollToItemAtTop(I item) {
int position = adapter.findItemPosition(item);
if (position != NO_POSITION) {
layoutManager
.scrollToPositionWithOffset(position, 0);
}
}
protected void displaySnackbar(@StringRes int stringId) {
Snackbar snackbar = make(list, stringId, Snackbar.LENGTH_SHORT); Snackbar snackbar = make(list, stringId, Snackbar.LENGTH_SHORT);
snackbar.getView().setBackgroundResource(R.color.briar_primary); snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.show(); snackbar.show();
@@ -318,7 +342,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
textInput.showSoftKeyboard(); textInput.showSoftKeyboard();
textInput.setHint(replyItem == null ? R.string.forum_new_message_hint : textInput.setHint(replyItem == null ? R.string.forum_new_message_hint :
R.string.forum_message_reply_hint); R.string.forum_message_reply_hint);
adapter.setReplyItem(replyItem); adapter.setHighlightedItem(
replyItem == null ? null : replyItem.getId());
} }
@Override @Override
@@ -326,10 +351,10 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
if (text.trim().length() == 0) if (text.trim().length() == 0)
return; return;
if (StringUtils.utf8IsTooLong(text, getMaxBodyLength())) { if (StringUtils.utf8IsTooLong(text, getMaxBodyLength())) {
displaySnackbarShort(R.string.text_too_long); displaySnackbar(R.string.text_too_long);
return; return;
} }
I replyItem = adapter.getReplyItem(); I replyItem = adapter.getHighlightedItem();
UiResultExceptionHandler<I, DbException> handler = UiResultExceptionHandler<I, DbException> handler =
new UiResultExceptionHandler<I, DbException>(this) { new UiResultExceptionHandler<I, DbException>(this) {
@Override @Override
@@ -346,7 +371,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
textInput.hideSoftKeyboard(); textInput.hideSoftKeyboard();
textInput.setVisibility(GONE); textInput.setVisibility(GONE);
textInput.setText(""); textInput.setText("");
adapter.setReplyItem(null); adapter.setHighlightedItem(null);
} }
protected abstract int getMaxBodyLength(); protected abstract int getMaxBodyLength();
@@ -377,7 +402,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
adapter.add(item); adapter.add(item);
if (isLocal) { if (isLocal) {
displaySnackbarShort(getItemPostedString()); displaySnackbar(getItemPostedString());
scrollToItemAtTop(item);
} else { } else {
updateUnreadCount(); updateUnreadCount();
} }

View File

@@ -16,32 +16,29 @@ import static android.view.View.VISIBLE;
public class ThreadPostViewHolder<I extends ThreadItem> public class ThreadPostViewHolder<I extends ThreadItem>
extends BaseThreadItemViewHolder<I> { extends BaseThreadItemViewHolder<I> {
private final TextView lvlText, repliesText; private final TextView lvlText;
private final View[] lvls; private final View[] lvls;
private final View chevron, replyButton; private final View replyButton;
private final static int[] nestedLineIds = {
R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
R.id.nested_line_4, R.id.nested_line_5
};
public ThreadPostViewHolder(View v) { public ThreadPostViewHolder(View v) {
super(v); super(v);
lvlText = (TextView) v.findViewById(R.id.nested_line_text); lvlText = (TextView) v.findViewById(R.id.nested_line_text);
repliesText = (TextView) v.findViewById(R.id.replies);
int[] nestedLineIds = {
R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
R.id.nested_line_4, R.id.nested_line_5
};
lvls = new View[nestedLineIds.length]; lvls = new View[nestedLineIds.length];
for (int i = 0; i < lvls.length; i++) { for (int i = 0; i < lvls.length; i++) {
lvls[i] = v.findViewById(nestedLineIds[i]); lvls[i] = v.findViewById(nestedLineIds[i]);
} }
chevron = v.findViewById(R.id.chevron);
replyButton = v.findViewById(R.id.btn_reply); replyButton = v.findViewById(R.id.btn_reply);
} }
// TODO improve encapsulation, so we don't need to pass the adapter here
@Override @Override
public void bind(final ThreadItemAdapter<I> adapter, public void bind(final I item, final ThreadItemListener<I> listener) {
final ThreadItemListener<I> listener, final I item, int pos) { super.bind(item, listener);
super.bind(adapter, listener, item, pos);
for (int i = 0; i < lvls.length; i++) { for (int i = 0; i < lvls.length; i++) {
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE); lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
@@ -53,41 +50,10 @@ public class ThreadPostViewHolder<I extends ThreadItem>
lvlText.setVisibility(GONE); lvlText.setVisibility(GONE);
} }
int replies = adapter.getReplyCount(item);
if (replies == 0) {
repliesText.setText("");
} else {
repliesText.setText(getContext().getResources()
.getQuantityString(R.plurals.message_replies, replies,
replies));
}
if (item.hasDescendants()) {
// chevron.setVisibility(VISIBLE);
if (item.isShowingDescendants()) {
chevron.setSelected(false);
} else {
chevron.setSelected(true);
}
chevron.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
chevron.setSelected(!chevron.isSelected());
if (chevron.isSelected()) {
adapter.hideDescendants(item);
} else {
adapter.showDescendants(item);
}
}
});
} else {
// chevron.setVisibility(INVISIBLE);
}
replyButton.setOnClickListener(new View.OnClickListener() { replyButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
listener.onReplyClick(item); listener.onReplyClick(item);
adapter.scrollTo(item);
} }
}); });
} }

View File

@@ -60,7 +60,7 @@
tools:text="Dec 24"/> tools:text="Dec 24"/>
<View <View
style="@style/Divider.ForumList" style="@style/Divider.ThreadItem"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@+id/postCountView"/> android:layout_below="@+id/postCountView"/>

View File

@@ -97,7 +97,7 @@
<View <View
android:id="@+id/divider" android:id="@+id/divider"
style="@style/Divider.ForumList" style="@style/Divider.ThreadItem"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@+id/statusView" android:layout_below="@+id/statusView"

View File

@@ -6,23 +6,14 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_medium"
android:baselineAligned="false" android:baselineAligned="false"
android:orientation="vertical"> android:orientation="vertical">
<View
android:id="@+id/top_divider"
style="@style/Divider.ForumList"
android:layout_alignParentTop="true"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/text" android:id="@+id/text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/top_divider" android:layout_margin="@dimen/margin_medium"
android:layout_marginBottom="@dimen/margin_small"
android:layout_marginRight="@dimen/margin_medium"
android:layout_marginTop="@dimen/margin_medium"
android:textColor="@color/briar_text_secondary" android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_medium" android:textSize="@dimen/text_size_medium"
android:textStyle="italic" android:textStyle="italic"
@@ -32,9 +23,12 @@
android:id="@+id/icon" android:id="@+id/icon"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignBottom="@+id/info"
android:layout_alignLeft="@+id/text" android:layout_alignLeft="@+id/text"
android:layout_alignTop="@+id/info"
android:layout_below="@+id/text" android:layout_below="@+id/text"
android:layout_marginRight="@dimen/margin_small" android:layout_marginRight="@dimen/margin_medium"
android:scaleType="center"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@drawable/ic_visibility"/> tools:src="@drawable/ic_visibility"/>
@@ -45,6 +39,7 @@
android:layout_alignEnd="@+id/text" android:layout_alignEnd="@+id/text"
android:layout_alignRight="@+id/text" android:layout_alignRight="@+id/text"
android:layout_below="@+id/text" android:layout_below="@+id/text"
android:layout_marginBottom="@dimen/margin_medium"
android:layout_toRightOf="@+id/icon" android:layout_toRightOf="@+id/icon"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="24dp" android:minHeight="24dp"
@@ -57,10 +52,10 @@
<org.briarproject.briar.android.view.AuthorView <org.briarproject.briar.android.view.AuthorView
android:id="@+id/author" android:id="@+id/author"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/button_size" android:layout_height="wrap_content"
android:layout_alignLeft="@+id/text" android:layout_alignLeft="@+id/text"
android:layout_below="@+id/info" android:layout_below="@+id/info"
android:gravity="center" android:layout_toLeftOf="@+id/optionsButton"
app:persona="commenter"/> app:persona="commenter"/>
<Button <Button
@@ -68,11 +63,16 @@
style="@style/BriarButtonFlat.Positive.Tiny" style="@style/BriarButtonFlat.Positive.Tiny"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignBottom="@+id/author"
android:layout_alignEnd="@+id/text" android:layout_alignEnd="@+id/text"
android:layout_alignRight="@+id/text" android:layout_alignRight="@+id/text"
android:layout_alignTop="@+id/author" android:layout_below="@+id/info"
android:layout_toRightOf="@+id/author"
android:gravity="right|center_vertical" android:gravity="right|center_vertical"
android:text="@string/options"/> android:text="@string/options"/>
<View
style="@style/Divider.ThreadItem"
android:layout_below="@+id/author"
android:layout_marginTop="@dimen/margin_medium"/>
</RelativeLayout> </RelativeLayout>

View File

@@ -81,7 +81,7 @@
android:text="@string/decline"/> android:text="@string/decline"/>
<View <View
style="@style/Divider.ForumList" style="@style/Divider.ThreadItem"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@+id/acceptButton"/> android:layout_below="@+id/acceptButton"/>

View File

@@ -113,7 +113,7 @@
tools:text="This is a description of the RSS feed. It can be several lines long, but it can also not exist at all if it is not present in the feed itself."/> tools:text="This is a description of the RSS feed. It can be several lines long, but it can also not exist at all if it is not present in the feed itself."/>
<View <View
style="@style/Divider.ForumList" style="@style/Divider.ThreadItem"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@+id/descriptionView" android:layout_below="@+id/descriptionView"

View File

@@ -62,23 +62,20 @@
android:background="@drawable/level_indicator_circle" android:background="@drawable/level_indicator_circle"
android:gravity="center" android:gravity="center"
android:textSize="@dimen/text_size_small" android:textSize="@dimen/text_size_small"
android:visibility="gone" android:visibility="gone"/>
/>
</RelativeLayout> </RelativeLayout>
<RelativeLayout <RelativeLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_weight="1"> android:layout_weight="1">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/text" android:id="@+id/text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingRight="@dimen/margin_medium" android:padding="@dimen/margin_medium"
android:paddingTop="@dimen/margin_medium"
android:textColor="@color/briar_text_primary" android:textColor="@color/briar_text_primary"
android:textIsSelectable="true" android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium" android:textSize="@dimen/text_size_medium"
@@ -87,26 +84,12 @@
<org.briarproject.briar.android.view.AuthorView <org.briarproject.briar.android.view.AuthorView
android:id="@+id/author" android:id="@+id/author"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/button_size" android:layout_height="wrap_content"
android:layout_alignLeft="@id/text" android:layout_alignLeft="@id/text"
android:layout_below="@id/text" android:layout_below="@id/text"
android:gravity="center" android:layout_marginLeft="@dimen/margin_medium"
app:persona="commenter"/>
<TextView
android:id="@+id/replies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/author"
android:layout_alignTop="@+id/author"
android:layout_toLeftOf="@+id/btn_reply" android:layout_toLeftOf="@+id/btn_reply"
android:layout_toRightOf="@+id/author" app:persona="commenter"/>
android:ellipsize="end"
android:gravity="right|end|center_vertical"
android:maxLines="1"
android:padding="@dimen/margin_medium"
android:textSize="@dimen/text_size_tiny"
tools:text="2 replies"/>
<TextView <TextView
android:id="@+id/btn_reply" android:id="@+id/btn_reply"
@@ -115,32 +98,17 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignBottom="@+id/author" android:layout_alignBottom="@+id/author"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_toLeftOf="@+id/chevron" android:layout_below="@+id/text"
android:layout_marginRight="@dimen/margin_medium"
android:text="@string/btn_reply" android:text="@string/btn_reply"
android:textSize="@dimen/text_size_tiny"/> android:textSize="@dimen/text_size_tiny"/>
<ImageView
android:id="@+id/chevron"
android:layout_width="@dimen/button_size"
android:layout_height="@dimen/button_size"
android:layout_alignBottom="@+id/author"
android:layout_alignParentRight="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:padding="@dimen/margin_medium"
android:scaleType="center"
android:src="@drawable/selector_chevron"
android:visibility="gone"/>
<View <View
android:id="@+id/top_divider" style="@style/Divider.ThreadItem"
style="@style/Divider.ForumList"
android:layout_width="match_parent"
android:layout_height="@dimen/margin_separator"
android:layout_alignLeft="@id/text" android:layout_alignLeft="@id/text"
android:layout_alignParentTop="true"/> android:layout_below="@+id/author"
android:layout_marginTop="@dimen/margin_medium"/>
</RelativeLayout> </RelativeLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -79,7 +79,7 @@
<item name="android:layout_marginLeft">@dimen/margin_large</item> <item name="android:layout_marginLeft">@dimen/margin_large</item>
</style> </style>
<style name="Divider.ForumList" parent="Divider"> <style name="Divider.ThreadItem" parent="Divider">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1dp</item> <item name="android:layout_height">1dp</item>
</style> </style>
@@ -102,7 +102,7 @@
<style name="DiscussionLevelIndicator"> <style name="DiscussionLevelIndicator">
<item name="android:layout_marginLeft">4dp</item> <item name="android:layout_marginLeft">4dp</item>
<item name="android:background">?android:attr/listDivider</item> <item name="android:background">@color/divider</item>
</style> </style>
<style name="BriarTabLayout" parent="Widget.Design.TabLayout"> <style name="BriarTabLayout" parent="Widget.Design.TabLayout">

View File

@@ -114,28 +114,15 @@ public class ForumActivityTest {
rc.getValue().onResult(dummyData); rc.getValue().onResult(dummyData);
ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter(); ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter();
Assert.assertNotNull(adapter); Assert.assertNotNull(adapter);
// Cascade close
assertEquals(6, adapter.getItemCount()); assertEquals(6, adapter.getItemCount());
adapter.hideDescendants(dummyData.get(2));
assertEquals(5, adapter.getItemCount());
adapter.hideDescendants(dummyData.get(1));
assertEquals(4, adapter.getItemCount());
adapter.hideDescendants(dummyData.get(0));
assertEquals(2, adapter.getItemCount());
assertTrue(dummyData.get(0).getText() assertTrue(dummyData.get(0).getText()
.equals(adapter.getVisibleItem(0).getText())); .equals(adapter.getItemAt(0).getText()));
assertTrue(dummyData.get(5).getText() assertTrue(dummyData.get(5).getText()
.equals(adapter.getVisibleItem(1).getText())); .equals(adapter.getItemAt(1).getText()));
// Cascade re-open
adapter.showDescendants(dummyData.get(0));
assertEquals(4, adapter.getItemCount());
adapter.showDescendants(dummyData.get(1));
assertEquals(5, adapter.getItemCount());
adapter.showDescendants(dummyData.get(2));
assertEquals(6, adapter.getItemCount());
assertTrue(dummyData.get(2).getText() assertTrue(dummyData.get(2).getText()
.equals(adapter.getVisibleItem(2).getText())); .equals(adapter.getItemAt(2).getText()));
assertTrue(dummyData.get(4).getText() assertTrue(dummyData.get(4).getText()
.equals(adapter.getVisibleItem(4).getText())); .equals(adapter.getItemAt(4).getText()));
} }
} }

View File

@@ -31,8 +31,6 @@ public interface MessageTree<T extends MessageTree.MessageNode> {
void setLevel(int level); void setLevel(int level);
void setDescendantCount(int descendantCount);
long getTimestamp(); long getTimestamp();
} }

View File

@@ -83,7 +83,6 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
list.add(node); list.add(node);
List<T> children = nodeMap.get(node.getId()); List<T> children = nodeMap.get(node.getId());
node.setLevel(level); node.setLevel(level);
node.setDescendantCount(children.size());
for (T child : children) { for (T child : children) {
traverse(list, child, level + 1); traverse(list, child, level + 1);
} }