Combined Blog Feed

This commit addes a combined blog feed that shows all posts of all
subscribed blogs in the order the blog posts have been received.

For now, this commit also hides other blog functionality like adding
additional blogs and browsing individual blogs.

Closes #417
parent e0d2d09b
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.54"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.54"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="15dp"
android:width="31dp"
android:height="12dp"
android:viewportHeight="20"
android:viewportWidth="49">
<path
......
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="15dp"
android:width="31dp"
android:height="12dp"
android:viewportHeight="20"
android:viewportWidth="49">
<path
......
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="15dp"
android:width="31dp"
android:height="12dp"
android:viewportHeight="20"
android:viewportWidth="49">
<path
......
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="15dp"
android:width="31dp"
android:height="12dp"
android:viewportHeight="20"
android:viewportWidth="49">
<path
......
......@@ -6,14 +6,86 @@
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
android:layout_marginStart="@dimen/listitem_horizontal_margin"
android:layout_marginTop="@dimen/margin_medium"
android:layout_marginTop="@dimen/listitem_vertical_margin"
android:background="?attr/selectableItemBackground">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
style="@style/BriarAvatar"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginBottom="@dimen/margin_medium"
android:layout_marginRight="@dimen/margin_medium"
tools:src="@drawable/ic_launcher"/>
<TextView
android:id="@+id/authorName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_tiny"
tools:text="Author Name"/>
<TextView
android:id="@+id/dateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/avatar"
android:layout_below="@+id/authorName"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:gravity="bottom"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_tiny"
tools:text="yesterday"/>
<TextView
android:id="@+id/newView"
style="@style/BriarTag"
android:layout_alignBottom="@+id/dateView"
android:layout_marginLeft="@dimen/margin_small"
android:layout_toRightOf="@+id/dateView"
android:text="@string/tag_new"
android:visibility="gone"/>
<org.briarproject.android.util.TrustIndicatorView
android:id="@+id/trustIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/authorName"
android:layout_alignTop="@+id/authorName"
android:layout_marginLeft="@dimen/margin_small"
android:layout_toRightOf="@+id/authorName"
android:scaleType="center"
tools:src="@drawable/trust_indicator_verified"/>
<ImageView
android:id="@+id/chatView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/commentView"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_chat"
android:visibility="gone"/>
<ImageView
android:id="@+id/commentView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_repeat"
android:visibility="gone"/>
<TextView
android:id="@+id/titleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_below="@+id/avatar"
android:layout_marginBottom="@dimen/margin_medium"
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
......@@ -35,34 +107,12 @@
android:textSize="@dimen/text_size_medium"
tools:text="This is a body text that shows the content of a blog post. This one is not short, but it is also not too long."/>
<TextView
android:id="@+id/newView"
style="@style/BriarTag"
android:layout_alignBottom="@id/dateView"
android:layout_alignParentLeft="true"
android:text="@string/tag_new"
tools:visibility="visible"/>
<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/bodyView"
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:layout_marginTop="@dimen/margin_small"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24"/>
<View
style="@style/Divider.ForumList"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/dateView"
android:layout_marginTop="@dimen/margin_medium"/>
android:layout_below="@+id/bodyView"
android:layout_marginTop="@dimen/listitem_vertical_margin"/>
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_write_blog_post"
android:icon="@drawable/forum_item_create_white"
android:title="@string/blogs_write_blog_post"
app:showAsAction="always"/>
</menu>
\ No newline at end of file
......@@ -20,6 +20,7 @@
<dimen name="text_size_xlarge">34sp</dimen>
<dimen name="listitem_horizontal_margin">16dp</dimen>
<dimen name="listitem_vertical_margin">10dp</dimen>
<dimen name="listitem_text_left_margin">72dp</dimen>
<dimen name="listitem_height_one_line_avatar">56dp</dimen>
<dimen name="listitem_height_contact_selector">68dp</dimen>
......
......@@ -247,7 +247,7 @@
<string name="progress_title_please_wait">Please wait..</string>
<!-- Blogs -->
<string name="blogs_button">Blogs</string>
<string name="blogs_button">Micro Blogs</string>
<string name="blogs_feed">Feed</string>
<string name="blogs_my_blogs">My Blogs</string>
......@@ -267,8 +267,12 @@
<string name="blogs_write_blog_post_title_hint">Add a title (optional)</string>
<string name="blogs_write_blog_post_body_hint">Type your blog post here</string>
<string name="blogs_publish_blog_post">Publish</string>
<string name="blogs_blog_post_created">Blog Post Created</string>
<string name="blogs_blog_post_received">New Blog Post Received</string>
<string name="blogs_blog_post_scroll_to">Scroll To</string>
<string name="blogs_blog_failed_to_load">Blog failed to load</string>
<string name="blogs_blog_post_failed_to_load">Blog Post failed to load</string>
<string name="blogs_feed_empty_state">This is the global blog feed.\n\nIt looks like nobody blogged anything, yet.\n\nBe the first and tap the pen icon to write a new blog post.</string>
<string name="blogs_delete_blog">Delete Blog</string>
<string name="blogs_delete_blog_dialog_message">Are you sure that you want to delete this Blog and all posts?\nNote that this will not delete the blog from other people\'s devices.</string>
<string name="blogs_delete_blog_ok">Delete Blog</string>
......
......@@ -4,11 +4,14 @@ import android.app.Activity;
import org.briarproject.android.blogs.BlogActivity;
import org.briarproject.android.blogs.BlogFragment;
import org.briarproject.android.blogs.BlogListFragment;
import org.briarproject.android.blogs.BlogPostFragment;
import org.briarproject.android.blogs.BlogsFragment;
import org.briarproject.android.blogs.CreateBlogActivity;
import org.briarproject.android.blogs.FeedFragment;
import org.briarproject.android.blogs.MyBlogsFragment;
import org.briarproject.android.contact.ContactListFragment;
import org.briarproject.android.blogs.WriteBlogPostActivity;
import org.briarproject.android.contact.ContactListFragment;
import org.briarproject.android.contact.ConversationActivity;
import org.briarproject.android.forum.AvailableForumsActivity;
import org.briarproject.android.forum.ContactSelectorFragment;
......@@ -18,7 +21,6 @@ import org.briarproject.android.forum.ForumListFragment;
import org.briarproject.android.forum.ForumSharingStatusActivity;
import org.briarproject.android.forum.ShareForumActivity;
import org.briarproject.android.forum.ShareForumMessageFragment;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.introduction.ContactChooserFragment;
import org.briarproject.android.introduction.IntroductionActivity;
......@@ -88,7 +90,9 @@ public interface ActivityComponent {
// Fragments
void inject(ContactListFragment fragment);
void inject(ForumListFragment fragment);
void inject(BaseFragment fragment);
void inject(BlogsFragment fragment);
void inject(BlogListFragment fragment);
void inject(FeedFragment fragment);
void inject(MyBlogsFragment fragment);
void inject(ChooseIdentityFragment fragment);
void inject(ShowQrCodeFragment fragment);
......
......@@ -6,6 +6,8 @@ import android.content.SharedPreferences;
import org.briarproject.android.blogs.BlogController;
import org.briarproject.android.blogs.BlogControllerImpl;
import org.briarproject.android.blogs.FeedController;
import org.briarproject.android.blogs.FeedControllerImpl;
import org.briarproject.android.controller.BriarController;
import org.briarproject.android.controller.BriarControllerImpl;
import org.briarproject.android.controller.ConfigController;
......@@ -116,6 +118,13 @@ public class ActivityModule {
return blogController;
}
@ActivityScope
@Provides
protected FeedController provideFeedController(
FeedControllerImpl feedController) {
return feedController;
}
@ActivityScope
@Provides
protected NavDrawerController provideNavDrawerController(
......
......@@ -7,15 +7,18 @@ import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.util.TrustIndicatorView;
import org.briarproject.api.identity.Author;
import org.briarproject.util.StringUtils;
import java.util.Collection;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import de.hdodenhof.circleimageview.CircleImageView;
import im.delight.android.identicons.IdenticonDrawable;
class BlogPostAdapter extends
RecyclerView.Adapter<BlogPostAdapter.BlogPostHolder> {
......@@ -76,18 +79,20 @@ class BlogPostAdapter extends
@Override
public void onBindViewHolder(final 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);
}
final BlogPostItem post = getItem(position);
Author author = post.getAuthor();
IdenticonDrawable d = new IdenticonDrawable(author.getId().getBytes());
ui.avatar.setImageDrawable(d);
ui.author.setText(author.getName());
ui.trust.setTrustLevel(post.getAuthorStatus());
// date
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, post.getTimestamp()));
// post body
ui.body.setText(StringUtils.fromUtf8(item.getBody()));
ui.body.setText(StringUtils.fromUtf8(post.getBody()));
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
......@@ -95,14 +100,6 @@ class BlogPostAdapter extends
listener.onBlogPostClick(ui.getAdapterPosition());
}
});
// date
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, item.getTimestamp()));
// new tag
if (item.isRead()) ui.unread.setVisibility(GONE);
else ui.unread.setVisibility(VISIBLE);
}
@Override
......@@ -136,18 +133,28 @@ class BlogPostAdapter extends
static class BlogPostHolder extends RecyclerView.ViewHolder {
private final ViewGroup layout;
private final TextView title;
private final TextView unread;
private final CircleImageView avatar;
private final TextView author;
private final TrustIndicatorView trust;
private final TextView date;
private final TextView unread;
private final ImageView chat;
private final ImageView comment;
private final TextView title;
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);
avatar = (CircleImageView) v.findViewById(R.id.avatar);
author = (TextView) v.findViewById(R.id.authorName);
trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator);
date = (TextView) v.findViewById(R.id.dateView);
unread = (TextView) v.findViewById(R.id.newView);
chat = (ImageView) v.findViewById(R.id.chatView);
comment = (ImageView) v.findViewById(R.id.commentView);
title = (TextView) v.findViewById(R.id.titleView);
body = (TextView) v.findViewById(R.id.bodyView);
}
}
......
......@@ -36,6 +36,10 @@ class BlogPostItem implements Comparable<BlogPostItem> {
return header.getTimestamp();
}
public long getTimeReceived() {
return header.getTimeReceived();
}
public Author getAuthor() {
return header.getAuthor();
}
......@@ -56,7 +60,7 @@ class BlogPostItem implements Comparable<BlogPostItem> {
public int compareTo(@NonNull BlogPostItem other) {
if (this == other) return 0;
// The blog with the newest message comes first
long aTime = getTimestamp(), bTime = other.getTimestamp();
long aTime = getTimeReceived(), bTime = other.getTimeReceived();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
// Break ties by post title
......
......@@ -15,6 +15,8 @@ import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.fragment.BaseFragment;
import static android.view.View.GONE;
public class BlogsFragment extends BaseFragment {
public final static String TAG = BlogsFragment.class.getName();
......@@ -54,6 +56,8 @@ public class BlogsFragment extends BaseFragment {
viewPager.setAdapter(tabAdapter);
tabLayout.setupWithViewPager(viewPager);
tabLayout.setVisibility(GONE);
if (savedInstanceState != null) {
int position = savedInstanceState.getInt(SELECTED_TAB, 0);
viewPager.setCurrentItem(position);
......@@ -88,17 +92,21 @@ public class BlogsFragment extends BaseFragment {
@Override
public int getCount() {
return titles.length;
return 1;
// return titles.length;
}
@Override
public Fragment getItem(int position) {
switch (position) {
case 1:
return new MyBlogsFragment();
default:
return BlogListFragment.newInstance(position);
}
return FeedFragment.newInstance();
// switch (position) {
// case 0:
// return FeedFragment.newInstance();
// case 1:
// return new MyBlogsFragment();
// default:
// return BlogListFragment.newInstance(position);
// }
}
@Override
......
package org.briarproject.android.blogs;
import org.briarproject.android.controller.ActivityLifecycleController;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.api.blogs.Blog;
import java.util.Collection;
public interface FeedController {
void onResume();
void onPause();
void loadPosts(
final UiResultHandler<Collection<BlogPostItem>> resultHandler);
void loadPersonalBlog(final UiResultHandler<Blog> resultHandler);
void setOnBlogPostAddedListener(OnBlogPostAddedListener listener);
interface OnBlogPostAddedListener {
void onBlogPostAdded(final BlogPostItem post);
}
}
package org.briarproject.android.blogs;
import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.UiResultHandler;
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.event.BlogPostAddedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.IdentityManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
public class FeedControllerImpl extends DbControllerImpl
implements FeedController, EventListener {
private static final Logger LOG =
Logger.getLogger(FeedControllerImpl.class.getName());
@Inject
protected volatile BlogManager blogManager;
@Inject
protected volatile IdentityManager identityManager;
@Inject
protected volatile EventBus eventBus;
private volatile OnBlogPostAddedListener listener;
@Inject
FeedControllerImpl() {
}
public void onResume() {
eventBus.addListener(this);
}
public void onPause() {
eventBus.removeListener(this);
}
@Override
public void eventOccurred(Event e) {
if (!(e instanceof BlogPostAddedEvent)) return;
LOG.info("New blog post added");
if (listener != null) {
final BlogPostAddedEvent m = (BlogPostAddedEvent) e;
final BlogPostHeader header = m.getHeader();
try {
final byte[] body = blogManager.getPostBody(header.getId());
final BlogPostItem post = new BlogPostItem(header, body);
listener.onBlogPostAdded(post);
} catch (DbException ex) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, ex.toString(), ex);
}
}
}
@Override
public void loadPosts(
final UiResultHandler<Collection<BlogPostItem>> resultHandler) {
LOG.info("Loading blog posts...");
runOnDbThread(new Runnable() {
@Override
public void run() {
Collection<BlogPostItem> posts = new ArrayList<>();
try {
// load blog posts
long now = System.currentTimeMillis();
for (Blog b : blogManager.getBlogs()) {
Collection<BlogPostHeader> header =
blogManager.getPostHeaders(b.getId());
for (BlogPostHeader h : header) {
byte[] body = blogManager.getPostBody(h.getId());
posts.add(new BlogPostItem(h, body));
}
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading posts took " + duration + " ms");
resultHandler.onResult(posts);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(null);
}
}
});
}
@Override