diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index fc74fb36e3abd6cca1a40735956a4a311489b9f0..6020c390ae7b5cfb696f052f4b240fb24ef1124e 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -45,9 +45,25 @@
 		    android:label="@string/contact_list_title" >
 		</activity>
 		<activity
-			android:name=".android.groups.CreateBlogActivity"
+			android:name=".android.blogs.BlogActivity"
+			android:label="@string/app_name" >
+		</activity>
+		<activity
+			android:name=".android.blogs.BlogListActivity"
+			android:label="@string/blogs_title" >
+		</activity>
+		<activity
+			android:name=".android.blogs.CreateBlogActivity"
 			android:label="@string/create_blog_title" >
 		</activity>
+		<activity
+			android:name=".android.blogs.ReadBlogPostActivity"
+			android:label="@string/app_name" >
+		</activity>
+		<activity
+			android:name=".android.blogs.WriteBlogPostActivity"
+			android:label="@string/compose_blog_title" >
+		</activity>
 		<activity
 			android:name=".android.groups.CreateGroupActivity"
 			android:label="@string/create_group_title" >
@@ -58,16 +74,12 @@
 		</activity>
 		<activity
 			android:name=".android.groups.GroupListActivity"
-			android:label="@string/app_name" >
+			android:label="@string/groups_title" >
 		</activity>
 		<activity
-			android:name=".android.groups.ReadGroupMessageActivity"
+			android:name=".android.groups.ReadGroupPostActivity"
 			android:label="@string/app_name" >
 		</activity>
-		<activity
-			android:name=".android.groups.WriteBlogPostActivity"
-			android:label="@string/compose_blog_title" >
-		</activity>
 		<activity
 			android:name=".android.groups.WriteGroupPostActivity"
 			android:label="@string/compose_group_title" >
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupNameComparator.java b/briar-android/src/net/sf/briar/android/GroupNameComparator.java
similarity index 52%
rename from briar-android/src/net/sf/briar/android/groups/GroupNameComparator.java
rename to briar-android/src/net/sf/briar/android/GroupNameComparator.java
index 47fdd6b538c7075e70d647e2d12f3ae569cc0769..8e397be1b11430d429e837f26c5343c2044216e7 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupNameComparator.java
+++ b/briar-android/src/net/sf/briar/android/GroupNameComparator.java
@@ -1,12 +1,13 @@
-package net.sf.briar.android.groups;
+package net.sf.briar.android;
 
 import java.util.Comparator;
 
 import net.sf.briar.api.messaging.Group;
 
