diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 53ce958da..0e11dbd84 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -159,6 +159,15 @@
/>
+
+
+
+
+
diff --git a/briar-android/res/layout/list_item_blog_post.xml b/briar-android/res/layout/list_item_blog_post.xml
new file mode 100644
index 000000000..4243d5ebf
--- /dev/null
+++ b/briar-android/res/layout/list_item_blog_post.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/res/menu/blogs_my_blog_actions.xml b/briar-android/res/menu/blogs_my_blog_actions.xml
new file mode 100644
index 000000000..5c9052edb
--- /dev/null
+++ b/briar-android/res/menu/blogs_my_blog_actions.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index e3793a546..6b2ca75b1 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -257,8 +257,13 @@
A short description of your new blog
Potential readers may or may not subscribe to your blog based on the content of the description.
You don\'t have any blogs.\n\nWhy don\'t you create one now by clicking the plus in the top right screen corner?
+ This is the place for content of your blog.\n\nIt seems like you haven\'t written anything yet.\n\nPlease tap the pen icon to compose a new blog post.\n\nDon\'t forget to go public and share your blog.
Blog created
This blog is empty
+ This blog is currently empty.\n\nEither the author hasn\'t written anything yet, or the person who shared this blog with you needs to come online, so posts can be synchronized.
+ NEW
+ more
+ Write Blog Post
Blog List
Available Blogs
diff --git a/briar-android/res/values/styles.xml b/briar-android/res/values/styles.xml
index 1ecc1494c..729554b99 100644
--- a/briar-android/res/values/styles.xml
+++ b/briar-android/res/values/styles.xml
@@ -57,6 +57,17 @@
- @android:color/primary_text_light
+
+
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index 50aa911d3..64ecc4cdb 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -2,6 +2,7 @@ package org.briarproject.android;
import android.app.Activity;
+import org.briarproject.android.blogs.BlogActivity;
import org.briarproject.android.blogs.CreateBlogActivity;
import org.briarproject.android.blogs.MyBlogsFragment;
import org.briarproject.android.contact.ContactListFragment;
@@ -67,6 +68,8 @@ public interface ActivityComponent {
void inject(CreateBlogActivity activity);
+ void inject(BlogActivity activity);
+
void inject(SettingsActivity activity);
void inject(ChangePasswordActivity activity);
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
new file mode 100644
index 000000000..097f6b092
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
@@ -0,0 +1,174 @@
+package org.briarproject.android.blogs;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.BriarActivity;
+import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.api.blogs.BlogManager;
+import org.briarproject.api.blogs.BlogPostHeader;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.NoSuchGroupException;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static android.support.design.widget.Snackbar.LENGTH_LONG;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+public class BlogActivity extends BriarActivity {
+
+ static final String BLOG_NAME = "briar.BLOG_NAME";
+ static final String IS_MY_BLOG = "briar.IS_MY_BLOG";
+ static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG";
+ private static final int WRITE_POST = 1;
+
+ private static final Logger LOG =
+ Logger.getLogger(BlogActivity.class.getName());
+
+ private BlogPostAdapter adapter;
+ private BriarRecyclerView list;
+ private String blogName;
+ private boolean myBlog;
+
+ // Fields that are accessed from background threads must be volatile
+ private volatile GroupId groupId = null;
+ private volatile boolean scrollToTop = false;
+ @Inject
+ volatile BlogManager blogManager;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ setContentView(R.layout.activity_blog);
+
+ Intent i = getIntent();
+ byte[] b = i.getByteArrayExtra(GROUP_ID);
+ if (b == null) throw new IllegalStateException("No Group in intent.");
+ groupId = new GroupId(b);
+ blogName = i.getStringExtra(BLOG_NAME);
+ if (blogName != null) setTitle(blogName);
+ myBlog = i.getBooleanExtra(IS_MY_BLOG, false);
+
+ adapter = new BlogPostAdapter(this, blogName);
+ list = (BriarRecyclerView) this.findViewById(R.id.postList);
+ list.setLayoutManager(new LinearLayoutManager(this));
+ list.setAdapter(adapter);
+ if (myBlog) {
+ list.setEmptyText(
+ getString(R.string.blogs_my_blogs_blog_empty_state));
+ } else {
+ list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
+ }
+
+ // show snackbar if this blog was just created
+ boolean isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
+ if (isNew) {
+ Snackbar s = Snackbar.make(list, R.string.blogs_my_blogs_created,
+ LENGTH_LONG);
+ s.getView().setBackgroundResource(R.color.briar_primary);
+ s.show();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ loadBlogPosts();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (myBlog) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.blogs_my_blog_actions, menu);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_write_blog_post:
+/* Intent i = new Intent(this, WriteBlogPostActivity.class);
+ i.putExtra(GROUP_ID, groupId.getBytes());
+ i.putExtra(BLOG_NAME, blogName);
+ startActivityForResult(i, WRITE_POST);
+*/ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == WRITE_POST && resultCode == RESULT_OK) {
+ scrollToTop = true;
+ }
+ }
+
+ @Override
+ public void injectActivity(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ private void loadBlogPosts() {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // load blog posts
+ long now = System.currentTimeMillis();
+ Collection posts = new ArrayList<>();
+ try {
+ Collection header =
+ blogManager.getPostHeaders(groupId);
+ for (BlogPostHeader h : header) {
+ posts.add(new BlogPostItem(h));
+ }
+ } catch (NoSuchGroupException e) {
+ // Continue
+ }
+ displayBlogPosts(posts);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Post header load took " + duration + " ms");
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ }
+ }
+ });
+ }
+
+ private void displayBlogPosts(final Collection items) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (items.size() == 0) {
+ list.showData();
+ } else {
+ adapter.addAll(items);
+ if (scrollToTop) list.scrollToPosition(0);
+ }
+ scrollToTop = false;
+ }
+ });
+ }
+
+ // TODO listen to events and add new blog posts as they come in
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
index be0ff5e12..f4573a99f 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
@@ -1,7 +1,11 @@
package org.briarproject.android.blogs;
+import android.app.Activity;
import android.content.Context;
+import android.content.Intent;
import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView;
@@ -13,12 +17,17 @@ import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.util.TextAvatarView;
+import org.briarproject.api.blogs.Blog;
import org.briarproject.api.sync.GroupId;
import java.util.Collection;
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import static org.briarproject.android.BriarActivity.GROUP_ID;
+import static org.briarproject.android.blogs.BlogActivity.BLOG_NAME;
+import static org.briarproject.android.blogs.BlogActivity.IS_MY_BLOG;
class BlogListAdapter extends
RecyclerView.Adapter {
@@ -72,9 +81,9 @@ class BlogListAdapter extends
}
});
- private final Context ctx;
+ private final Activity ctx;
- BlogListAdapter(Context ctx) {
+ BlogListAdapter(Activity ctx) {
this.ctx = ctx;
}
@@ -122,13 +131,16 @@ class BlogListAdapter extends
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- // TODO #415
-/* Intent i = new Intent(ctx, BlogActivity.class);
+ Intent i = new Intent(ctx, BlogActivity.class);
Blog b = item.getBlog();
i.putExtra(GROUP_ID, b.getId().getBytes());
i.putExtra(BLOG_NAME, b.getName());
- ctx.startActivity(i);
-*/ }
+ i.putExtra(IS_MY_BLOG, item.isOurs());
+ ActivityOptionsCompat options = ActivityOptionsCompat
+ .makeCustomAnimation(ctx, android.R.anim.fade_in,
+ android.R.anim.fade_out);
+ ActivityCompat.startActivity(ctx, i, options.toBundle());
+ }
});
}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListItem.java b/briar-android/src/org/briarproject/android/blogs/BlogListItem.java
index 681dac42f..e35564679 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogListItem.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogListItem.java
@@ -11,8 +11,9 @@ class BlogListItem {
private final int postCount;
private final long timestamp;
private final int unread;
+ private final boolean ours;
- BlogListItem(Blog blog, Collection headers) {
+ BlogListItem(Blog blog, Collection headers, boolean ours) {
this.blog = blog;
if (headers.isEmpty()) {
postCount = 0;
@@ -33,6 +34,7 @@ class BlogListItem {
this.timestamp = newest.getTimestamp();
this.unread = unread;
}
+ this.ours = ours;
}
Blog getBlog() {
@@ -58,4 +60,8 @@ class BlogListItem {
int getUnreadCount() {
return unread;
}
+
+ public boolean isOurs() {
+ return ours;
+ }
}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
new file mode 100644
index 000000000..8aea31ee3
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
@@ -0,0 +1,153 @@
+package org.briarproject.android.blogs;
+
+import android.content.Context;
+import android.support.v7.util.SortedList;
+import android.support.v7.widget.RecyclerView;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.util.StringUtils;
+
+import java.util.Collection;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+class BlogPostAdapter extends
+ RecyclerView.Adapter {
+
+ private SortedList posts = new SortedList<>(
+ BlogPostItem.class, new SortedList.Callback() {
+
+ @Override
+ public int compare(BlogPostItem a, BlogPostItem b) {
+ if (a == b) return 0;
+ // The blog with the newest message comes first
+ long aTime = a.getTimestamp(), bTime = b.getTimestamp();
+ if (aTime > bTime) return -1;
+ if (aTime < bTime) return 1;
+ // Break ties by post title
+ if (a.getTitle() != null && b.getTitle() != null) {
+ return String.CASE_INSENSITIVE_ORDER
+ .compare(a.getTitle(), b.getTitle());
+ }
+ return 0;
+ }
+
+ @Override
+ public void onInserted(int position, int count) {
+ notifyItemRangeInserted(position, count);
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ notifyItemRangeRemoved(position, count);
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ notifyItemMoved(fromPosition, toPosition);
+ }
+
+ @Override
+ public void onChanged(int position, int count) {
+ notifyItemRangeChanged(position, count);
+ }
+
+ @Override
+ public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) {
+ return a.isRead() == b.isRead();
+ }
+
+ @Override
+ public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
+ return a.getId().equals(b.getId());
+ }
+ });
+
+ private final Context ctx;
+ private final String blogTitle;
+
+ BlogPostAdapter(Context ctx, String blogTitle) {
+ this.ctx = ctx;
+ this.blogTitle = blogTitle;
+ }
+
+ @Override
+ public BlogPostHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(ctx).inflate(
+ R.layout.list_item_blog_post, parent, false);
+ return new BlogPostHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(BlogPostHolder ui, int position) {
+ final BlogPostItem item = getItem(position);
+
+ // title
+ if (item.getTitle() != null) {
+ ui.title.setText(item.getTitle());
+ ui.title.setVisibility(VISIBLE);
+ } else {
+ ui.title.setVisibility(GONE);
+ }
+
+ // post body
+ ui.body.setText(StringUtils.fromUtf8(item.getBody()));
+
+ // date
+ ui.date.setText(
+ DateUtils.getRelativeTimeSpanString(ctx, item.getTimestamp()));
+
+ // new tag
+ if (item.isRead()) ui.unread.setVisibility(GONE);
+ else ui.unread.setVisibility(VISIBLE);
+ }
+
+ @Override
+ public int getItemCount() {
+ return posts.size();
+ }
+
+ public BlogPostItem getItem(int position) {
+ return posts.get(position);
+ }
+
+ public void addAll(Collection items) {
+ posts.addAll(items);
+ }
+
+ public void remove(BlogPostItem item) {
+ posts.remove(item);
+ }
+
+ public void clear() {
+ posts.clear();
+ }
+
+ public boolean isEmpty() {
+ return posts.size() == 0;
+ }
+
+ static class BlogPostHolder extends RecyclerView.ViewHolder {
+ private final ViewGroup layout;
+ private final TextView title;
+ private final TextView unread;
+ private final TextView date;
+ private final TextView body;
+
+ BlogPostHolder(View v) {
+ super(v);
+
+ layout = (ViewGroup) v;
+ title = (TextView) v.findViewById(R.id.titleView);
+ unread = (TextView) v.findViewById(R.id.newView);
+ date = (TextView) v.findViewById(R.id.dateView);
+ body = (TextView) v.findViewById(R.id.bodyView);
+ }
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java
new file mode 100644
index 000000000..3efdea6c7
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java
@@ -0,0 +1,52 @@
+package org.briarproject.android.blogs;
+
+import org.briarproject.api.blogs.BlogPostHeader;
+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
+class BlogPostItem {
+
+ private final BlogPostHeader header;
+ private final byte[] body;
+ private boolean read;
+
+ BlogPostItem(BlogPostHeader header, byte[] body) {
+ this.header = header;
+ this.body = body;
+ read = header.isRead();
+ }
+
+ public MessageId getId() {
+ return header.getId();
+ }
+
+ public String getTitle() {
+ return header.getTitle();
+ }
+
+ public byte[] getBody() {
+ return body;
+ }
+
+ public long getTimestamp() {
+ return header.getTimestamp();
+ }
+
+ public Author getAuthor() {
+ return header.getAuthor();
+ }
+
+ Status getAuthorStatus() {
+ return header.getAuthorStatus();
+ }
+
+ public void setRead(boolean read) {
+ this.read = read;
+ }
+
+ public boolean isRead() {
+ return read;
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java b/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java
index 97934a65f..980d67d1d 100644
--- a/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java
+++ b/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java
@@ -1,8 +1,11 @@
package org.briarproject.android.blogs;
+import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
@@ -29,11 +32,15 @@ import java.util.logging.Logger;
import javax.inject.Inject;
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
+import static org.briarproject.android.blogs.BlogActivity.BLOG_NAME;
+import static org.briarproject.android.blogs.BlogActivity.IS_MY_BLOG;
+import static org.briarproject.android.blogs.BlogActivity.IS_NEW_BLOG;
import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_DESC_LENGTH;
import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_TITLE_LENGTH;
@@ -169,14 +176,18 @@ public class CreateBlogActivity extends BriarActivity
runOnUiThread(new Runnable() {
@Override
public void run() {
- // TODO
-/* Intent i = new Intent(CreateBlogActivity.this,
- BlogActivity.class);
+ Intent i =
+ new Intent(CreateBlogActivity.this, BlogActivity.class);
i.putExtra(GROUP_ID, b.getId().getBytes());
i.putExtra(BLOG_NAME, b.getName());
- startActivity(i);
-*/ Toast.makeText(CreateBlogActivity.this,
- R.string.blogs_my_blogs_created, LENGTH_LONG).show();
+ i.putExtra(IS_MY_BLOG, true);
+ i.putExtra(IS_NEW_BLOG, true);
+ ActivityOptionsCompat options =
+ makeCustomAnimation(CreateBlogActivity.this,
+ android.R.anim.fade_in,
+ android.R.anim.fade_out);
+ ActivityCompat.startActivity(CreateBlogActivity.this, i,
+ options.toBundle());
supportFinishAfterTransition();
}
});
diff --git a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java b/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java
index d6ef76ae3..1cc4ee8ac 100644
--- a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java
+++ b/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java
@@ -135,7 +135,7 @@ public class MyBlogsFragment extends BaseFragment {
try {
Collection headers =
blogManager.getPostHeaders(b.getId());
- blogs.add(new BlogListItem(b, headers));
+ blogs.add(new BlogListItem(b, headers, true));
} catch (NoSuchGroupException e) {
// Continue
}