mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-18 21:59:54 +01:00
Refactor Forum Activity and adapters to be re-used for private groups
This commit is contained in:
@@ -7,12 +7,12 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<org.briarproject.android.view.BriarRecyclerView
|
<org.briarproject.android.view.BriarRecyclerView
|
||||||
android:id="@+id/forum_discussion_list"
|
android:id="@+id/list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
app:scrollToEnd="false"
|
app:emptyText="@string/no_forum_posts"
|
||||||
app:emptyText="@string/no_forum_posts"/>
|
app:scrollToEnd="false"/>
|
||||||
|
|
||||||
<org.briarproject.android.view.TextInputView
|
<org.briarproject.android.view.TextInputView
|
||||||
android:id="@+id/text_input_container"
|
android:id="@+id/text_input_container"
|
||||||
|
|||||||
@@ -3,36 +3,28 @@ package org.briarproject.android.forum;
|
|||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.LayoutRes;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.design.widget.Snackbar;
|
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.app.ActivityOptionsCompat;
|
import android.support.v4.app.ActivityOptionsCompat;
|
||||||
import android.support.v4.content.ContextCompat;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
import org.briarproject.android.ActivityComponent;
|
import org.briarproject.android.ActivityComponent;
|
||||||
import org.briarproject.android.BriarActivity;
|
|
||||||
import org.briarproject.android.api.AndroidNotificationManager;
|
|
||||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||||
import org.briarproject.android.controller.handler.UiResultHandler;
|
import org.briarproject.android.controller.handler.UiResultHandler;
|
||||||
import org.briarproject.android.forum.ForumController.ForumPostListener;
|
import org.briarproject.android.forum.ForumController.ForumPostListener;
|
||||||
import org.briarproject.android.forum.NestedForumAdapter.OnNestedForumListener;
|
|
||||||
import org.briarproject.android.sharing.ShareForumActivity;
|
import org.briarproject.android.sharing.ShareForumActivity;
|
||||||
import org.briarproject.android.sharing.SharingStatusForumActivity;
|
import org.briarproject.android.sharing.SharingStatusForumActivity;
|
||||||
import org.briarproject.android.view.BriarRecyclerView;
|
import org.briarproject.android.threaded.ThreadListActivity;
|
||||||
import org.briarproject.android.view.TextInputView;
|
|
||||||
import org.briarproject.android.view.TextInputView.TextInputListener;
|
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.forum.Forum;
|
import org.briarproject.api.forum.Forum;
|
||||||
import org.briarproject.api.forum.ForumPostHeader;
|
import org.briarproject.api.forum.ForumPostHeader;
|
||||||
import org.briarproject.api.sync.GroupId;
|
|
||||||
import org.briarproject.util.StringUtils;
|
import org.briarproject.util.StringUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -42,56 +34,32 @@ import javax.inject.Inject;
|
|||||||
|
|
||||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||||
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
|
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
|
||||||
import static android.view.View.GONE;
|
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
import static android.widget.Toast.LENGTH_SHORT;
|
import static android.widget.Toast.LENGTH_SHORT;
|
||||||
|
|
||||||
public class ForumActivity extends BriarActivity implements
|
public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAdapter>
|
||||||
ForumPostListener, TextInputListener, OnNestedForumListener {
|
implements ForumPostListener {
|
||||||
|
|
||||||
static final String FORUM_NAME = "briar.FORUM_NAME";
|
static final String FORUM_NAME = "briar.FORUM_NAME";
|
||||||
|
|
||||||
private static final int REQUEST_FORUM_SHARED = 3;
|
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
|
@Inject
|
||||||
protected ForumController forumController;
|
protected ForumController forumController;
|
||||||
|
|
||||||
// Protected access for testing
|
@Override
|
||||||
protected NestedForumAdapter forumAdapter;
|
public void injectActivity(ActivityComponent component) {
|
||||||
|
component.inject(this);
|
||||||
private BriarRecyclerView recyclerView;
|
}
|
||||||
private TextInputView textInput;
|
|
||||||
|
|
||||||
private volatile GroupId groupId = null;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(final Bundle state) {
|
public void onCreate(final Bundle state) {
|
||||||
super.onCreate(state);
|
super.onCreate(state);
|
||||||
|
|
||||||
setContentView(R.layout.activity_forum);
|
|
||||||
|
|
||||||
Intent i = getIntent();
|
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);
|
String forumName = i.getStringExtra(FORUM_NAME);
|
||||||
if (forumName != null) setTitle(forumName);
|
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,
|
forumController.loadForum(groupId,
|
||||||
new UiResultExceptionHandler<List<ForumEntry>, DbException>(
|
new UiResultExceptionHandler<List<ForumEntry>, DbException>(
|
||||||
this) {
|
this) {
|
||||||
@@ -101,14 +69,15 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
if (forum != null) setTitle(forum.getName());
|
if (forum != null) setTitle(forum.getName());
|
||||||
List<ForumEntry> entries = new ArrayList<>(result);
|
List<ForumEntry> entries = new ArrayList<>(result);
|
||||||
if (entries.isEmpty()) {
|
if (entries.isEmpty()) {
|
||||||
recyclerView.showData();
|
list.showData();
|
||||||
} else {
|
} else {
|
||||||
forumAdapter.setEntries(entries);
|
adapter.setItems(entries);
|
||||||
|
list.showData();
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
byte[] replyId =
|
byte[] replyId =
|
||||||
state.getByteArray(KEY_REPLY_ID);
|
state.getByteArray(KEY_REPLY_ID);
|
||||||
if (replyId != null)
|
if (replyId != null)
|
||||||
forumAdapter.setReplyEntryById(replyId);
|
adapter.setReplyItemById(replyId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,36 +91,20 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
protected @LayoutRes int getLayout() {
|
||||||
super.onRestoreInstanceState(savedInstanceState);
|
return R.layout.activity_forum;
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void injectActivity(ActivityComponent component) {
|
protected NestedForumAdapter createAdapter(
|
||||||
component.inject(this);
|
LinearLayoutManager layoutManager) {
|
||||||
|
return new NestedForumAdapter(this, layoutManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displaySnackbarShort(int stringId) {
|
@Override
|
||||||
Snackbar snackbar =
|
public void onResume() {
|
||||||
Snackbar.make(recyclerView, stringId, Snackbar.LENGTH_SHORT);
|
super.onResume();
|
||||||
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
notificationManager.clearForumPostNotification(groupId);
|
||||||
snackbar.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -172,34 +125,10 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
return super.onCreateOptionsMenu(menu);
|
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
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
ActivityOptionsCompat options = ActivityOptionsCompat
|
ActivityOptionsCompat options =
|
||||||
.makeCustomAnimation(this, android.R.anim.slide_in_left,
|
makeCustomAnimation(this, android.R.anim.slide_in_left,
|
||||||
android.R.anim.slide_out_right);
|
android.R.anim.slide_out_right);
|
||||||
// Handle presses on the action bar items
|
// Handle presses on the action bar items
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
@@ -228,31 +157,33 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected void markItemRead(ForumEntry entry) {
|
||||||
public void onResume() {
|
forumController.entryRead(entry);
|
||||||
super.onResume();
|
|
||||||
notificationManager.blockNotification(groupId);
|
|
||||||
notificationManager.clearForumPostNotification(groupId);
|
|
||||||
recyclerView.startPeriodicUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onForumPostReceived(ForumPostHeader header) {
|
||||||
super.onPause();
|
forumController.loadPost(header,
|
||||||
notificationManager.unblockNotification(groupId);
|
new UiResultExceptionHandler<ForumEntry, DbException>(this) {
|
||||||
recyclerView.stopPeriodicUpdate();
|
@Override
|
||||||
|
public void onResultUi(final ForumEntry result) {
|
||||||
|
addItem(result, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
// TODO add proper exception handling
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(String text) {
|
protected void sendItem(String text, @Nullable ForumEntry replyItem) {
|
||||||
if (text.trim().length() == 0)
|
UiResultExceptionHandler<ForumEntry, DbException> handler =
|
||||||
return;
|
|
||||||
ForumEntry replyEntry = forumAdapter.getReplyEntry();
|
|
||||||
UiResultExceptionHandler<ForumEntry, DbException> resultHandler =
|
|
||||||
new UiResultExceptionHandler<ForumEntry, DbException>(this) {
|
new UiResultExceptionHandler<ForumEntry, DbException>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onResultUi(ForumEntry result) {
|
public void onResultUi(ForumEntry result) {
|
||||||
onForumEntryAdded(result, true);
|
addItem(result, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -261,17 +192,28 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (replyItem == null) {
|
||||||
if (replyEntry == null) {
|
|
||||||
// root post
|
// root post
|
||||||
forumController.createPost(StringUtils.toUtf8(text), resultHandler);
|
forumController.createPost(StringUtils.toUtf8(text), handler);
|
||||||
} else {
|
} else {
|
||||||
forumController.createPost(StringUtils.toUtf8(text),
|
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() {
|
private void showUnsubscribeDialog() {
|
||||||
@@ -304,61 +246,4 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
builder.show();
|
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<ForumEntry, DbException>(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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,22 @@
|
|||||||
package org.briarproject.android.forum;
|
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.forum.ForumPostHeader;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.Author.Status;
|
import org.briarproject.api.identity.Author.Status;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
/* This class is not thread safe */
|
/* This class is not thread safe */
|
||||||
public class ForumEntry implements MessageTree.MessageNode {
|
public class ForumEntry extends ThreadItem {
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
ForumEntry(ForumPostHeader h, String text) {
|
ForumEntry(ForumPostHeader h, String text) {
|
||||||
this(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
|
super(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
|
||||||
h.getAuthorStatus());
|
h.getAuthorStatus(), h.isRead());
|
||||||
this.isRead = h.isRead();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ForumEntry(MessageId messageId, MessageId parentId, String text,
|
public ForumEntry(MessageId messageId, MessageId parentId, String text,
|
||||||
long timestamp, Author author, Status status) {
|
long timestamp, Author author, Status status) {
|
||||||
this.messageId = messageId;
|
super(messageId, parentId, text, timestamp, author, status, true);
|
||||||
this.parentId = parentId;
|
|
||||||
this.text = text;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.author = author;
|
|
||||||
this.status = status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,289 +1,20 @@
|
|||||||
package org.briarproject.android.forum;
|
package org.briarproject.android.forum;
|
||||||
|
|
||||||
import android.animation.Animator;
|
import android.support.annotation.UiThread;
|
||||||
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.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
import org.briarproject.android.util.NestedTreeList;
|
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||||
import org.briarproject.android.view.AuthorView;
|
|
||||||
import org.briarproject.api.sync.MessageId;
|
|
||||||
import org.briarproject.util.StringUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
@UiThread
|
||||||
import java.util.HashMap;
|
public class NestedForumAdapter extends ThreadItemAdapter<ForumEntry> {
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static android.support.v7.widget.RecyclerView.NO_POSITION;
|
public NestedForumAdapter(ThreadItemListener<ForumEntry> listener,
|
||||||
import static android.view.View.GONE;
|
|
||||||
import static android.view.View.INVISIBLE;
|
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
|
|
||||||
public class NestedForumAdapter
|
|
||||||
extends RecyclerView.Adapter<NestedForumAdapter.NestedForumHolder> {
|
|
||||||
|
|
||||||
private static final int UNDEFINED = -1;
|
|
||||||
|
|
||||||
private final NestedTreeList<ForumEntry> forumEntries =
|
|
||||||
new NestedTreeList<>();
|
|
||||||
private final Map<ForumEntry, ValueAnimator> 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,
|
|
||||||
LinearLayoutManager layoutManager) {
|
LinearLayoutManager layoutManager) {
|
||||||
this.ctx = ctx;
|
super(listener, layoutManager);
|
||||||
this.listener = listener;
|
|
||||||
this.layoutManager = layoutManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
ForumEntry getReplyEntry() {
|
|
||||||
return replyEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setEntries(List<ForumEntry> 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<Integer> getSubTreeIndexes(int pos, int levelLimit) {
|
|
||||||
List<Integer> 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<Integer> 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<Integer> 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -294,159 +25,4 @@ public class NestedForumAdapter
|
|||||||
return new NestedForumHolder(v);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.briarproject.android.forum;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.briarproject.android.threaded.ThreadItemViewHolder;
|
||||||
|
|
||||||
|
public class NestedForumHolder extends ThreadItemViewHolder<ForumEntry> {
|
||||||
|
|
||||||
|
public NestedForumHolder(View v) {
|
||||||
|
super(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.briarproject.android.util;
|
package org.briarproject.android.threaded;
|
||||||
|
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<I extends ThreadItem>
|
||||||
|
extends RecyclerView.Adapter<ThreadItemViewHolder<I>> {
|
||||||
|
|
||||||
|
static final int UNDEFINED = -1;
|
||||||
|
|
||||||
|
private final NestedTreeList<I> items =
|
||||||
|
new NestedTreeList<>();
|
||||||
|
private final Map<I, ValueAnimator> animatingItems =
|
||||||
|
new HashMap<>();
|
||||||
|
// highlight not dependant on time
|
||||||
|
private I replyItem;
|
||||||
|
// temporary highlight
|
||||||
|
private I addedEntry;
|
||||||
|
|
||||||
|
private final ThreadItemListener<I> listener;
|
||||||
|
private final LinearLayoutManager layoutManager;
|
||||||
|
|
||||||
|
public ThreadItemAdapter(ThreadItemListener<I> listener,
|
||||||
|
LinearLayoutManager layoutManager) {
|
||||||
|
this.listener = listener;
|
||||||
|
this.layoutManager = layoutManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(ThreadItemViewHolder<I> 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<I> 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<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
|
||||||
|
* @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<I> {
|
||||||
|
void onItemVisible(I item);
|
||||||
|
|
||||||
|
void onReplyClick(I item);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<I extends ThreadItem>
|
||||||
|
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<I> 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<I> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<I extends ThreadItem, A extends ThreadItemAdapter<I>>
|
||||||
|
extends BriarActivity
|
||||||
|
implements TextInputListener, ThreadItemListener<I> {
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -126,9 +126,9 @@ public class ForumActivityTest {
|
|||||||
adapter.hideDescendants(dummyData.get(0));
|
adapter.hideDescendants(dummyData.get(0));
|
||||||
assertEquals(2, adapter.getItemCount());
|
assertEquals(2, adapter.getItemCount());
|
||||||
assertTrue(dummyData.get(0).getText()
|
assertTrue(dummyData.get(0).getText()
|
||||||
.equals(adapter.getVisibleEntry(0).getText()));
|
.equals(adapter.getVisibleItem(0).getText()));
|
||||||
assertTrue(dummyData.get(5).getText()
|
assertTrue(dummyData.get(5).getText()
|
||||||
.equals(adapter.getVisibleEntry(1).getText()));
|
.equals(adapter.getVisibleItem(1).getText()));
|
||||||
// Cascade re-open
|
// Cascade re-open
|
||||||
adapter.showDescendants(dummyData.get(0));
|
adapter.showDescendants(dummyData.get(0));
|
||||||
assertEquals(4, adapter.getItemCount());
|
assertEquals(4, adapter.getItemCount());
|
||||||
@@ -137,8 +137,8 @@ public class ForumActivityTest {
|
|||||||
adapter.showDescendants(dummyData.get(2));
|
adapter.showDescendants(dummyData.get(2));
|
||||||
assertEquals(6, adapter.getItemCount());
|
assertEquals(6, adapter.getItemCount());
|
||||||
assertTrue(dummyData.get(2).getText()
|
assertTrue(dummyData.get(2).getText()
|
||||||
.equals(adapter.getVisibleEntry(2).getText()));
|
.equals(adapter.getVisibleItem(2).getText()));
|
||||||
assertTrue(dummyData.get(4).getText()
|
assertTrue(dummyData.get(4).getText()
|
||||||
.equals(adapter.getVisibleEntry(4).getText()));
|
.equals(adapter.getVisibleItem(4).getText()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class TestForumActivity extends ForumActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public NestedForumAdapter getAdapter() {
|
public NestedForumAdapter getAdapter() {
|
||||||
return forumAdapter;
|
return adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user