diff --git a/briar-android/res/layout/fragment_blogs_my.xml b/briar-android/res/layout/fragment_blogs_my.xml
index a552dc0fc..288adfaa3 100644
--- a/briar-android/res/layout/fragment_blogs_my.xml
+++ b/briar-android/res/layout/fragment_blogs_my.xml
@@ -1,26 +1,7 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
+ tools:listitem="@layout/list_item_blog"/>
diff --git a/briar-android/res/layout/list_item_blog.xml b/briar-android/res/layout/list_item_blog.xml
new file mode 100644
index 000000000..1fe1f22db
--- /dev/null
+++ b/briar-android/res/layout/list_item_blog.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/res/layout/list_item_forum.xml b/briar-android/res/layout/list_item_forum.xml
index 171a40859..0652bec6b 100644
--- a/briar-android/res/layout/list_item_forum.xml
+++ b/briar-android/res/layout/list_item_forum.xml
@@ -31,7 +31,7 @@
tools:text="This is a name of a forum"/>
+ android:layout_below="@+id/postCountView"/>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 3447cf1ec..e3793a546 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -81,12 +81,12 @@
Leave Forum
Left Forum
Sharing Status
- No posts
+ No posts
- %d unread post
- %d unread posts
-
+
- %d post
- %d posts
@@ -256,7 +256,9 @@
Blog title (cannot be changed later)
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?
Blog created
+ This blog is empty
Blog List
Available Blogs
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
new file mode 100644
index 000000000..be0ff5e12
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
@@ -0,0 +1,197 @@
+package org.briarproject.android.blogs;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+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.android.util.TextAvatarView;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+class BlogListAdapter extends
+ RecyclerView.Adapter {
+
+ private SortedList blogs = new SortedList<>(
+ BlogListItem.class, new SortedList.Callback() {
+
+ @Override
+ public int compare(BlogListItem a, BlogListItem 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 blog name
+ String aName = a.getName();
+ String bName = b.getName();
+ return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
+ }
+
+ @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(BlogListItem a, BlogListItem b) {
+ return a.getBlog().equals(b.getBlog()) &&
+ a.getTimestamp() == b.getTimestamp() &&
+ a.getUnreadCount() == b.getUnreadCount();
+ }
+
+ @Override
+ public boolean areItemsTheSame(BlogListItem a, BlogListItem b) {
+ return a.getBlog().equals(b.getBlog());
+ }
+ });
+
+ private final Context ctx;
+
+ BlogListAdapter(Context ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public BlogViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(ctx).inflate(
+ R.layout.list_item_blog, parent, false);
+ return new BlogViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(BlogViewHolder ui, int position) {
+ final BlogListItem item = getItem(position);
+
+ // Avatar
+ ui.avatar.setText(item.getName().substring(0, 1));
+ ui.avatar.setBackgroundBytes(item.getBlog().getId().getBytes());
+ ui.avatar.setUnreadCount(item.getUnreadCount());
+
+ // Blog Name
+ ui.name.setText(item.getName());
+
+ // Post Count
+ int postCount = item.getPostCount();
+ ui.postCount.setText(ctx.getResources()
+ .getQuantityString(R.plurals.posts, postCount, postCount));
+ ui.postCount.setTextColor(
+ ContextCompat.getColor(ctx, R.color.briar_text_secondary));
+
+ // Date and Status
+ if (item.isEmpty()) {
+ ui.date.setVisibility(GONE);
+ ui.avatar.setProblem(true);
+ ui.status.setText(ctx.getString(R.string.blogs_blog_is_empty));
+ ui.status.setVisibility(VISIBLE);
+ } else {
+ long timestamp = item.getTimestamp();
+ ui.date.setText(
+ DateUtils.getRelativeTimeSpanString(ctx, timestamp));
+ ui.date.setVisibility(VISIBLE);
+ ui.status.setVisibility(GONE);
+ }
+
+ // Open Blog on Click
+ ui.layout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // TODO #415
+/* 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);
+*/ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return blogs.size();
+ }
+
+ public BlogListItem getItem(int position) {
+ return blogs.get(position);
+ }
+
+ @Nullable
+ public BlogListItem getItem(GroupId g) {
+ for (int i = 0; i < blogs.size(); i++) {
+ BlogListItem item = blogs.get(i);
+ if (item.getBlog().getGroup().getId().equals(g)) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ public void addAll(Collection items) {
+ blogs.addAll(items);
+ }
+
+ void updateItem(BlogListItem item) {
+ BlogListItem oldItem = getItem(item.getBlog().getGroup().getId());
+ int position = blogs.indexOf(oldItem);
+ blogs.updateItemAt(position, item);
+ }
+
+ public void remove(BlogListItem item) {
+ blogs.remove(item);
+ }
+
+ public void clear() {
+ blogs.clear();
+ }
+
+ public boolean isEmpty() {
+ return blogs.size() == 0;
+ }
+
+ static class BlogViewHolder extends RecyclerView.ViewHolder {
+
+ private final ViewGroup layout;
+ private final TextAvatarView avatar;
+ private final TextView name;
+ private final TextView postCount;
+ private final TextView date;
+ private final TextView status;
+
+ BlogViewHolder(View v) {
+ super(v);
+
+ layout = (ViewGroup) v;
+ avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
+ name = (TextView) v.findViewById(R.id.nameView);
+ postCount = (TextView) v.findViewById(R.id.postCountView);
+ date = (TextView) v.findViewById(R.id.dateView);
+ status = (TextView) v.findViewById(R.id.statusView);
+ }
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListItem.java b/briar-android/src/org/briarproject/android/blogs/BlogListItem.java
new file mode 100644
index 000000000..681dac42f
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogListItem.java
@@ -0,0 +1,61 @@
+package org.briarproject.android.blogs;
+
+import org.briarproject.api.blogs.Blog;
+import org.briarproject.api.blogs.BlogPostHeader;
+
+import java.util.Collection;
+
+class BlogListItem {
+
+ private final Blog blog;
+ private final int postCount;
+ private final long timestamp;
+ private final int unread;
+
+ BlogListItem(Blog blog, Collection headers) {
+ this.blog = blog;
+ if (headers.isEmpty()) {
+ postCount = 0;
+ timestamp = 0;
+ unread = 0;
+ } else {
+ BlogPostHeader newest = null;
+ long timestamp = -1;
+ int unread = 0;
+ for (BlogPostHeader h : headers) {
+ if (h.getTimestamp() > timestamp) {
+ timestamp = h.getTimestamp();
+ newest = h;
+ }
+ if (!h.isRead()) unread++;
+ }
+ this.postCount = headers.size();
+ this.timestamp = newest.getTimestamp();
+ this.unread = unread;
+ }
+ }
+
+ Blog getBlog() {
+ return blog;
+ }
+
+ String getName() {
+ return blog.getName();
+ }
+
+ boolean isEmpty() {
+ return postCount == 0;
+ }
+
+ int getPostCount() {
+ return postCount;
+ }
+
+ long getTimestamp() {
+ return timestamp;
+ }
+
+ int getUnreadCount() {
+ return unread;
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java b/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java
index cb28dd0ea..d6ef76ae3 100644
--- a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java
+++ b/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java
@@ -5,26 +5,50 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.api.blogs.Blog;
+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.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.logging.Logger;
import javax.inject.Inject;
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
public class MyBlogsFragment extends BaseFragment {
public final static String TAG = MyBlogsFragment.class.getName();
+ private static final Logger LOG = Logger.getLogger(TAG);
+ private BriarRecyclerView list;
+ private BlogListAdapter adapter;
+
+ // Fields that are accessed from background threads must be volatile
+ @Inject
+ protected volatile IdentityManager identityManager;
+ @Inject
+ volatile BlogManager blogManager;
+
@Inject
public MyBlogsFragment() {
}
@@ -35,19 +59,30 @@ public class MyBlogsFragment extends BaseFragment {
Bundle savedInstanceState) {
setHasOptionsMenu(true);
- View v = inflater.inflate(R.layout.fragment_blogs_my, container,
- false);
- TextView numView = (TextView) v.findViewById(R.id.num);
- numView.setText("My Blogs");
+ adapter = new BlogListAdapter(getActivity());
- return v;
+ list = (BriarRecyclerView) inflater
+ .inflate(R.layout.fragment_blogs_my, container, false);
+ list.setLayoutManager(new LinearLayoutManager(getActivity()));
+ list.setAdapter(adapter);
+ list.setEmptyText(getString(R.string.blogs_my_blogs_empty_state));
+
+ return list;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
listener.getActivityComponent().inject(this);
+ // Starting from here, we can use injected objects
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ adapter.clear();
+ loadBlogs();
}
@Override
@@ -85,4 +120,49 @@ public class MyBlogsFragment extends BaseFragment {
component.inject(this);
}
+ private void loadBlogs() {
+ listener.runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // load blogs
+ long now = System.currentTimeMillis();
+ Collection blogs = new ArrayList<>();
+ Collection authors =
+ identityManager.getLocalAuthors();
+ LocalAuthor a = authors.iterator().next();
+ for (Blog b : blogManager.getBlogs(a)) {
+ try {
+ Collection headers =
+ blogManager.getPostHeaders(b.getId());
+ blogs.add(new BlogListItem(b, headers));
+ } catch (NoSuchGroupException e) {
+ // Continue
+ }
+ }
+ displayBlogs(blogs);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Full blog load took " + duration + " ms");
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ }
+ }
+ });
+ }
+
+ private void displayBlogs(final Collection items) {
+ listener.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (items.size() == 0) {
+ list.showData();
+ } else {
+ adapter.addAll(items);
+ }
+ }
+ });
+ }
+
}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
index 17fd01346..2051eadff 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
@@ -104,16 +104,16 @@ class ForumListAdapter extends
// Post Count
int postCount = item.getPostCount();
if (postCount > 0) {
- ui.unread.setText(ctx.getResources()
- .getQuantityString(R.plurals.forum_posts, postCount,
+ ui.postCount.setText(ctx.getResources()
+ .getQuantityString(R.plurals.posts, postCount,
postCount));
- ui.unread.setTextColor(
+ ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_secondary));
} else {
ui.avatar.setProblem(true);
- ui.unread.setText(ctx.getString(R.string.forum_no_posts));
- ui.unread.setTextColor(
+ ui.postCount.setText(ctx.getString(R.string.no_posts));
+ ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_tertiary));
}
@@ -187,7 +187,7 @@ class ForumListAdapter extends
private final ViewGroup layout;
private final TextAvatarView avatar;
private final TextView name;
- private final TextView unread;
+ private final TextView postCount;
private final TextView date;
ForumViewHolder(View v) {
@@ -196,7 +196,7 @@ class ForumListAdapter extends
layout = (ViewGroup) v;
avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.forumNameView);
- unread = (TextView) v.findViewById(R.id.unreadView);
+ postCount = (TextView) v.findViewById(R.id.postCountView);
date = (TextView) v.findViewById(R.id.dateView);
}
}