Add a BlogActivity that shows a list of blog posts

This commit lays the groundwork for #415
This commit is contained in:
Torsten Grote
2016-06-07 17:44:45 -03:00
parent d05237d2c1
commit 365fbb45ad
14 changed files with 540 additions and 14 deletions

View File

@@ -159,6 +159,15 @@
/>
</activity>
<activity
android:name=".android.blogs.BlogActivity"
android:parentActivityName=".android.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.NavDrawerActivity"
/>
</activity>
<activity
android:name=".android.identity.CreateIdentityActivity"
android:label="@string/new_identity_title"

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<org.briarproject.android.util.BriarRecyclerView
android:id="@+id/postList"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:scrollToEnd="false"
tools:context=".android.blogs.BlogActivity"/>

View File

@@ -0,0 +1,68 @@
<?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:layout_marginTop="@dimen/margin_medium"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/titleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginBottom="@dimen/margin_medium"
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:ellipsize="end"
android:maxLines="3"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_large"
android:visibility="gone"
tools:text="This is a blog post title which can also be longer"/>
<TextView
android:id="@+id/bodyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/titleView"
android:layout_marginEnd="@dimen/margin_medium"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:textColor="@color/briar_text_secondary"
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"/>
</RelativeLayout>

View File

@@ -0,0 +1,12 @@
<?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="ifRoom"/>
</menu>

View File

@@ -257,8 +257,13 @@
<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_blog_empty_state">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.</string>
<string name="blogs_my_blogs_created">Blog created</string>
<string name="blogs_blog_is_empty">This blog is empty</string>
<string name="blogs_other_blog_empty_state">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.</string>
<string name="tag_new">NEW</string>
<string name="blogs_post_more">more</string>
<string name="blogs_write_blog_post">Write Blog Post</string>
<string name="blogs_blog_list">Blog List</string>
<string name="blogs_available_blogs">Available Blogs</string>

View File

@@ -57,6 +57,17 @@
<item name="android:textColor">@android:color/primary_text_light</item>
</style>
<style name="BriarTag">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginRight">@dimen/margin_medium</item>
<item name="android:paddingLeft">3dp</item>
<item name="android:paddingRight">3dp</item>
<item name="android:background">@color/briar_primary</item>
<item name="android:textSize">@dimen/text_size_tiny</item>
<item name="android:textColor">@color/briar_text_primary_inverse</item>
</style>
<style name="Divider">
<item name="android:background">@color/divider</item>
</style>

View File

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

View File

@@ -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<BlogPostItem> posts = new ArrayList<>();
try {
Collection<BlogPostHeader> 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<BlogPostItem> 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
}

View File

@@ -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<BlogListAdapter.BlogViewHolder> {
@@ -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());
}
});
}

View File

@@ -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<BlogPostHeader> headers) {
BlogListItem(Blog blog, Collection<BlogPostHeader> 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;
}
}

View File

@@ -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<BlogPostAdapter.BlogPostHolder> {
private SortedList<BlogPostItem> posts = new SortedList<>(
BlogPostItem.class, new SortedList.Callback<BlogPostItem>() {
@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<BlogPostItem> 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);
}
}
}

View File

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

View File

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

View File

@@ -135,7 +135,7 @@ public class MyBlogsFragment extends BaseFragment {
try {
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(b.getId());
blogs.add(new BlogListItem(b, headers));
blogs.add(new BlogListItem(b, headers, true));
} catch (NoSuchGroupException e) {
// Continue
}