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); } }