diff --git a/briar-android/res/layout/activity_forum.xml b/briar-android/res/layout/activity_forum.xml
index b796da899..81c7e4a2b 100644
--- a/briar-android/res/layout/activity_forum.xml
+++ b/briar-android/res/layout/activity_forum.xml
@@ -7,12 +7,12 @@
android:orientation="vertical">
+ app:emptyText="@string/no_forum_posts"
+ app:scrollToEnd="false"/>
+ implements ForumPostListener {
static final String FORUM_NAME = "briar.FORUM_NAME";
private static final int REQUEST_FORUM_SHARED = 3;
- private static final String KEY_INPUT_VISIBILITY = "inputVisibility";
- private static final String KEY_REPLY_ID = "replyId";
-
- @Inject
- AndroidNotificationManager notificationManager;
@Inject
protected ForumController forumController;
- // Protected access for testing
- protected NestedForumAdapter forumAdapter;
-
- private BriarRecyclerView recyclerView;
- private TextInputView textInput;
-
- private volatile GroupId groupId = null;
+ @Override
+ public void injectActivity(ActivityComponent component) {
+ component.inject(this);
+ }
@Override
public void onCreate(final Bundle state) {
super.onCreate(state);
- setContentView(R.layout.activity_forum);
-
Intent i = getIntent();
- byte[] b = i.getByteArrayExtra(GROUP_ID);
- if (b == null) throw new IllegalStateException();
- groupId = new GroupId(b);
String forumName = i.getStringExtra(FORUM_NAME);
if (forumName != null) setTitle(forumName);
- textInput = (TextInputView) findViewById(R.id.text_input_container);
- textInput.setVisibility(GONE);
- textInput.setListener(this);
- recyclerView =
- (BriarRecyclerView) findViewById(R.id.forum_discussion_list);
- LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
- recyclerView.setLayoutManager(linearLayoutManager);
- forumAdapter = new NestedForumAdapter(this, this, linearLayoutManager);
- recyclerView.setAdapter(forumAdapter);
-
forumController.loadForum(groupId,
new UiResultExceptionHandler, DbException>(
this) {
@@ -101,14 +69,15 @@ public class ForumActivity extends BriarActivity implements
if (forum != null) setTitle(forum.getName());
List entries = new ArrayList<>(result);
if (entries.isEmpty()) {
- recyclerView.showData();
+ list.showData();
} else {
- forumAdapter.setEntries(entries);
+ adapter.setItems(entries);
+ list.showData();
if (state != null) {
byte[] replyId =
state.getByteArray(KEY_REPLY_ID);
if (replyId != null)
- forumAdapter.setReplyEntryById(replyId);
+ adapter.setReplyItemById(replyId);
}
}
}
@@ -122,36 +91,20 @@ public class ForumActivity extends BriarActivity implements
}
@Override
- protected void onRestoreInstanceState(Bundle savedInstanceState) {
- super.onRestoreInstanceState(savedInstanceState);
- textInput.setVisibility(
- savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY) ?
- VISIBLE : GONE);
- }
-
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putBoolean(KEY_INPUT_VISIBILITY,
- textInput.getVisibility() == VISIBLE);
- ForumEntry replyEntry = forumAdapter.getReplyEntry();
- if (replyEntry != null) {
- outState.putByteArray(KEY_REPLY_ID,
- replyEntry.getMessageId().getBytes());
- }
+ protected @LayoutRes int getLayout() {
+ return R.layout.activity_forum;
}
@Override
- public void injectActivity(ActivityComponent component) {
- component.inject(this);
+ protected NestedForumAdapter createAdapter(
+ LinearLayoutManager layoutManager) {
+ return new NestedForumAdapter(this, layoutManager);
}
- private void displaySnackbarShort(int stringId) {
- Snackbar snackbar =
- Snackbar.make(recyclerView, stringId, Snackbar.LENGTH_SHORT);
- snackbar.getView().setBackgroundResource(R.color.briar_primary);
- snackbar.show();
+ @Override
+ public void onResume() {
+ super.onResume();
+ notificationManager.clearForumPostNotification(groupId);
}
@Override
@@ -172,34 +125,10 @@ public class ForumActivity extends BriarActivity implements
return super.onCreateOptionsMenu(menu);
}
- @Override
- public void onBackPressed() {
- if (textInput.getVisibility() == VISIBLE) {
- textInput.setVisibility(GONE);
- forumAdapter.setReplyEntry(null);
- } else {
- super.onBackPressed();
- }
- }
-
- private void showTextInput(@Nullable ForumEntry replyEntry) {
- // An animation here would be an overkill because of the keyboard
- // popping up.
- // only clear the text when the input container was not visible
- if (textInput.getVisibility() != VISIBLE) {
- textInput.setVisibility(VISIBLE);
- textInput.setText("");
- }
- textInput.showSoftKeyboard();
- textInput.setHint(replyEntry == null ? R.string.forum_new_message_hint :
- R.string.forum_message_reply_hint);
- forumAdapter.setReplyEntry(replyEntry);
- }
-
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
- ActivityOptionsCompat options = ActivityOptionsCompat
- .makeCustomAnimation(this, android.R.anim.slide_in_left,
+ ActivityOptionsCompat options =
+ makeCustomAnimation(this, android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
// Handle presses on the action bar items
switch (item.getItemId()) {
@@ -228,31 +157,33 @@ public class ForumActivity extends BriarActivity implements
}
}
- @Override
- public void onResume() {
- super.onResume();
- notificationManager.blockNotification(groupId);
- notificationManager.clearForumPostNotification(groupId);
- recyclerView.startPeriodicUpdate();
+ protected void markItemRead(ForumEntry entry) {
+ forumController.entryRead(entry);
}
@Override
- public void onPause() {
- super.onPause();
- notificationManager.unblockNotification(groupId);
- recyclerView.stopPeriodicUpdate();
+ public void onForumPostReceived(ForumPostHeader header) {
+ forumController.loadPost(header,
+ new UiResultExceptionHandler(this) {
+ @Override
+ public void onResultUi(final ForumEntry result) {
+ addItem(result, false);
+ }
+
+ @Override
+ public void onExceptionUi(DbException exception) {
+ // TODO add proper exception handling
+ }
+ });
}
@Override
- public void onSendClick(String text) {
- if (text.trim().length() == 0)
- return;
- ForumEntry replyEntry = forumAdapter.getReplyEntry();
- UiResultExceptionHandler resultHandler =
+ protected void sendItem(String text, @Nullable ForumEntry replyItem) {
+ UiResultExceptionHandler handler =
new UiResultExceptionHandler(this) {
@Override
public void onResultUi(ForumEntry result) {
- onForumEntryAdded(result, true);
+ addItem(result, true);
}
@Override
@@ -261,17 +192,28 @@ public class ForumActivity extends BriarActivity implements
finish();
}
};
-
- if (replyEntry == null) {
+ if (replyItem == null) {
// root post
- forumController.createPost(StringUtils.toUtf8(text), resultHandler);
+ forumController.createPost(StringUtils.toUtf8(text), handler);
} else {
forumController.createPost(StringUtils.toUtf8(text),
- replyEntry.getId(), resultHandler);
+ replyItem.getId(), handler);
}
- textInput.hideSoftKeyboard();
- textInput.setVisibility(GONE);
- forumAdapter.setReplyEntry(null);
+ }
+
+ @Override
+ public void onForumRemoved() {
+ supportFinishAfterTransition();
+ }
+
+ @Override
+ protected int getItemPostedString() {
+ return R.string.forum_new_entry_posted;
+ }
+
+ @Override
+ protected int getItemReceivedString() {
+ return R.string.forum_new_entry_received;
}
private void showUnsubscribeDialog() {
@@ -304,61 +246,4 @@ public class ForumActivity extends BriarActivity implements
builder.show();
}
- @Override
- public void onEntryVisible(ForumEntry forumEntry) {
- if (!forumEntry.isRead()) {
- forumEntry.setRead(true);
- forumController.entryRead(forumEntry);
- }
- }
-
- @Override
- public void onReplyClick(ForumEntry forumEntry) {
- showTextInput(forumEntry);
- }
-
- private void onForumEntryAdded(final ForumEntry entry, boolean isLocal) {
- forumAdapter.addEntry(entry);
- if (isLocal && forumAdapter.isVisible(entry)) {
- displaySnackbarShort(R.string.forum_new_entry_posted);
- } else {
- Snackbar snackbar = Snackbar.make(recyclerView,
- isLocal ? R.string.forum_new_entry_posted :
- R.string.forum_new_entry_received,
- Snackbar.LENGTH_LONG);
- snackbar.getView().setBackgroundResource(R.color.briar_primary);
- snackbar.setActionTextColor(ContextCompat
- .getColor(ForumActivity.this,
- R.color.briar_button_positive));
- snackbar.setAction(R.string.show, new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- forumAdapter.scrollToEntry(entry);
- }
- });
- snackbar.getView().setBackgroundResource(R.color.briar_primary);
- snackbar.show();
- }
- }
-
- @Override
- public void onForumPostReceived(ForumPostHeader header) {
- forumController.loadPost(header,
- new UiResultExceptionHandler(this) {
- @Override
- public void onResultUi(final ForumEntry result) {
- onForumEntryAdded(result, false);
- }
-
- @Override
- public void onExceptionUi(DbException exception) {
- // TODO add proper exception handling
- }
- });
- }
-
- @Override
- public void onForumRemoved() {
- finish();
- }
}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumEntry.java b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
index 49e1ee32f..02817e1ab 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumEntry.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
@@ -1,102 +1,22 @@
package org.briarproject.android.forum;
-import org.briarproject.api.clients.MessageTree;
+import org.briarproject.android.threaded.ThreadItem;
import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.Author.Status;
import org.briarproject.api.sync.MessageId;
/* This class is not thread safe */
-public class ForumEntry implements MessageTree.MessageNode {
-
- public final static int LEVEL_UNDEFINED = -1;
-
- private final MessageId messageId;
- private final MessageId parentId;
- private final String text;
- private final long timestamp;
- private final Author author;
- private Status status;
- private int level = LEVEL_UNDEFINED;
- private boolean isShowingDescendants = true;
- private int descendantCount = 0;
- private boolean isRead = true;
+public class ForumEntry extends ThreadItem {
ForumEntry(ForumPostHeader h, String text) {
- this(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
- h.getAuthorStatus());
- this.isRead = h.isRead();
+ super(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
+ h.getAuthorStatus(), h.isRead());
}
public ForumEntry(MessageId messageId, MessageId parentId, String text,
long timestamp, Author author, Status status) {
- this.messageId = messageId;
- this.parentId = parentId;
- this.text = text;
- this.timestamp = timestamp;
- this.author = author;
- this.status = status;
+ super(messageId, parentId, text, timestamp, author, status, true);
}
- public String getText() {
- return text;
- }
-
- public int getLevel() {
- return level;
- }
-
- @Override
- public MessageId getId() {
- return messageId;
- }
-
- @Override
- public MessageId getParentId() {
- return parentId;
- }
-
- public long getTimestamp() {
- return timestamp;
- }
-
- public Author getAuthor() {
- return author;
- }
-
- public Status getStatus() {
- return status;
- }
-
- boolean isShowingDescendants() {
- return isShowingDescendants;
- }
-
- public void setLevel(int level) {
- this.level = level;
- }
-
- void setShowingDescendants(boolean showingDescendants) {
- this.isShowingDescendants = showingDescendants;
- }
-
- MessageId getMessageId() {
- return messageId;
- }
-
- public boolean isRead() {
- return isRead;
- }
-
- void setRead(boolean read) {
- isRead = read;
- }
-
- public boolean hasDescendants() {
- return descendantCount > 0;
- }
-
- public void setDescendantCount(int descendantCount) {
- this.descendantCount = descendantCount;
- }
}
diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java b/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
index f1e2f5de2..d1cb24008 100644
--- a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
+++ b/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
@@ -1,289 +1,20 @@
package org.briarproject.android.forum;
-import android.animation.Animator;
-import android.animation.ArgbEvaluator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.graphics.drawable.ColorDrawable;
-import android.support.annotation.Nullable;
-import android.support.v4.content.ContextCompat;
+import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.TextView;
import org.briarproject.R;
-import org.briarproject.android.util.NestedTreeList;
-import org.briarproject.android.view.AuthorView;
-import org.briarproject.api.sync.MessageId;
-import org.briarproject.util.StringUtils;
+import org.briarproject.android.threaded.ThreadItemAdapter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+@UiThread
+public class NestedForumAdapter extends ThreadItemAdapter {
-import static android.support.v7.widget.RecyclerView.NO_POSITION;
-import static android.view.View.GONE;
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-
-public class NestedForumAdapter
- extends RecyclerView.Adapter {
-
- private static final int UNDEFINED = -1;
-
- private final NestedTreeList forumEntries =
- new NestedTreeList<>();
- private final Map animatingEntries =
- new HashMap<>();
- // highlight not dependant on time
- private ForumEntry replyEntry;
- // temporary highlight
- private ForumEntry addedEntry;
- private final Context ctx;
- private final OnNestedForumListener listener;
- private final LinearLayoutManager layoutManager;
-
- public NestedForumAdapter(Context ctx, OnNestedForumListener listener,
+ public NestedForumAdapter(ThreadItemListener listener,
LinearLayoutManager layoutManager) {
- this.ctx = ctx;
- this.listener = listener;
- this.layoutManager = layoutManager;
- }
-
- ForumEntry getReplyEntry() {
- return replyEntry;
- }
-
- void setEntries(List entries) {
- forumEntries.clear();
- forumEntries.addAll(entries);
- notifyItemRangeInserted(0, entries.size());
- }
-
- void addEntry(ForumEntry entry) {
- forumEntries.add(entry);
- addedEntry = entry;
- if (entry.getParentId() == null) {
- notifyItemInserted(getVisiblePos(entry));
- } else {
- // Try to find the entry's parent and perform the proper ui update if
- // it's present and visible.
- for (int i = forumEntries.indexOf(entry) - 1; i >= 0; i--) {
- ForumEntry higherEntry = forumEntries.get(i);
- if (higherEntry.getLevel() < entry.getLevel()) {
- // parent found
- if (higherEntry.isShowingDescendants()) {
- int parentVisiblePos = getVisiblePos(higherEntry);
- if (parentVisiblePos != NO_POSITION) {
- // parent is visible, we need to update its ui
- notifyItemChanged(parentVisiblePos);
- // new entry insert ui
- int visiblePos = getVisiblePos(entry);
- notifyItemInserted(visiblePos);
- break;
- }
- } else {
- // do not show the new entry if its parent is not showing
- // descendants (this can be overridden by the user by
- // pressing the snack bar)
- break;
- }
- }
- }
- }
- }
-
- void scrollToEntry(ForumEntry entry) {
- int visiblePos = getVisiblePos(entry);
- if (visiblePos == NO_POSITION && entry.getParentId() != null) {
- // The entry is not visible due to being hidden by its parent entry.
- // Find the parent and make it visible and traverse up the parent
- // chain if necessary to make the entry visible
- MessageId parentId = entry.getParentId();
- for (int i = forumEntries.indexOf(entry) - 1; i >= 0; i--) {
- ForumEntry higherEntry = forumEntries.get(i);
- if (higherEntry.getId().equals(parentId)) {
- // parent found
- showDescendants(higherEntry);
- int parentPos = getVisiblePos(higherEntry);
- if (parentPos != NO_POSITION) {
- // parent or ancestor is visible, entry's visibility
- // is ensured
- notifyItemChanged(parentPos);
- visiblePos = parentPos;
- break;
- }
- // parent or ancestor is hidden, we need to continue up the
- // dependency chain
- parentId = higherEntry.getParentId();
- }
- }
- }
- if (visiblePos != NO_POSITION)
- layoutManager.scrollToPositionWithOffset(visiblePos, 0);
- }
-
- private int getReplyCount(ForumEntry entry) {
- int counter = 0;
- int pos = forumEntries.indexOf(entry);
- if (pos >= 0) {
- int ancestorLvl = entry.getLevel();
- for (int i = pos + 1; i < forumEntries.size(); i++) {
- int descendantLvl = forumEntries.get(i).getLevel();
- if (descendantLvl <= ancestorLvl)
- break;
- if (descendantLvl == ancestorLvl + 1)
- counter++;
- }
- }
- return counter;
- }
-
- void setReplyEntryById(byte[] id) {
- MessageId messageId = new MessageId(id);
- for (ForumEntry entry : forumEntries) {
- if (entry.getId().equals(messageId)) {
- setReplyEntry(entry);
- break;
- }
- }
- }
-
- void setReplyEntry(@Nullable ForumEntry entry) {
- if (replyEntry != null) {
- notifyItemChanged(getVisiblePos(replyEntry));
- }
- replyEntry = entry;
- if (replyEntry != null) {
- notifyItemChanged(getVisiblePos(replyEntry));
- }
- }
-
- private List getSubTreeIndexes(int pos, int levelLimit) {
- List indexList = new ArrayList<>();
-
- for (int i = pos + 1; i < getItemCount(); i++) {
- ForumEntry entry = getVisibleEntry(i);
- if (entry != null && entry.getLevel() > levelLimit) {
- indexList.add(i);
- } else {
- break;
- }
- }
- return indexList;
- }
-
- void showDescendants(ForumEntry forumEntry) {
- forumEntry.setShowingDescendants(true);
- int visiblePos = getVisiblePos(forumEntry);
- List indexList =
- getSubTreeIndexes(visiblePos, forumEntry.getLevel());
- if (!indexList.isEmpty()) {
- if (indexList.size() == 1) {
- notifyItemInserted(indexList.get(0));
- } else {
- notifyItemRangeInserted(indexList.get(0),
- indexList.size());
- }
- }
- }
-
- void hideDescendants(ForumEntry forumEntry) {
- int visiblePos = getVisiblePos(forumEntry);
- List indexList =
- getSubTreeIndexes(visiblePos, forumEntry.getLevel());
- if (!indexList.isEmpty()) {
- // stop animating children
- for (int index : indexList) {
- ValueAnimator anim =
- animatingEntries.get(forumEntries.get(index));
- if (anim != null && anim.isRunning()) {
- anim.cancel();
- }
- }
- if (indexList.size() == 1) {
- notifyItemRemoved(indexList.get(0));
- } else {
- notifyItemRangeRemoved(indexList.get(0),
- indexList.size());
- }
- }
- forumEntry.setShowingDescendants(false);
- }
-
-
- /**
- *
- * @param position is visible entry index
- * @return the visible entry at index position from an ordered list of visible
- * entries, or null if position is larger then the number of visible entries.
- */
- @Nullable
- ForumEntry getVisibleEntry(int position) {
- int levelLimit = UNDEFINED;
- for (ForumEntry forumEntry : forumEntries) {
- if (levelLimit >= 0) {
- // skip hidden entries that their parent is hiding
- if (forumEntry.getLevel() > levelLimit) {
- continue;
- }
- levelLimit = UNDEFINED;
- }
- if (!forumEntry.isShowingDescendants()) {
- levelLimit = forumEntry.getLevel();
- }
- if (position-- == 0) {
- return forumEntry;
- }
- }
- return null;
- }
-
- private void animateFadeOut(final NestedForumHolder ui,
- final ForumEntry addedEntry) {
- ui.setIsRecyclable(false);
- ValueAnimator anim = new ValueAnimator();
- animatingEntries.put(addedEntry, anim);
- ColorDrawable viewColor = (ColorDrawable) ui.cell.getBackground();
- anim.setIntValues(viewColor.getColor(), ContextCompat
- .getColor(ctx, R.color.window_background));
- anim.setEvaluator(new ArgbEvaluator());
- anim.addListener(new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) {
-
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- ui.setIsRecyclable(true);
- animatingEntries.remove(addedEntry);
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- ui.setIsRecyclable(true);
- animatingEntries.remove(addedEntry);
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) {
-
- }
- });
- anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- ui.cell.setBackgroundColor(
- (Integer) valueAnimator.getAnimatedValue());
- }
- });
- anim.setDuration(5000);
- anim.start();
+ super(listener, layoutManager);
}
@Override
@@ -294,159 +25,4 @@ public class NestedForumAdapter
return new NestedForumHolder(v);
}
- @Override
- public void onBindViewHolder(
- final NestedForumHolder ui, final int position) {
- final ForumEntry entry = getVisibleEntry(position);
- if (entry == null) return;
- listener.onEntryVisible(entry);
-
- ui.textView.setText(StringUtils.trim(entry.getText()));
-
- if (position == 0) {
- ui.topDivider.setVisibility(View.INVISIBLE);
- } else {
- ui.topDivider.setVisibility(View.VISIBLE);
- }
-
- for (int i = 0; i < ui.lvls.length; i++) {
- ui.lvls[i].setVisibility(i < entry.getLevel() ? VISIBLE : GONE);
- }
- if (entry.getLevel() > 5) {
- ui.lvlText.setVisibility(VISIBLE);
- ui.lvlText.setText("" + entry.getLevel());
- } else {
- ui.lvlText.setVisibility(GONE);
- }
- ui.author.setAuthor(entry.getAuthor());
- ui.author.setDate(entry.getTimestamp());
- ui.author.setAuthorStatus(entry.getStatus());
-
- int replies = getReplyCount(entry);
- if (replies == 0) {
- ui.repliesText.setText("");
- } else {
- ui.repliesText.setText(
- ctx.getResources()
- .getQuantityString(R.plurals.message_replies,
- replies, replies));
- }
-
- if (entry.hasDescendants()) {
- ui.chevron.setVisibility(VISIBLE);
- if (entry.isShowingDescendants()) {
- ui.chevron.setSelected(false);
- } else {
- ui.chevron.setSelected(true);
- }
- ui.chevron.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- ui.chevron.setSelected(!ui.chevron.isSelected());
- if (ui.chevron.isSelected()) {
- hideDescendants(entry);
- } else {
- showDescendants(entry);
- }
- }
- });
- } else {
- ui.chevron.setVisibility(INVISIBLE);
- }
- if (entry.equals(replyEntry)) {
- ui.cell.setBackgroundColor(ContextCompat
- .getColor(ctx, R.color.forum_cell_highlight));
- } else if (entry.equals(addedEntry)) {
-
- ui.cell.setBackgroundColor(ContextCompat
- .getColor(ctx, R.color.forum_cell_highlight));
- animateFadeOut(ui, addedEntry);
- addedEntry = null;
- } else {
- ui.cell.setBackgroundColor(ContextCompat
- .getColor(ctx, R.color.window_background));
- }
- ui.replyButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- listener.onReplyClick(entry);
- scrollToEntry(entry);
- }
- });
- }
-
- public boolean isVisible(ForumEntry entry) {
- return getVisiblePos(entry) != NO_POSITION;
- }
-
- /**
- * @param sEntry the ForumEntry to find the visible positoin of, or null to
- * return the total cound of visible elements
- * @return the visible position of sEntry, or the total number of visible
- * elements if sEntry is null. If sEntry is not visible a NO_POSITION is
- * returned.
- */
- private int getVisiblePos(@Nullable ForumEntry sEntry) {
- int visibleCounter = 0;
- int levelLimit = UNDEFINED;
- for (ForumEntry fEntry : forumEntries) {
- if (levelLimit >= 0) {
- if (fEntry.getLevel() > levelLimit) {
- // skip all the entries below a non visible branch
- continue;
- }
- levelLimit = UNDEFINED;
- }
- if (sEntry != null && sEntry.equals(fEntry)) {
- return visibleCounter;
- } else if (!fEntry.isShowingDescendants()) {
- levelLimit = fEntry.getLevel();
- }
- visibleCounter++;
- }
- return sEntry == null ? visibleCounter : NO_POSITION;
- }
-
- @Override
- public int getItemCount() {
- return getVisiblePos(null);
- }
-
- static class NestedForumHolder extends RecyclerView.ViewHolder {
-
- private final TextView textView, lvlText, repliesText;
- private final AuthorView author;
- private final View[] lvls;
- private final View chevron, replyButton;
- private final ViewGroup cell;
- private final View topDivider;
-
- private NestedForumHolder(View v) {
- super(v);
-
- textView = (TextView) v.findViewById(R.id.text);
- lvlText = (TextView) v.findViewById(R.id.nested_line_text);
- author = (AuthorView) v.findViewById(R.id.author);
- 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];
- for (int i = 0; i < lvls.length; i++) {
- lvls[i] = v.findViewById(nestedLineIds[i]);
- }
- chevron = v.findViewById(R.id.chevron);
- replyButton = v.findViewById(R.id.btn_reply);
- cell = (ViewGroup) v.findViewById(R.id.forum_cell);
- topDivider = v.findViewById(R.id.top_divider);
- }
-
- }
-
- interface OnNestedForumListener {
- void onEntryVisible(ForumEntry forumEntry);
-
- void onReplyClick(ForumEntry forumEntry);
- }
}
diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java b/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java
new file mode 100644
index 000000000..8263ccb4a
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java
@@ -0,0 +1,13 @@
+package org.briarproject.android.forum;
+
+import android.view.View;
+
+import org.briarproject.android.threaded.ThreadItemViewHolder;
+
+public class NestedForumHolder extends ThreadItemViewHolder {
+
+ public NestedForumHolder(View v) {
+ super(v);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
new file mode 100644
index 000000000..ee84c1fb0
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
@@ -0,0 +1,22 @@
+package org.briarproject.android.privategroup.conversation;
+
+import org.briarproject.android.threaded.ThreadItem;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.Author.Status;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.sync.MessageId;
+
+class GroupMessageItem extends ThreadItem {
+
+ public GroupMessageItem(MessageId messageId, MessageId parentId,
+ String text, long timestamp, Author author, Status status,
+ boolean isRead) {
+ super(messageId, parentId, text, timestamp, author, status, isRead);
+ }
+
+ public GroupMessageItem(GroupMessageHeader h, String text) {
+ this(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
+ h.getAuthorStatus(), h.isRead());
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/util/NestedTreeList.java b/briar-android/src/org/briarproject/android/threaded/NestedTreeList.java
similarity index 96%
rename from briar-android/src/org/briarproject/android/util/NestedTreeList.java
rename to briar-android/src/org/briarproject/android/threaded/NestedTreeList.java
index d190a9f6d..aec3c58be 100644
--- a/briar-android/src/org/briarproject/android/util/NestedTreeList.java
+++ b/briar-android/src/org/briarproject/android/threaded/NestedTreeList.java
@@ -1,4 +1,4 @@
-package org.briarproject.android.util;
+package org.briarproject.android.threaded;
import android.support.annotation.UiThread;
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItem.java b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
new file mode 100644
index 000000000..a5a220759
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
@@ -0,0 +1,95 @@
+package org.briarproject.android.threaded;
+
+import org.briarproject.api.clients.MessageTree.MessageNode;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.Author.Status;
+import org.briarproject.api.sync.MessageId;
+
+import static org.briarproject.android.threaded.ThreadItemAdapter.UNDEFINED;
+
+/* This class is not thread safe */
+public abstract class ThreadItem implements MessageNode {
+
+ private final MessageId messageId;
+ private final MessageId parentId;
+ private final String text;
+ private final long timestamp;
+ private final Author author;
+ private final Status status;
+ private int level = UNDEFINED;
+ private boolean isShowingDescendants = true;
+ private int descendantCount = 0;
+ private boolean isRead;
+
+ public ThreadItem(MessageId messageId, MessageId parentId, String text,
+ long timestamp, Author author, Status status, boolean isRead) {
+ this.messageId = messageId;
+ this.parentId = parentId;
+ this.text = text;
+ this.timestamp = timestamp;
+ this.author = author;
+ this.status = status;
+ this.isRead = isRead;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ @Override
+ public MessageId getId() {
+ return messageId;
+ }
+
+ @Override
+ public MessageId getParentId() {
+ return parentId;
+ }
+
+ @Override
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public Author getAuthor() {
+ return author;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public boolean isShowingDescendants() {
+ return isShowingDescendants;
+ }
+
+ @Override
+ public void setLevel(int level) {
+ this.level = level;
+ }
+
+ public void setShowingDescendants(boolean showingDescendants) {
+ this.isShowingDescendants = showingDescendants;
+ }
+
+ public boolean isRead() {
+ return isRead;
+ }
+
+ public void setRead(boolean read) {
+ isRead = read;
+ }
+
+ public boolean hasDescendants() {
+ return descendantCount > 0;
+ }
+
+ @Override
+ public void setDescendantCount(int descendantCount) {
+ this.descendantCount = descendantCount;
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
new file mode 100644
index 000000000..82e1fca18
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
@@ -0,0 +1,298 @@
+package org.briarproject.android.threaded;
+
+import android.animation.ValueAnimator;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+
+import org.briarproject.api.sync.MessageId;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
+
+@UiThread
+public abstract class ThreadItemAdapter
+ extends RecyclerView.Adapter> {
+
+ static final int UNDEFINED = -1;
+
+ private final NestedTreeList items =
+ new NestedTreeList<>();
+ private final Map animatingItems =
+ new HashMap<>();
+ // highlight not dependant on time
+ private I replyItem;
+ // temporary highlight
+ private I addedEntry;
+
+ private final ThreadItemListener listener;
+ private final LinearLayoutManager layoutManager;
+
+ public ThreadItemAdapter(ThreadItemListener listener,
+ LinearLayoutManager layoutManager) {
+ this.listener = listener;
+ this.layoutManager = layoutManager;
+ }
+
+ @Override
+ public void onBindViewHolder(ThreadItemViewHolder ui, int position) {
+ final I item = getVisibleItem(position);
+ if (item == null) return;
+ listener.onItemVisible(item);
+ ui.bind(this, listener, item, position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return getVisiblePos(null);
+ }
+
+ public I getReplyItem() {
+ return replyItem;
+ }
+
+ public void setItems(List items) {
+ this.items.clear();
+ this.items.addAll(items);
+ notifyDataSetChanged();
+ }
+
+ public void add(I item) {
+ items.add(item);
+ addedEntry = item;
+ if (item.getParentId() == null) {
+ 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;
+ }
+ }
+ }
+ }
+ }
+
+ public void scrollTo(I item) {
+ int visiblePos = getVisiblePos(item);
+ if (visiblePos == NO_POSITION && item.getParentId() != 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
+ MessageId parentId = item.getParentId();
+ 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, entry'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 (visiblePos != NO_POSITION)
+ layoutManager.scrollToPositionWithOffset(visiblePos, 0);
+ }
+
+ int getReplyCount(I item) {
+ int counter = 0;
+ int pos = items.indexOf(item);
+ 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;
+ }
+
+ public void setReplyItem(@Nullable I entry) {
+ if (replyItem != null) {
+ notifyItemChanged(getVisiblePos(replyItem));
+ }
+ replyItem = entry;
+ if (replyItem != null) {
+ notifyItemChanged(getVisiblePos(replyItem));
+ }
+ }
+
+ public void setReplyItemById(byte[] id) {
+ MessageId messageId = new MessageId(id);
+ for (I item : items) {
+ if (item.getId().equals(messageId)) {
+ setReplyItem(item);
+ break;
+ }
+ }
+ }
+
+ private List getSubTreeIndexes(int pos, int levelLimit) {
+ List 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 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 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
+ * @param position is visible entry index
+ * @return the visible entry at index position from an ordered list of
+ * visible entries, or null if position is larger than
+ * the number of visible entries.
+ */
+ @Nullable
+ public I getVisibleItem(int position) {
+ int levelLimit = UNDEFINED;
+ for (I item : items) {
+ if (levelLimit >= 0) {
+ // skip hidden entries 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;
+ }
+
+ public boolean isVisible(I item) {
+ return getVisiblePos(item) != NO_POSITION;
+ }
+
+ /**
+ * Returns the visible position of the given ThreadItem
+ * @param item the ThreadItem to find the visible position of, or null to
+ * return the total count of visible elements
+ * @return the visible position of item, or the total number of visible
+ * elements if sEntry is null. If item is not visible NO_POSITION is
+ * returned.
+ */
+ private int getVisiblePos(@Nullable I item) {
+ int visibleCounter = 0;
+ int levelLimit = UNDEFINED;
+ for (I iItem : items) {
+ if (levelLimit >= 0) {
+ if (iItem.getLevel() > levelLimit) {
+ // skip all the entries below a non visible branch
+ continue;
+ }
+ levelLimit = UNDEFINED;
+ }
+ if (item != null && item.equals(iItem)) {
+ return visibleCounter;
+ } else if (!iItem.isShowingDescendants()) {
+ levelLimit = iItem.getLevel();
+ }
+ visibleCounter++;
+ }
+ return item == null ? visibleCounter : NO_POSITION;
+ }
+
+ I getAddedItem() {
+ return addedEntry;
+ }
+
+ void clearAddedItem() {
+ addedEntry = null;
+ }
+
+ void addAnimatingItem(I item, ValueAnimator anim) {
+ animatingItems.put(item, anim);
+ }
+
+ void removeAnimatingItem(I item) {
+ animatingItems.remove(item);
+ }
+
+ public interface ThreadItemListener {
+ void onItemVisible(I item);
+
+ void onReplyClick(I item);
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java b/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java
new file mode 100644
index 000000000..27ca9d80d
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java
@@ -0,0 +1,182 @@
+package org.briarproject.android.threaded;
+
+import android.animation.Animator;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.ColorDrawable;
+import android.support.annotation.UiThread;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
+import org.briarproject.android.view.AuthorView;
+import org.briarproject.util.StringUtils;
+
+import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+@UiThread
+public abstract class ThreadItemViewHolder
+ extends RecyclerView.ViewHolder {
+
+ private final static int ANIMATION_DURATION = 5000;
+
+ private final TextView textView, lvlText, repliesText;
+ private final AuthorView author;
+ private final View[] lvls;
+ private final View chevron, replyButton;
+ private final ViewGroup cell;
+ private final View topDivider;
+
+ public ThreadItemViewHolder(View v) {
+ super(v);
+
+ textView = (TextView) v.findViewById(R.id.text);
+ lvlText = (TextView) v.findViewById(R.id.nested_line_text);
+ author = (AuthorView) v.findViewById(R.id.author);
+ 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];
+ for (int i = 0; i < lvls.length; i++) {
+ lvls[i] = v.findViewById(nestedLineIds[i]);
+ }
+ chevron = v.findViewById(R.id.chevron);
+ replyButton = v.findViewById(R.id.btn_reply);
+ cell = (ViewGroup) v.findViewById(R.id.forum_cell);
+ topDivider = v.findViewById(R.id.top_divider);
+ }
+
+ // TODO improve encapsulation, so we don't need to pass the adapter here
+ public void bind(final ThreadItemAdapter adapter,
+ final ThreadItemListener listener, final I item, int pos) {
+
+ textView.setText(StringUtils.trim(item.getText()));
+
+ if (pos == 0) {
+ topDivider.setVisibility(View.INVISIBLE);
+ } else {
+ topDivider.setVisibility(View.VISIBLE);
+ }
+
+ for (int i = 0; i < lvls.length; i++) {
+ lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
+ }
+ if (item.getLevel() > 5) {
+ lvlText.setVisibility(VISIBLE);
+ lvlText.setText("" + item.getLevel());
+ } else {
+ lvlText.setVisibility(GONE);
+ }
+ author.setAuthor(item.getAuthor());
+ author.setDate(item.getTimestamp());
+ author.setAuthorStatus(item.getStatus());
+
+ 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);
+ }
+ if (item.equals(adapter.getReplyItem())) {
+ cell.setBackgroundColor(ContextCompat
+ .getColor(getContext(), R.color.forum_cell_highlight));
+ } else if (item.equals(adapter.getAddedItem())) {
+ cell.setBackgroundColor(ContextCompat
+ .getColor(getContext(), R.color.forum_cell_highlight));
+ animateFadeOut(adapter, adapter.getAddedItem());
+ adapter.clearAddedItem();
+ } else {
+ cell.setBackgroundColor(ContextCompat
+ .getColor(getContext(), R.color.window_background));
+ }
+ replyButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ listener.onReplyClick(item);
+ adapter.scrollTo(item);
+ }
+ });
+ }
+
+ private void animateFadeOut(final ThreadItemAdapter adapter,
+ final I addedItem) {
+
+ setIsRecyclable(false);
+ ValueAnimator anim = new ValueAnimator();
+ adapter.addAnimatingItem(addedItem, anim);
+ ColorDrawable viewColor = (ColorDrawable) cell.getBackground();
+ anim.setIntValues(viewColor.getColor(), ContextCompat
+ .getColor(getContext(), R.color.window_background));
+ anim.setEvaluator(new ArgbEvaluator());
+ anim.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setIsRecyclable(true);
+ adapter.removeAnimatingItem(addedItem);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ setIsRecyclable(true);
+ adapter.removeAnimatingItem(addedItem);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+
+ }
+ });
+ anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ cell.setBackgroundColor(
+ (Integer) valueAnimator.getAnimatedValue());
+ }
+ });
+ anim.setDuration(ANIMATION_DURATION);
+ anim.start();
+ }
+
+ private Context getContext() {
+ return textView.getContext();
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
new file mode 100644
index 000000000..ea18e720f
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
@@ -0,0 +1,205 @@
+package org.briarproject.android.threaded;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.briarproject.R;
+import org.briarproject.android.BriarActivity;
+import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
+import org.briarproject.android.view.BriarRecyclerView;
+import org.briarproject.android.view.TextInputView;
+import org.briarproject.android.view.TextInputView.TextInputListener;
+import org.briarproject.api.sync.GroupId;
+
+import javax.inject.Inject;
+
+import static android.support.design.widget.Snackbar.make;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+public abstract class ThreadListActivity>
+ extends BriarActivity
+ implements TextInputListener, ThreadItemListener {
+
+ protected static final String KEY_INPUT_VISIBILITY = "inputVisibility";
+ protected static final String KEY_REPLY_ID = "replyId";
+
+ @Inject
+ protected AndroidNotificationManager notificationManager;
+
+ protected A adapter;
+ protected BriarRecyclerView list;
+ protected TextInputView textInput;
+ protected GroupId groupId;
+
+ @CallSuper
+ @Override
+ public void onCreate(final Bundle state) {
+ super.onCreate(state);
+
+ setContentView(getLayout());
+
+ Intent i = getIntent();
+ byte[] b = i.getByteArrayExtra(GROUP_ID);
+ if (b == null) throw new IllegalStateException("No GroupId in intent.");
+ groupId = new GroupId(b);
+
+ textInput = (TextInputView) findViewById(R.id.text_input_container);
+ textInput.setVisibility(GONE);
+ textInput.setListener(this);
+ list = (BriarRecyclerView) findViewById(R.id.list);
+ LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
+ list.setLayoutManager(linearLayoutManager);
+ adapter = createAdapter(linearLayoutManager);
+ list.setAdapter(adapter);
+ }
+
+ protected abstract @LayoutRes int getLayout();
+
+ protected abstract A createAdapter(LinearLayoutManager layoutManager);
+
+ @CallSuper
+ @Override
+ public void onResume() {
+ super.onResume();
+ notificationManager.blockNotification(groupId);
+ list.startPeriodicUpdate();
+ }
+
+ @CallSuper
+ @Override
+ public void onPause() {
+ super.onPause();
+ notificationManager.unblockNotification(groupId);
+ list.stopPeriodicUpdate();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ textInput.setVisibility(
+ savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY) ?
+ VISIBLE : GONE);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_INPUT_VISIBILITY,
+ textInput.getVisibility() == VISIBLE);
+ ThreadItem replyItem = adapter.getReplyItem();
+ if (replyItem != null) {
+ outState.putByteArray(KEY_REPLY_ID,
+ replyItem.getId().getBytes());
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ if (textInput.isKeyboardOpen()) textInput.hideSoftKeyboard();
+ supportFinishAfterTransition();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (textInput.getVisibility() == VISIBLE) {
+ textInput.setVisibility(GONE);
+ adapter.setReplyItem(null);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void onItemVisible(I item) {
+ if (!item.isRead()) {
+ item.setRead(true);
+ markItemRead(item);
+ }
+ }
+
+ protected abstract void markItemRead(I item);
+
+ @Override
+ public void onReplyClick(I item) {
+ showTextInput(item);
+ }
+
+ protected void displaySnackbarShort(int stringId) {
+ Snackbar snackbar = make(list, stringId, Snackbar.LENGTH_SHORT);
+ snackbar.getView().setBackgroundResource(R.color.briar_primary);
+ snackbar.show();
+ }
+
+ protected void showTextInput(@Nullable I replyItem) {
+ // An animation here would be an overkill because of the keyboard
+ // popping up.
+ // only clear the text when the input container was not visible
+ if (textInput.getVisibility() != VISIBLE) {
+ textInput.setVisibility(VISIBLE);
+ textInput.setText("");
+ }
+ textInput.requestFocus();
+ textInput.showSoftKeyboard();
+ textInput.setHint(replyItem == null ? R.string.forum_new_message_hint :
+ R.string.forum_message_reply_hint);
+ adapter.setReplyItem(replyItem);
+ }
+
+ @Override
+ public void onSendClick(String text) {
+ if (text.trim().length() == 0)
+ return;
+ I replyItem = adapter.getReplyItem();
+ sendItem(text, replyItem);
+ textInput.hideSoftKeyboard();
+ textInput.setVisibility(GONE);
+ adapter.setReplyItem(null);
+ }
+
+ protected abstract void sendItem(String text, I replyToItem);
+
+ protected void addItem(final I item, boolean isLocal) {
+ adapter.add(item);
+ if (isLocal && adapter.isVisible(item)) {
+ displaySnackbarShort(getItemPostedString());
+ } else {
+ Snackbar snackbar = Snackbar.make(list,
+ isLocal ? getItemPostedString() : getItemReceivedString(),
+ Snackbar.LENGTH_LONG);
+ snackbar.getView().setBackgroundResource(R.color.briar_primary);
+ snackbar.setActionTextColor(ContextCompat
+ .getColor(ThreadListActivity.this,
+ R.color.briar_button_positive));
+ snackbar.setAction(R.string.show, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ adapter.scrollTo(item);
+ }
+ });
+ snackbar.getView().setBackgroundResource(R.color.briar_primary);
+ snackbar.show();
+ }
+ }
+
+ protected abstract @StringRes int getItemPostedString();
+
+ protected abstract @StringRes int getItemReceivedString();
+
+}
diff --git a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
index 3e5431ac9..7842c11e2 100644
--- a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
+++ b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
@@ -126,9 +126,9 @@ public class ForumActivityTest {
adapter.hideDescendants(dummyData.get(0));
assertEquals(2, adapter.getItemCount());
assertTrue(dummyData.get(0).getText()
- .equals(adapter.getVisibleEntry(0).getText()));
+ .equals(adapter.getVisibleItem(0).getText()));
assertTrue(dummyData.get(5).getText()
- .equals(adapter.getVisibleEntry(1).getText()));
+ .equals(adapter.getVisibleItem(1).getText()));
// Cascade re-open
adapter.showDescendants(dummyData.get(0));
assertEquals(4, adapter.getItemCount());
@@ -137,8 +137,8 @@ public class ForumActivityTest {
adapter.showDescendants(dummyData.get(2));
assertEquals(6, adapter.getItemCount());
assertTrue(dummyData.get(2).getText()
- .equals(adapter.getVisibleEntry(2).getText()));
+ .equals(adapter.getVisibleItem(2).getText()));
assertTrue(dummyData.get(4).getText()
- .equals(adapter.getVisibleEntry(4).getText()));
+ .equals(adapter.getVisibleItem(4).getText()));
}
}
diff --git a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
index 8acbf3b8d..806fee299 100644
--- a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
+++ b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
@@ -16,7 +16,7 @@ public class TestForumActivity extends ForumActivity {
}
public NestedForumAdapter getAdapter() {
- return forumAdapter;
+ return adapter;
}
@Override