-class GroupNameComparator implements Comparator<Group> {
+public class GroupNameComparator implements Comparator<Group> {
 
-	static final GroupNameComparator INSTANCE = new GroupNameComparator();
+	public static final GroupNameComparator INSTANCE =
+			new GroupNameComparator();
 
 	public int compare(Group a, Group b) {
 		return String.CASE_INSENSITIVE_ORDER.compare(a.getName(), b.getName());
diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
index 5596b1cecd2b9484a468f4af0c97558a1d7bb9a5..f33052720509bf02f224ede0e71f26f5385b37e8 100644
--- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
+++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
@@ -14,6 +14,7 @@ import java.util.logging.Logger;
 import net.sf.briar.R;
 import net.sf.briar.android.BriarService.BriarBinder;
 import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.blogs.BlogListActivity;
 import net.sf.briar.android.contact.ContactListActivity;
 import net.sf.briar.android.groups.GroupListActivity;
 import net.sf.briar.android.messages.ConversationListActivity;
@@ -183,12 +184,8 @@ public class HomeScreenActivity extends BriarActivity {
 		groupsButton.setText(R.string.groups_button);
 		groupsButton.setOnClickListener(new OnClickListener() {
 			public void onClick(View view) {
-				Intent i = new Intent(HomeScreenActivity.this,
-						GroupListActivity.class);
-				i.putExtra("net.sf.briar.RESTRICTED", false);
-				i.putExtra("net.sf.briar.TITLE",
-						getResources().getString(R.string.groups_title));
-				startActivity(i);
+				startActivity(new Intent(HomeScreenActivity.this,
+						GroupListActivity.class));
 			}
 		});
 		buttons.add(groupsButton);
@@ -201,12 +198,8 @@ public class HomeScreenActivity extends BriarActivity {
 		blogsButton.setText(R.string.blogs_button);
 		blogsButton.setOnClickListener(new OnClickListener() {
 			public void onClick(View view) {
-				Intent i = new Intent(HomeScreenActivity.this,
-						GroupListActivity.class);
-				i.putExtra("net.sf.briar.RESTRICTED", true);
-				i.putExtra("net.sf.briar.TITLE",
-						getResources().getString(R.string.blogs_title));
-				startActivity(i);
+				startActivity(new Intent(HomeScreenActivity.this,
+						BlogListActivity.class));
 			}
 		});
 		buttons.add(blogsButton);
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..b30e2764020ced7dbdbb1dc301cdd7471462128f
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java
@@ -0,0 +1,237 @@
+package net.sf.briar.android.blogs;
+
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static net.sf.briar.android.blogs.ReadBlogPostActivity.RESULT_NEXT;
+import static net.sf.briar.android.blogs.ReadBlogPostActivity.RESULT_PREV;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.AscendingHeaderComparator;
+import net.sf.briar.android.BriarActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.widgets.HorizontalBorder;
+import net.sf.briar.api.Author;
+import net.sf.briar.api.android.DatabaseUiExecutor;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.db.NoSuchSubscriptionException;
+import net.sf.briar.api.db.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.GroupMessageAddedEvent;
+import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.RatingChangedEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.GroupId;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class BlogActivity extends BriarActivity implements DatabaseListener,
+OnClickListener, OnItemClickListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(BlogActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	private String groupName = null;
+	private BlogAdapter adapter = null;
+	private ListView list = null;
+
+	// Fields that are accessed from background threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
+	private volatile GroupId groupId = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(b == null) throw new IllegalStateException();
+		groupId = new GroupId(b);
+		groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+		if(groupName == null) throw new IllegalStateException();
+		setTitle(groupName);
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(MATCH_MATCH);
+		layout.setOrientation(VERTICAL);
+		layout.setGravity(CENTER_HORIZONTAL);
+
+		adapter = new BlogAdapter(this);
+		list = new ListView(this);
+		// Give me all the width and all the unused height
+		list.setLayoutParams(MATCH_WRAP_1);
+		list.setAdapter(adapter);
+		list.setOnItemClickListener(this);
+		layout.addView(list);
+
+		layout.addView(new HorizontalBorder(this));
+
+		ImageButton composeButton = new ImageButton(this);
+		composeButton.setBackgroundResource(0);
+		composeButton.setImageResource(R.drawable.content_new_email);
+		composeButton.setOnClickListener(this);
+		layout.addView(composeButton);
+
+		setContentView(layout);
+
+		// Bind to the service so we can wait for it to start
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		db.addListener(this);
+		loadHeaders();
+	}
+
+	private void loadHeaders() {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					Collection<GroupMessageHeader> headers =
+							db.getMessageHeaders(groupId);
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Load took " + duration + " ms");
+					displayHeaders(headers);
+				} catch(NoSuchSubscriptionException e) {
+					if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
+					finishOnUiThread();
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	private void displayHeaders(final Collection<GroupMessageHeader> headers) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				adapter.clear();
+				for(GroupMessageHeader h : headers) adapter.add(h);
+				adapter.sort(AscendingHeaderComparator.INSTANCE);
+				adapter.notifyDataSetChanged();
+				selectFirstUnread();
+			}
+		});
+	}
+
+	private void selectFirstUnread() {
+		int firstUnread = -1, count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			if(!adapter.getItem(i).isRead()) {
+				firstUnread = i;
+				break;
+			}
+		}
+		if(firstUnread == -1) list.setSelection(count - 1);
+		else list.setSelection(firstUnread);
+	}
+
+	@Override
+	public void onActivityResult(int request, int result, Intent data) {
+		if(result == RESULT_PREV) {
+			int position = request - 1;
+			if(position >= 0 && position < adapter.getCount())
+				displayMessage(position);
+		} else if(result == RESULT_NEXT) {
+			int position = request + 1;
+			if(position >= 0 && position < adapter.getCount())
+				displayMessage(position);
+		}
+	}
+
+	@Override
+	public void onPause() {
+		super.onPause();
+		db.removeListener(this);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof GroupMessageAddedEvent) {
+			GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
+			if(g.getGroup().getId().equals(groupId)) {
+				if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+				loadHeaders();
+			}
+		} else if(e instanceof MessageExpiredEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
+			loadHeaders();
+		} else if(e instanceof RatingChangedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Rating changed, reloading");
+			loadHeaders();
+		} else if(e instanceof SubscriptionRemovedEvent) {
+			SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e;
+			if(s.getGroup().getId().equals(groupId)) {
+				if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
+				finishOnUiThread();
+			}
+		}
+	}
+
+	public void onClick(View view) {
+		Intent i = new Intent(this, WriteBlogPostActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+		startActivity(i);
+	}
+
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		displayMessage(position);
+	}
+
+	private void displayMessage(int position) {
+		GroupMessageHeader item = adapter.getItem(position);
+		Intent i = new Intent(this, ReadBlogPostActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+		i.putExtra("net.sf.briar.GROUP_NAME", groupName);
+		i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
+		Author author = item.getAuthor();
+		if(author != null) {
+			i.putExtra("net.sf.briar.AUTHOR_ID", author.getId().getBytes());
+			i.putExtra("net.sf.briar.AUTHOR_NAME", author.getName());
+			i.putExtra("net.sf.briar.RATING", item.getRating().toString());
+		}
+		i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType());
+		i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp());
+		startActivityForResult(i, position);
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogAdapter.java b/briar-android/src/net/sf/briar/android/blogs/BlogAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..969b520e3687b2fba546a6563878e807ed27c4ec
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogAdapter.java
@@ -0,0 +1,114 @@
+package net.sf.briar.android.blogs;
+
+import static android.graphics.Typeface.BOLD;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.View.INVISIBLE;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1;
+import static net.sf.briar.api.messaging.Rating.GOOD;
+import static net.sf.briar.api.messaging.Rating.UNRATED;
+
+import java.util.ArrayList;
+
+import net.sf.briar.R;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.Author;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.messaging.Rating;
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+class BlogAdapter extends ArrayAdapter<GroupMessageHeader> {
+
+	BlogAdapter(Context ctx) {
+		super(ctx, android.R.layout.simple_expandable_list_item_1,
+				new ArrayList<GroupMessageHeader>());
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		GroupMessageHeader item = getItem(position);
+		Context ctx = getContext();
+
+		// FIXME: Use a RelativeLayout
+		LinearLayout layout = new LinearLayout(ctx);
+		layout.setOrientation(HORIZONTAL);
+		if(!item.isRead()) {
+			Resources res = ctx.getResources();
+			layout.setBackgroundColor(res.getColor(R.color.unread_background));
+		}
+
+		LinearLayout innerLayout = new LinearLayout(ctx);
+		// Give me all the unused width
+		innerLayout.setLayoutParams(WRAP_WRAP_1);
+		innerLayout.setOrientation(VERTICAL);
+
+		LinearLayout authorLayout = new LinearLayout(ctx);
+		authorLayout.setOrientation(HORIZONTAL);
+		authorLayout.setGravity(CENTER_VERTICAL);
+
+		ImageView thumb = new ImageView(ctx);
+		thumb.setPadding(10, 10, 10, 10);
+		Rating rating = item.getRating();
+		if(rating == GOOD) thumb.setImageResource(R.drawable.rating_good);
+		else thumb.setImageResource(R.drawable.rating_bad);
+		if(rating == UNRATED) thumb.setVisibility(INVISIBLE);
+		authorLayout.addView(thumb);
+
+		TextView name = new TextView(ctx);
+		// Give me all the unused width
+		name.setLayoutParams(WRAP_WRAP_1);
+		name.setTextSize(18);
+		name.setMaxLines(1);
+		name.setPadding(0, 10, 10, 10);
+		Author author = item.getAuthor();
+		Resources res = ctx.getResources();
+		if(author == null) {
+			name.setTextColor(res.getColor(R.color.anonymous_author));
+			name.setText(R.string.anonymous);
+		} else {
+			name.setText(author.getName());
+		}
+		authorLayout.addView(name);
+		innerLayout.addView(authorLayout);
+
+		if(item.getContentType().equals("text/plain")) {
+			TextView subject = new TextView(ctx);
+			subject.setTextSize(14);
+			subject.setMaxLines(2);
+			subject.setPadding(10, 0, 10, 10);
+			if(!item.isRead()) subject.setTypeface(null, BOLD);
+			String s = item.getSubject();
+			subject.setText(s == null ? "" : s);
+			innerLayout.addView(subject);
+		} else {
+			LinearLayout attachmentLayout = new LinearLayout(ctx);
+			attachmentLayout.setOrientation(HORIZONTAL);
+			ImageView attachment = new ImageView(ctx);
+			attachment.setPadding(10, 0, 10, 10);
+			attachment.setImageResource(R.drawable.content_attachment);
+			attachmentLayout.addView(attachment);
+			attachmentLayout.addView(new HorizontalSpace(ctx));
+			innerLayout.addView(attachmentLayout);
+		}
+		layout.addView(innerLayout);
+
+		TextView date = new TextView(ctx);
+		date.setTextSize(14);
+		date.setPadding(0, 10, 10, 10);
+		long then = item.getTimestamp(), now = System.currentTimeMillis();
+		date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
+		layout.addView(date);
+
+		return layout;
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..b6dfdfdcabbeb29e286a0ef8a2c722391497a2a8
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
@@ -0,0 +1,314 @@
+package net.sf.briar.android.blogs;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.BriarFragmentActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.widgets.HorizontalBorder;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.android.DatabaseUiExecutor;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.db.NoSuchSubscriptionException;
+import net.sf.briar.api.db.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.GroupMessageAddedEvent;
+import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.Group;
+import net.sf.briar.api.messaging.GroupId;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class BlogListActivity extends BriarFragmentActivity
+implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
+
+	private static final Logger LOG =
+			Logger.getLogger(BlogListActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	private BlogListAdapter adapter = null;
+	private ListView list = null;
+	private ImageButton newBlogButton = null, composeButton = null;
+
+	// Fields that are accessed from background threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(MATCH_MATCH);
+		layout.setOrientation(VERTICAL);
+		layout.setGravity(CENTER_HORIZONTAL);
+
+		adapter = new BlogListAdapter(this);
+		list = new ListView(this);
+		// Give me all the width and all the unused height
+		list.setLayoutParams(MATCH_WRAP_1);
+		list.setAdapter(adapter);
+		list.setOnItemClickListener(adapter);
+		layout.addView(list);
+
+		layout.addView(new HorizontalBorder(this));
+
+		LinearLayout footer = new LinearLayout(this);
+		footer.setLayoutParams(MATCH_WRAP);
+		footer.setOrientation(HORIZONTAL);
+		footer.setGravity(CENTER);
+		footer.addView(new HorizontalSpace(this));
+
+		newBlogButton = new ImageButton(this);
+		newBlogButton.setBackgroundResource(0);
+		newBlogButton.setImageResource(R.drawable.social_new_blog);
+		newBlogButton.setOnClickListener(this);
+		footer.addView(newBlogButton);
+		footer.addView(new HorizontalSpace(this));
+
+		composeButton = new ImageButton(this);
+		composeButton.setBackgroundResource(0);
+		composeButton.setImageResource(R.drawable.content_new_email);
+		composeButton.setOnClickListener(this);
+		footer.addView(composeButton);
+		footer.addView(new HorizontalSpace(this));
+		layout.addView(footer);
+
+		setContentView(layout);
+
+		// Bind to the service so we can wait for it to start
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		db.addListener(this);
+		loadHeaders();
+	}
+
+	private void loadHeaders() {
+		clearHeaders();
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					Set<GroupId> local = new HashSet<GroupId>();
+					for(Group g : db.getLocalGroups()) local.add(g.getId());
+					for(Group g : db.getSubscriptions()) {
+						if(!g.isRestricted()) continue;
+						boolean postable = local.contains(g.getId());
+						try {
+							Collection<GroupMessageHeader> headers =
+									db.getMessageHeaders(g.getId());
+							displayHeaders(g, postable, headers);
+						} catch(NoSuchSubscriptionException e) {
+							if(LOG.isLoggable(INFO))
+								LOG.info("Subscription removed");
+						}
+					}
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Full load took " + duration + " ms");
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	private void clearHeaders() {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				adapter.clear();
+			}
+		});
+	}
+
+	private void displayHeaders(final Group g, final boolean postable,
+			final Collection<GroupMessageHeader> headers) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				// Remove the old item, if any
+				BlogListItem item = findGroup(g.getId());
+				if(item != null) adapter.remove(item);
+				// Add a new item
+				adapter.add(new BlogListItem(g, postable, headers));
+				adapter.sort(GroupComparator.INSTANCE);
+				adapter.notifyDataSetChanged();
+				selectFirstUnread();
+			} 
+		});
+	}
+
+	private BlogListItem findGroup(GroupId g) {
+		int count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			BlogListItem item = adapter.getItem(i);
+			if(item.getGroupId().equals(g)) return item;
+		}
+		return null; // Not found
+	}
+
+	private void selectFirstUnread() {
+		int firstUnread = -1, count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			if(adapter.getItem(i).getUnreadCount() > 0) {
+				firstUnread = i;
+				break;
+			}
+		}
+		if(firstUnread == -1) list.setSelection(count - 1);
+		else list.setSelection(firstUnread);
+	}
+
+	@Override
+	public void onPause() {
+		super.onPause();
+		db.removeListener(this);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		if(view == newBlogButton) {
+			startActivity(new Intent(this, CreateBlogActivity.class));
+		} else if(view == composeButton) {
+			if(countPostableGroups() == 0) {
+				NoBlogsDialog dialog = new NoBlogsDialog();
+				dialog.setListener(this);
+				dialog.show(getSupportFragmentManager(), "NoGroupsDialog");
+			} else {
+				startActivity(new Intent(this, WriteBlogPostActivity.class));
+			}
+		}
+	}
+
+	private int countPostableGroups() {
+		int postable = 0, count = adapter.getCount();
+		for(int i = 0; i < count; i++)
+			if(adapter.getItem(i).isPostable()) postable++;
+		return postable;
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof GroupMessageAddedEvent) {
+			Group g = ((GroupMessageAddedEvent) e).getGroup();
+			if(g.isRestricted()) {
+				if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+				loadHeaders(g);
+			}
+		} else if(e instanceof MessageExpiredEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
+			loadHeaders();
+		} else if(e instanceof SubscriptionRemovedEvent) {
+			Group g = ((SubscriptionRemovedEvent) e).getGroup();
+			if(g.isRestricted()) {
+				// Reload the group, expecting NoSuchSubscriptionException
+				if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
+				loadHeaders(g);
+			}
+		}
+	}
+
+	private void loadHeaders(final Group g) {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					Collection<GroupMessageHeader> headers =
+							db.getMessageHeaders(g.getId());
+					boolean postable = db.getLocalGroups().contains(g);
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Partial load took " + duration + " ms");
+					displayHeaders(g, postable, headers);
+				} catch(NoSuchSubscriptionException e) {
+					if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
+					removeGroup(g.getId());
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	private void removeGroup(final GroupId g) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				BlogListItem item = findGroup(g);
+				if(item != null) {
+					adapter.remove(item);
+					selectFirstUnread();
+				}
+			}
+		});
+	}
+
+	public void createGroupButtonClicked() {
+		startActivity(new Intent(this, CreateBlogActivity.class));
+	}
+
+	public void cancelButtonClicked() {
+		// That's nice dear
+	}
+
+	private static class GroupComparator implements Comparator<BlogListItem> {
+
+		private static final GroupComparator INSTANCE = new GroupComparator();
+
+		public int compare(BlogListItem a, BlogListItem b) {
+			// The item 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 group name
+			String aName = a.getGroupName(), bName = b.getGroupName();
+			return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
+		}
+	}
+}
\ No newline at end of file
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java b/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..f37c1f04f20edcf3ca3aa8d9a6ae91f5f56d0396
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java
@@ -0,0 +1,108 @@
+package net.sf.briar.android.blogs;
+
+import static android.graphics.Typeface.BOLD;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1;
+
+import java.util.ArrayList;
+
+import net.sf.briar.R;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+class BlogListAdapter extends ArrayAdapter<BlogListItem>
+implements OnItemClickListener {
+
+	BlogListAdapter(Context ctx) {
+		super(ctx, android.R.layout.simple_expandable_list_item_1,
+				new ArrayList<BlogListItem>());
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		BlogListItem item = getItem(position);
+		Context ctx = getContext();
+		Resources res = ctx.getResources();
+
+		LinearLayout layout = new LinearLayout(ctx);
+		layout.setOrientation(HORIZONTAL);
+		if(item.getUnreadCount() > 0)
+			layout.setBackgroundColor(res.getColor(R.color.unread_background));
+
+		LinearLayout innerLayout = new LinearLayout(ctx);
+		// Give me all the unused width
+		innerLayout.setLayoutParams(WRAP_WRAP_1);
+		innerLayout.setOrientation(VERTICAL);
+
+		TextView name = new TextView(ctx);
+		name.setTextSize(18);
+		name.setMaxLines(1);
+		name.setPadding(10, 10, 10, 10);
+		int unread = item.getUnreadCount();
+		if(unread > 0) name.setText(item.getGroupName() + " (" + unread + ")");
+		else name.setText(item.getGroupName());
+		innerLayout.addView(name);
+
+		if(item.isEmpty()) {
+			TextView noPosts = new TextView(ctx);
+			noPosts.setTextSize(14);
+			noPosts.setPadding(10, 0, 10, 10);
+			noPosts.setTextColor(res.getColor(R.color.no_posts));
+			noPosts.setText(R.string.no_posts);
+			innerLayout.addView(noPosts);
+			layout.addView(innerLayout);
+		} else {
+			if(item.getContentType().equals("text/plain")) {
+				TextView subject = new TextView(ctx);
+				subject.setTextSize(14);
+				subject.setMaxLines(2);
+				subject.setPadding(10, 0, 10, 10);
+				if(item.getUnreadCount() > 0) subject.setTypeface(null, BOLD);
+				String s = item.getSubject();
+				subject.setText(s == null ? "" : s);
+				innerLayout.addView(subject);
+			} else {
+				LinearLayout attachmentLayout = new LinearLayout(ctx);
+				attachmentLayout.setOrientation(HORIZONTAL);
+				ImageView attachment = new ImageView(ctx);
+				attachment.setPadding(10, 0, 10, 10);
+				attachment.setImageResource(R.drawable.content_attachment);
+				attachmentLayout.addView(attachment);
+				attachmentLayout.addView(new HorizontalSpace(ctx));
+				innerLayout.addView(attachmentLayout);
+			}
+			layout.addView(innerLayout);
+
+			TextView date = new TextView(ctx);
+			date.setTextSize(14);
+			date.setPadding(0, 10, 10, 10);
+			long then = item.getTimestamp(), now = System.currentTimeMillis();
+			date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
+			layout.addView(date);
+		}
+
+		return layout;
+	}
+
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		BlogListItem item = getItem(position);
+		Intent i = new Intent(getContext(), BlogActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", item.getGroupId().getBytes());
+		i.putExtra("net.sf.briar.GROUP_NAME", item.getGroupName());
+		getContext().startActivity(i);
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java b/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d955fcc53c41e32e247d3d086242c89a97b3133
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java
@@ -0,0 +1,85 @@
+package net.sf.briar.android.blogs;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import net.sf.briar.android.DescendingHeaderComparator;
+import net.sf.briar.api.Author;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.messaging.Group;
+import net.sf.briar.api.messaging.GroupId;
+
+class BlogListItem {
+
+	private final Group group;
+	private final boolean postable, empty;
+	private final String authorName, contentType, subject;
+	private final long timestamp;
+	private final int unread;
+
+	BlogListItem(Group group, boolean postable,
+			Collection<GroupMessageHeader> headers) {
+		this.group = group;
+		this.postable = postable;
+		empty = headers.isEmpty();
+		if(empty) {
+			authorName = null;
+			contentType = null;
+			subject = null;
+			timestamp = 0;
+			unread = 0;
+		} else {
+			List<GroupMessageHeader> list =
+					new ArrayList<GroupMessageHeader>(headers);
+			Collections.sort(list, DescendingHeaderComparator.INSTANCE);
+			GroupMessageHeader newest = list.get(0);
+			Author a = newest.getAuthor();
+			if(a == null) authorName = null;
+			else authorName = a.getName();
+			contentType = newest.getContentType();
+			subject = newest.getSubject();
+			timestamp = newest.getTimestamp();
+			int unread = 0;
+			for(GroupMessageHeader h : list) if(!h.isRead()) unread++;
+			this.unread = unread;
+		}
+	}
+
+	GroupId getGroupId() {
+		return group.getId();
+	}
+
+	String getGroupName() {
+		return group.getName();
+	}
+
+	boolean isPostable() {
+		return postable;
+	}
+
+	boolean isEmpty() {
+		return empty;
+	}
+
+	String getAuthorName() {
+		return authorName;
+	}
+
+	String getContentType() {
+		return contentType;
+	}
+
+	String getSubject() {
+		return subject;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	int getUnreadCount() {
+		return unread;
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/CreateBlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java
similarity index 99%
rename from briar-android/src/net/sf/briar/android/groups/CreateBlogActivity.java
rename to briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java
index 46f6c9b8fe0d0743b6d826ff186cd97b390dd0cf..6d9ce55b7b98533db58b886abac57b28faec7870 100644
--- a/briar-android/src/net/sf/briar/android/groups/CreateBlogActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.groups;
+package net.sf.briar.android.blogs;
 
 import static android.text.InputType.TYPE_CLASS_TEXT;
 import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
diff --git a/briar-android/src/net/sf/briar/android/groups/LocalGroupSpinnerAdapter.java b/briar-android/src/net/sf/briar/android/blogs/LocalGroupSpinnerAdapter.java
similarity index 96%
rename from briar-android/src/net/sf/briar/android/groups/LocalGroupSpinnerAdapter.java
rename to briar-android/src/net/sf/briar/android/blogs/LocalGroupSpinnerAdapter.java
index 34a2f3ffa94c7f1224b423fc6aab4d012ee326e6..8dc22083a4bfb2b9e8625730bf5591cc515a6c94 100644
--- a/briar-android/src/net/sf/briar/android/groups/LocalGroupSpinnerAdapter.java
+++ b/briar-android/src/net/sf/briar/android/blogs/LocalGroupSpinnerAdapter.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.groups;
+package net.sf.briar.android.blogs;
 
 import java.util.ArrayList;
 
diff --git a/briar-android/src/net/sf/briar/android/blogs/NoBlogsDialog.java b/briar-android/src/net/sf/briar/android/blogs/NoBlogsDialog.java
new file mode 100644
index 0000000000000000000000000000000000000000..15b15cfbeae485c8b574e4b52bdfe474aba5bc28
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/NoBlogsDialog.java
@@ -0,0 +1,43 @@
+package net.sf.briar.android.blogs;
+
+import net.sf.briar.R;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+public class NoBlogsDialog extends DialogFragment {
+
+	private Listener listener = null;
+
+	void setListener(Listener listener) {
+		this.listener = listener;
+	}
+
+	@Override
+	public Dialog onCreateDialog(Bundle state) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+		builder.setMessage(R.string.no_blogs);
+		builder.setPositiveButton(R.string.create_button,
+				new DialogInterface.OnClickListener() {
+			public void onClick(DialogInterface dialog, int id) {
+				listener.createGroupButtonClicked();
+			}
+		});
+		builder.setNegativeButton(R.string.cancel_button,
+				new DialogInterface.OnClickListener() {
+			public void onClick(DialogInterface dialog, int id) {
+				listener.cancelButtonClicked();
+			}
+		});
+		return builder.create();
+	}
+
+	interface Listener {
+
+		void createGroupButtonClicked();
+
+		void cancelButtonClicked();
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java b/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..bb97cbca536547bb44c9dc093b2986a07e3442bf
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java
@@ -0,0 +1,375 @@
+package net.sf.briar.android.blogs;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
+import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1;
+import static net.sf.briar.api.messaging.Rating.BAD;
+import static net.sf.briar.api.messaging.Rating.GOOD;
+import static net.sf.briar.api.messaging.Rating.UNRATED;
+
+import java.io.UnsupportedEncodingException;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.BriarActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.widgets.HorizontalBorder;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.AuthorId;
+import net.sf.briar.api.android.BundleEncrypter;
+import net.sf.briar.api.android.DatabaseUiExecutor;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.NoSuchMessageException;
+import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.MessageId;
+import net.sf.briar.api.messaging.Rating;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.google.inject.Inject;
+
+public class ReadBlogPostActivity extends BriarActivity
+implements OnClickListener {
+
+	static final int RESULT_REPLY = RESULT_FIRST_USER;
+	static final int RESULT_PREV = RESULT_FIRST_USER + 1;
+	static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
+
+	private static final Logger LOG =
+			Logger.getLogger(ReadBlogPostActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private BundleEncrypter bundleEncrypter;
+	private GroupId groupId = null;
+	private Rating rating = UNRATED;
+	private boolean read;
+	private ImageView thumb = null;
+	private ImageButton goodButton = null, badButton = null, readButton = null;
+	private ImageButton prevButton = null, nextButton = null;
+	private ImageButton replyButton = null;
+	private TextView content = null;
+
+	// Fields that are accessed from background threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
+	private volatile MessageId messageId = null;
+	private volatile AuthorId authorId = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(b == null) throw new IllegalStateException();
+		groupId = new GroupId(b);
+		String groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+		if(groupName == null) throw new IllegalStateException();
+		setTitle(groupName);
+		b = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID");
+		if(b == null) throw new IllegalStateException();
+		messageId = new MessageId(b);
+		String authorName = null;
+		b = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
+		if(b != null) {
+			authorId = new AuthorId(b);
+			authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME");
+			if(authorName == null) throw new IllegalStateException();
+			String r = i.getStringExtra("net.sf.briar.RATING");
+			if(r != null) rating = Rating.valueOf(r);
+		}
+		String contentType = i.getStringExtra("net.sf.briar.CONTENT_TYPE");
+		if(contentType == null) throw new IllegalStateException();
+		long timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1);
+		if(timestamp == -1) throw new IllegalStateException();
+
+		if(state != null && bundleEncrypter.decrypt(state)) {
+			read = state.getBoolean("net.sf.briar.READ");
+		} else {
+			read = false;
+			setReadInDatabase(true);
+		}
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(MATCH_WRAP);
+		layout.setOrientation(VERTICAL);
+
+		ScrollView scrollView = new ScrollView(this);
+		// Give me all the width and all the unused height
+		scrollView.setLayoutParams(MATCH_WRAP_1);
+
+		LinearLayout message = new LinearLayout(this);
+		message.setOrientation(VERTICAL);
+		Resources res = getResources();
+		message.setBackgroundColor(res.getColor(R.color.content_background));
+
+		LinearLayout header = new LinearLayout(this);
+		header.setLayoutParams(MATCH_WRAP);
+		header.setOrientation(HORIZONTAL);
+		header.setGravity(CENTER_VERTICAL);
+
+		thumb = new ImageView(this);
+		thumb.setPadding(0, 10, 10, 10);
+		if(rating == GOOD) thumb.setImageResource(R.drawable.rating_good);
+		else thumb.setImageResource(R.drawable.rating_bad);
+		if(rating == UNRATED) thumb.setVisibility(INVISIBLE);
+		header.addView(thumb);
+
+		TextView author = new TextView(this);
+		// Give me all the unused width
+		author.setLayoutParams(WRAP_WRAP_1);
+		author.setTextSize(18);
+		author.setMaxLines(1);
+		author.setPadding(10, 10, 10, 10);
+		if(authorName == null) {
+			author.setTextColor(res.getColor(R.color.anonymous_author));
+			author.setText(R.string.anonymous);
+		} else {
+			author.setText(authorName);
+		}
+		header.addView(author);
+
+		TextView date = new TextView(this);
+		date.setTextSize(14);
+		date.setPadding(0, 10, 10, 10);
+		long now = System.currentTimeMillis();
+		date.setText(DateUtils.formatSameDayTime(timestamp, now, SHORT, SHORT));
+		header.addView(date);
+		message.addView(header);
+
+		if(contentType.equals("text/plain")) {
+			// Load and display the message body
+			content = new TextView(this);
+			content.setPadding(10, 0, 10, 10);
+			message.addView(content);
+			loadMessageBody();
+		}
+		scrollView.addView(message);
+		layout.addView(scrollView);
+
+		layout.addView(new HorizontalBorder(this));
+
+		LinearLayout footer = new LinearLayout(this);
+		footer.setLayoutParams(MATCH_WRAP);
+		footer.setOrientation(HORIZONTAL);
+		footer.setGravity(CENTER);
+
+		goodButton = new ImageButton(this);
+		goodButton.setBackgroundResource(0);
+		goodButton.setImageResource(R.drawable.rating_good);
+		if(authorName == null) goodButton.setEnabled(false);
+		else goodButton.setOnClickListener(this);
+		footer.addView(goodButton);
+		footer.addView(new HorizontalSpace(this));
+
+		badButton = new ImageButton(this);
+		badButton.setBackgroundResource(0);
+		badButton.setImageResource(R.drawable.rating_bad);
+		badButton.setOnClickListener(this);
+		if(authorName == null) badButton.setEnabled(false);
+		else badButton.setOnClickListener(this);
+		footer.addView(badButton);
+		footer.addView(new HorizontalSpace(this));
+
+		readButton = new ImageButton(this);
+		readButton.setBackgroundResource(0);
+		if(read) readButton.setImageResource(R.drawable.content_unread);
+		else readButton.setImageResource(R.drawable.content_read);
+		readButton.setOnClickListener(this);
+		footer.addView(readButton);
+		footer.addView(new HorizontalSpace(this));
+
+		prevButton = new ImageButton(this);
+		prevButton.setBackgroundResource(0);
+		prevButton.setImageResource(R.drawable.navigation_previous_item);
+		prevButton.setOnClickListener(this);
+		footer.addView(prevButton);
+		footer.addView(new HorizontalSpace(this));
+
+		nextButton = new ImageButton(this);
+		nextButton.setBackgroundResource(0);
+		nextButton.setImageResource(R.drawable.navigation_next_item);
+		nextButton.setOnClickListener(this);
+		footer.addView(nextButton);
+		footer.addView(new HorizontalSpace(this));
+
+		replyButton = new ImageButton(this);
+		replyButton.setBackgroundResource(0);
+		replyButton.setImageResource(R.drawable.social_reply_all);
+		replyButton.setOnClickListener(this);
+		footer.addView(replyButton);
+		layout.addView(footer);
+
+		setContentView(layout);
+
+		// Bind to the service so we can wait for it to start
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	private void setReadInDatabase(final boolean read) {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					db.setReadFlag(messageId, read);
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Setting flag took " + duration + " ms");
+					setReadInUi(read);
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	private void setReadInUi(final boolean read) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				ReadBlogPostActivity.this.read = read;
+				if(read) readButton.setImageResource(R.drawable.content_unread);
+				else readButton.setImageResource(R.drawable.content_read);
+			}
+		});
+	}
+
+	private void loadMessageBody() {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					byte[] body = db.getMessageBody(messageId);
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Loading message took " + duration + " ms");
+					final String text = new String(body, "UTF-8");
+					runOnUiThread(new Runnable() {
+						public void run() {
+							content.setText(text);
+						}
+					});
+				} catch(NoSuchMessageException e) {
+					if(LOG.isLoggable(INFO)) LOG.info("Message removed");
+					finishOnUiThread();
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				} catch(UnsupportedEncodingException e) {
+					throw new RuntimeException(e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void onSaveInstanceState(Bundle state) {
+		state.putBoolean("net.sf.briar.READ", read);
+		bundleEncrypter.encrypt(state);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		if(view == goodButton) {
+			if(rating == BAD) setRatingInDatabase(UNRATED);
+			else if(rating == UNRATED) setRatingInDatabase(GOOD);
+		} else if(view == badButton) {
+			if(rating == GOOD) setRatingInDatabase(UNRATED);
+			else if(rating == UNRATED) setRatingInDatabase(BAD);
+		} else if(view == readButton) {
+			setReadInDatabase(!read);
+		} else if(view == prevButton) {
+			setResult(RESULT_PREV);
+			finish();
+		} else if(view == nextButton) {
+			setResult(RESULT_NEXT);
+			finish();
+		} else if(view == replyButton) {
+			Intent i = new Intent(this, WriteBlogPostActivity.class);
+			i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+			i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
+			startActivity(i);
+			setResult(RESULT_REPLY);
+			finish();
+		}
+	}
+
+	private void setRatingInDatabase(final Rating r) {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					db.setRating(authorId, r);
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Setting rating took " + duration + " ms");
+					setRatingInUi(r);
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	private void setRatingInUi(final Rating r) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				rating = r;
+				if(r == GOOD) {
+					thumb.setImageResource(R.drawable.rating_good);
+					thumb.setVisibility(VISIBLE);
+				} else if(r == BAD) {
+					thumb.setImageResource(R.drawable.rating_bad);
+					thumb.setVisibility(VISIBLE);
+				} else {
+					thumb.setVisibility(INVISIBLE);
+				}
+			}
+		});
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/WriteBlogPostActivity.java b/briar-android/src/net/sf/briar/android/blogs/WriteBlogPostActivity.java
similarity index 99%
rename from briar-android/src/net/sf/briar/android/groups/WriteBlogPostActivity.java
rename to briar-android/src/net/sf/briar/android/blogs/WriteBlogPostActivity.java
index 37c8c79c8897b90fc9d1f60614441a2e380a844e..15a807ab437f79ce42abb809c95304e8e2cdcbd5 100644
--- a/briar-android/src/net/sf/briar/android/groups/WriteBlogPostActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/WriteBlogPostActivity.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.groups;
+package net.sf.briar.android.blogs;
 
 import static android.text.InputType.TYPE_CLASS_TEXT;
 import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
@@ -22,6 +22,7 @@ import net.sf.briar.R;
 import net.sf.briar.android.BriarActivity;
 import net.sf.briar.android.BriarService;
 import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.GroupNameComparator;
 import net.sf.briar.android.identity.CreateIdentityActivity;
 import net.sf.briar.android.identity.LocalAuthorItem;
 import net.sf.briar.android.identity.LocalAuthorItemComparator;
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
index 710eb668a6882d256eeff3ca3b2ddae859c17530..0cdd423055dccde1550d623c11fe7be6d7770ece 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
@@ -4,8 +4,8 @@ import static android.view.Gravity.CENTER_HORIZONTAL;
 import static android.widget.LinearLayout.VERTICAL;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
-import static net.sf.briar.android.groups.ReadGroupMessageActivity.RESULT_NEXT;
-import static net.sf.briar.android.groups.ReadGroupMessageActivity.RESULT_PREV;
+import static net.sf.briar.android.groups.ReadGroupPostActivity.RESULT_NEXT;
+import static net.sf.briar.android.groups.ReadGroupPostActivity.RESULT_PREV;
 import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
 import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
 
@@ -53,7 +53,6 @@ OnClickListener, OnItemClickListener {
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
 
-	private boolean restricted = false;
 	private String groupName = null;
 	private GroupAdapter adapter = null;
 	private ListView list = null;
@@ -68,7 +67,6 @@ OnClickListener, OnItemClickListener {
 		super.onCreate(null);
 
 		Intent i = getIntent();
-		restricted = i.getBooleanExtra("net.sf.briar.RESTRICTED", false);
 		byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
 		if(b == null) throw new IllegalStateException();
 		groupId = new GroupId(b);
@@ -210,15 +208,9 @@ OnClickListener, OnItemClickListener {
 	}
 
 	public void onClick(View view) {
-		if(restricted) {
-			Intent i = new Intent(this, WriteBlogPostActivity.class);
-			i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
-			startActivity(i);
-		} else {
-			Intent i = new Intent(this, WriteGroupPostActivity.class);
-			i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
-			startActivity(i);
-		}
+		Intent i = new Intent(this, WriteGroupPostActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+		startActivity(i);
 	}
 
 	public void onItemClick(AdapterView<?> parent, View view, int position,
@@ -228,8 +220,7 @@ OnClickListener, OnItemClickListener {
 
 	private void displayMessage(int position) {
 		GroupMessageHeader item = adapter.getItem(position);
-		Intent i = new Intent(this, ReadGroupMessageActivity.class);
-		i.putExtra("net.sf.briar.RESTRICTED", restricted);
+		Intent i = new Intent(this, ReadGroupPostActivity.class);
 		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
 		i.putExtra("net.sf.briar.GROUP_NAME", groupName);
 		i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
index ccbfa93dcd53f4f30765c3b861a532a467eb5a56..083497f91f7849d551e02b62bc8b5436ba0d7f07 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
@@ -12,8 +12,6 @@ import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
 
 import java.util.Collection;
 import java.util.Comparator;
-import java.util.HashSet;
-import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -61,7 +59,6 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 	// Fields that are accessed from background threads must be volatile
 	@Inject private volatile DatabaseComponent db;
 	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
-	private volatile boolean restricted = false;
 
 	@Override
 	public void onCreate(Bundle state) {
@@ -71,12 +68,6 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 		layout.setOrientation(VERTICAL);
 		layout.setGravity(CENTER_HORIZONTAL);
 
-		Intent i = getIntent();
-		restricted = i.getBooleanExtra("net.sf.briar.RESTRICTED", false);
-		String title = i.getStringExtra("net.sf.briar.TITLE");
-		if(title == null) throw new IllegalStateException();
-		setTitle(title);
-
 		adapter = new GroupListAdapter(this);
 		list = new ListView(this);
 		// Give me all the width and all the unused height
@@ -95,9 +86,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 
 		newGroupButton = new ImageButton(this);
 		newGroupButton.setBackgroundResource(0);
-		if(restricted)
-			newGroupButton.setImageResource(R.drawable.social_new_blog);
-		else newGroupButton.setImageResource(R.drawable.social_new_chat);
+		newGroupButton.setImageResource(R.drawable.social_new_chat);
 		newGroupButton.setOnClickListener(this);
 		footer.addView(newGroupButton);
 		footer.addView(new HorizontalSpace(this));
@@ -131,33 +120,15 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 				try {
 					serviceConnection.waitForStartup();
 					long now = System.currentTimeMillis();
-					Collection<Group> subs = db.getSubscriptions();
-					if(restricted) {
-						Set<GroupId> local = new HashSet<GroupId>();
-						for(Group g : db.getLocalGroups()) local.add(g.getId());
-						for(Group g : subs) {
-							if(!g.isRestricted()) continue;
-							boolean postable = local.contains(g.getId());
-							try {
-								Collection<GroupMessageHeader> headers =
-										db.getMessageHeaders(g.getId());
-								displayHeaders(g, postable, headers);
-							} catch(NoSuchSubscriptionException e) {
-								if(LOG.isLoggable(INFO))
-									LOG.info("Subscription removed");
-							}
-						}
-					} else {
-						for(Group g : subs) {
-							if(g.isRestricted()) continue;
-							try {
-								Collection<GroupMessageHeader> headers =
-										db.getMessageHeaders(g.getId());
-								displayHeaders(g, true, headers);
-							} catch(NoSuchSubscriptionException e) {
-								if(LOG.isLoggable(INFO))
-									LOG.info("Subscription removed");
-							}
+					for(Group g : db.getSubscriptions()) {
+						if(g.isRestricted()) continue;
+						try {
+							Collection<GroupMessageHeader> headers =
+									db.getMessageHeaders(g.getId());
+							displayHeaders(g, headers);
+						} catch(NoSuchSubscriptionException e) {
+							if(LOG.isLoggable(INFO))
+								LOG.info("Subscription removed");
 						}
 					}
 					long duration = System.currentTimeMillis() - now;
@@ -183,7 +154,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 		});
 	}
 
-	private void displayHeaders(final Group g, final boolean postable,
+	private void displayHeaders(final Group g,
 			final Collection<GroupMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
@@ -191,7 +162,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 				GroupListItem item = findGroup(g.getId());
 				if(item != null) adapter.remove(item);
 				// Add a new item
-				adapter.add(new GroupListItem(g, postable, headers));
+				adapter.add(new GroupListItem(g, headers));
 				adapter.sort(GroupComparator.INSTANCE);
 				adapter.notifyDataSetChanged();
 				selectFirstUnread();
@@ -234,34 +205,22 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 
 	public void onClick(View view) {
 		if(view == newGroupButton) {
-			if(restricted)
-				startActivity(new Intent(this, CreateBlogActivity.class));
-			else startActivity(new Intent(this, CreateGroupActivity.class));
+			startActivity(new Intent(this, CreateGroupActivity.class));
 		} else if(view == composeButton) {
-			if(countPostableGroups() == 0) {
+			if(adapter.isEmpty()) {
 				NoGroupsDialog dialog = new NoGroupsDialog();
 				dialog.setListener(this);
-				dialog.setRestricted(restricted);
 				dialog.show(getSupportFragmentManager(), "NoGroupsDialog");
-			} else if(restricted) {
-				startActivity(new Intent(this, WriteBlogPostActivity.class));
 			} else {
 				startActivity(new Intent(this, WriteGroupPostActivity.class));
 			}
 		}
 	}
 
-	private int countPostableGroups() {
-		int postable = 0, count = adapter.getCount();
-		for(int i = 0; i < count; i++)
-			if(adapter.getItem(i).isPostable()) postable++;
-		return postable;
-	}
-
 	public void eventOccurred(DatabaseEvent e) {
 		if(e instanceof GroupMessageAddedEvent) {
 			Group g = ((GroupMessageAddedEvent) e).getGroup();
-			if(g.isRestricted() == restricted) {
+			if(!g.isRestricted()) {
 				if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
 				loadHeaders(g);
 			}
@@ -270,7 +229,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 			loadHeaders();
 		} else if(e instanceof SubscriptionRemovedEvent) {
 			Group g = ((SubscriptionRemovedEvent) e).getGroup();
-			if(g.isRestricted() == restricted) {
+			if(!g.isRestricted()) {
 				// Reload the group, expecting NoSuchSubscriptionException
 				if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
 				loadHeaders(g);
@@ -284,15 +243,12 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 				try {
 					serviceConnection.waitForStartup();
 					long now = System.currentTimeMillis();
-					boolean postable;
-					if(restricted) postable = db.getLocalGroups().contains(g);
-					else postable = true;
 					Collection<GroupMessageHeader> headers =
 							db.getMessageHeaders(g.getId());
 					long duration = System.currentTimeMillis() - now;
 					if(LOG.isLoggable(INFO))
 						LOG.info("Partial load took " + duration + " ms");
-					displayHeaders(g, postable, headers);
+					displayHeaders(g, headers);
 				} catch(NoSuchSubscriptionException e) {
 					if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
 					removeGroup(g.getId());
@@ -320,10 +276,8 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 		});
 	}
 
-	public void createGroupButtonClicked() {
-		if(restricted)
-			startActivity(new Intent(this, CreateBlogActivity.class));
-		else startActivity(new Intent(this, CreateGroupActivity.class));
+	public void createButtonClicked() {
+		startActivity(new Intent(this, CreateGroupActivity.class));
 	}
 
 	public void cancelButtonClicked() {
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
index 0fd73a29a8a549a575435a3ebb35b70abe99e631..0d6478856f2a7009c764f745e631d59a89e0cc1c 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
@@ -101,7 +101,6 @@ implements OnItemClickListener {
 			long id) {
 		GroupListItem item = getItem(position);
 		Intent i = new Intent(getContext(), GroupActivity.class);
-		i.putExtra("net.sf.briar.RESTRICTED", item.isRestricted());
 		i.putExtra("net.sf.briar.GROUP_ID", item.getGroupId().getBytes());
 		i.putExtra("net.sf.briar.GROUP_NAME", item.getGroupName());
 		getContext().startActivity(i);
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
index abc61370d85fe8b353cd4a7ac8e5b72e01b780d8..0fc071ad398c4a964c71a64ce3e331c8dbba56c0 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
@@ -14,15 +14,13 @@ import net.sf.briar.api.messaging.GroupId;
 class GroupListItem {
 
 	private final Group group;
-	private final boolean postable, empty;
+	private final boolean empty;
 	private final String authorName, contentType, subject;
 	private final long timestamp;
 	private final int unread;
 
-	GroupListItem(Group group, boolean postable,
-			Collection<GroupMessageHeader> headers) {
+	GroupListItem(Group group, Collection<GroupMessageHeader> headers) {
 		this.group = group;
-		this.postable = postable;
 		empty = headers.isEmpty();
 		if(empty) {
 			authorName = null;
@@ -55,14 +53,6 @@ class GroupListItem {
 		return group.getName();
 	}
 
-	boolean isRestricted() {
-		return group.isRestricted();
-	}
-
-	boolean isPostable() {
-		return postable;
-	}
-
 	boolean isEmpty() {
 		return empty;
 	}
diff --git a/briar-android/src/net/sf/briar/android/groups/NoGroupsDialog.java b/briar-android/src/net/sf/briar/android/groups/NoGroupsDialog.java
index e5aab93e17816c6a296139d1aa153cd20aeca1f8..34613b52c9a95144f115621896c01c6c31f6e8b3 100644
--- a/briar-android/src/net/sf/briar/android/groups/NoGroupsDialog.java
+++ b/briar-android/src/net/sf/briar/android/groups/NoGroupsDialog.java
@@ -10,24 +10,19 @@ import android.support.v4.app.DialogFragment;
 public class NoGroupsDialog extends DialogFragment {
 
 	private Listener listener = null;
-	private boolean restricted = false;
 
 	void setListener(Listener listener) {
 		this.listener = listener;
 	}
 
-	void setRestricted(boolean restricted) {
-		this.restricted = restricted;
-	}
-
 	@Override
 	public Dialog onCreateDialog(Bundle state) {
 		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-		builder.setMessage(restricted ? R.string.no_blogs : R.string.no_groups);
+		builder.setMessage(R.string.no_groups);
 		builder.setPositiveButton(R.string.create_button,
 				new DialogInterface.OnClickListener() {
 			public void onClick(DialogInterface dialog, int id) {
-				listener.createGroupButtonClicked();
+				listener.createButtonClicked();
 			}
 		});
 		builder.setNegativeButton(R.string.cancel_button,
@@ -41,7 +36,7 @@ public class NoGroupsDialog extends DialogFragment {
 
 	interface Listener {
 
-		void createGroupButtonClicked();
+		void createButtonClicked();
 
 		void cancelButtonClicked();
 	}
diff --git a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/ReadGroupPostActivity.java
similarity index 94%
rename from briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
rename to briar-android/src/net/sf/briar/android/groups/ReadGroupPostActivity.java
index cce96dde745376183b08025fa968cfdc20dd7a48..e815281d0d3a2ff25ac5dce45864f065a51e3577 100644
--- a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/ReadGroupPostActivity.java
@@ -49,7 +49,7 @@ import android.widget.TextView;
 
 import com.google.inject.Inject;
 
-public class ReadGroupMessageActivity extends BriarActivity
+public class ReadGroupPostActivity extends BriarActivity
 implements OnClickListener {
 
 	static final int RESULT_REPLY = RESULT_FIRST_USER;
@@ -57,13 +57,12 @@ implements OnClickListener {
 	static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
 
 	private static final Logger LOG =
-			Logger.getLogger(ReadGroupMessageActivity.class.getName());
+			Logger.getLogger(ReadGroupPostActivity.class.getName());
 
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
 
 	@Inject private BundleEncrypter bundleEncrypter;
-	private boolean restricted = false;
 	private GroupId groupId = null;
 	private Rating rating = UNRATED;
 	private boolean read;
@@ -84,7 +83,6 @@ implements OnClickListener {
 		super.onCreate(null);
 
 		Intent i = getIntent();
-		restricted = i.getBooleanExtra("net.sf.briar.RESTRICTED", false);
 		byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
 		if(b == null) throw new IllegalStateException();
 		groupId = new GroupId(b);
@@ -258,7 +256,7 @@ implements OnClickListener {
 	private void setReadInUi(final boolean read) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				ReadGroupMessageActivity.this.read = read;
+				ReadGroupPostActivity.this.read = read;
 				if(read) readButton.setImageResource(R.drawable.content_unread);
 				else readButton.setImageResource(R.drawable.content_read);
 			}
@@ -326,17 +324,10 @@ implements OnClickListener {
 			setResult(RESULT_NEXT);
 			finish();
 		} else if(view == replyButton) {
-			if(restricted) {
-				Intent i = new Intent(this, WriteBlogPostActivity.class);
-				i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
-				i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
-				startActivity(i);
-			} else {
-				Intent i = new Intent(this, WriteGroupPostActivity.class);
-				i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
-				i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
-				startActivity(i);
-			}
+			Intent i = new Intent(this, WriteGroupPostActivity.class);
+			i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+			i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
+			startActivity(i);
 			setResult(RESULT_REPLY);
 			finish();
 		}
diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java
index e4f388f0ec53305455c77ecc9ce645cd99a5f993..9b4621c7ea60983580d1da8f8aedf561ae3ebfcb 100644
--- a/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java
@@ -24,6 +24,7 @@ import java.util.logging.Logger;
 import net.sf.briar.R;
 import net.sf.briar.android.BriarActivity;
 import net.sf.briar.android.BriarService;
+import net.sf.briar.android.GroupNameComparator;
 import net.sf.briar.android.BriarService.BriarServiceConnection;
 import net.sf.briar.android.identity.CreateIdentityActivity;
 import net.sf.briar.android.identity.LocalAuthorItem;