diff --git a/briar-android/res/layout/fragment_blogs_my.xml b/briar-android/res/layout/fragment_blogs_my.xml index a552dc0fce49e2a0f2c2ca14340ab4c38ae51fff..288adfaa3937ea3d0b242b6c7177007b3e994d67 100644 --- a/briar-android/res/layout/fragment_blogs_my.xml +++ b/briar-android/res/layout/fragment_blogs_my.xml @@ -1,26 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- This is just a placeholder to be replaced by the real My Blogs list --> -<LinearLayout +<org.briarproject.android.util.BriarRecyclerView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical"> - - <TextView - android:id="@+id/num" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="center_horizontal" - android:padding="@dimen/margin_activity_horizontal" - android:textSize="128sp" - tools:text="1"/> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="@dimen/margin_activity_horizontal" - android:text="There is nothing for you to see here.\n\nMove along and come back later." - android:textSize="@dimen/text_size_large"/> - -</LinearLayout> \ 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 0000000000000000000000000000000000000000..1fe1f22db024383b68baea541fbfea8e11eb88e3 --- /dev/null +++ b/briar-android/res/layout/list_item_blog.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/listitem_horizontal_margin" + android:layout_marginStart="@dimen/listitem_horizontal_margin" + android:background="?attr/selectableItemBackground"> + + <org.briarproject.android.util.TextAvatarView + android:id="@+id/avatarView" + android:layout_width="@dimen/listitem_picture_frame_size" + android:layout_height="@dimen/listitem_picture_frame_size" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_marginEnd="@dimen/listitem_horizontal_margin" + android:layout_marginRight="@dimen/listitem_horizontal_margin" + android:layout_marginTop="@dimen/margin_medium"/> + + <TextView + android:id="@+id/nameView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_marginTop="@dimen/listitem_horizontal_margin" + android:layout_toEndOf="@+id/avatarView" + android:layout_toRightOf="@+id/avatarView" + android:maxLines="2" + android:textColor="@color/briar_text_primary" + android:textSize="@dimen/text_size_medium" + tools:text="This is a name of a blog"/> + + <TextView + android:id="@+id/postCountView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/nameView" + android:layout_marginBottom="@dimen/margin_small" + android:layout_toEndOf="@+id/avatarView" + android:layout_toRightOf="@+id/avatarView" + android:paddingTop="@dimen/margin_small" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_small" + tools:text="1337 posts"/> + + <TextView + android:id="@+id/dateView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:layout_below="@+id/nameView" + android:layout_marginEnd="@dimen/listitem_horizontal_margin" + android:layout_marginRight="@dimen/listitem_horizontal_margin" + android:paddingTop="@dimen/margin_small" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_small" + tools:text="Dec 24"/> + + <TextView + android:id="@+id/statusView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/postCountView" + android:layout_toEndOf="@+id/avatarView" + android:layout_toRightOf="@+id/avatarView" + android:textColor="@color/briar_text_tertiary" + tools:text="@string/blogs_blog_is_empty"/> + + <View + style="@style/Divider.ForumList" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_below="@+id/statusView" + android:layout_marginTop="@dimen/listitem_horizontal_margin"/> + +</RelativeLayout> + diff --git a/briar-android/res/layout/list_item_forum.xml b/briar-android/res/layout/list_item_forum.xml index 171a40859c67555455afbe89f7e902882c39ac6b..0652bec6b01927253e4bbe35986de7a1ace3cb24 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"/> <TextView - android:id="@+id/unreadView" + android:id="@+id/postCountView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/forumNameView" @@ -62,7 +62,7 @@ style="@style/Divider.ForumList" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:layout_below="@+id/unreadView"/> + android:layout_below="@+id/postCountView"/> </RelativeLayout> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 3447cf1ec3a3b136b7f7841edcc5fca35bad4cd1..e3793a54638e9f83418dacf23af35e3f961b918b 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -81,12 +81,12 @@ <string name="forum_leave">Leave Forum</string> <string name="forum_left_toast">Left Forum</string> <string name="forum_sharing_status">Sharing Status</string> - <string name="forum_no_posts">No posts</string> + <string name="no_posts">No posts</string> <plurals name="unread_posts"> <item quantity="one">%d unread post</item> <item quantity="other">%d unread posts</item> </plurals> - <plurals name="forum_posts"> + <plurals name="posts"> <item quantity="one">%d post</item> <item quantity="other">%d posts</item> </plurals> @@ -256,7 +256,9 @@ <string name="blogs_my_blogs_create_hint_title">Blog title (cannot be changed later)</string> <string name="blogs_my_blogs_create_hint_desc">A short description of your new blog</string> <string name="blogs_my_blogs_create_hint_desc_explanation">Potential readers may or may not subscribe to your blog based on the content of the description.</string> + <string name="blogs_my_blogs_empty_state">You don\'t have any blogs.\n\nWhy don\'t you create one now by clicking the plus in the top right screen corner?</string> <string name="blogs_my_blogs_created">Blog created</string> + <string name="blogs_blog_is_empty">This blog is empty</string> <string name="blogs_blog_list">Blog List</string> <string name="blogs_available_blogs">Available Blogs</string> 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 0000000000000000000000000000000000000000..be0ff5e12ffc478be447035692ac00687486e397 --- /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<BlogListAdapter.BlogViewHolder> { + + private SortedList<BlogListItem> blogs = new SortedList<>( + BlogListItem.class, new SortedList.Callback<BlogListItem>() { + + @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<BlogListItem> 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 0000000000000000000000000000000000000000..681dac42f5859f5af9f55d712ec407a770dedaf2 --- /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<BlogPostHeader> 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 cb28dd0ea5500b8afe442b063616b5a57b7bc1d0..d6ef76ae31e889a13001800da7dd8bf34fe13cca 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()); + + 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 v; + 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<BlogListItem> blogs = new ArrayList<>(); + Collection<LocalAuthor> authors = + identityManager.getLocalAuthors(); + LocalAuthor a = authors.iterator().next(); + for (Blog b : blogManager.getBlogs(a)) { + try { + Collection<BlogPostHeader> 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<BlogListItem> 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 17fd01346b17fbedbcf58567a430987c9c77d209..2051eadffdb8c7eb0f0b7e60e7f97a8b27510719 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); } }