diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java
index 2af57aa1a05779b4be53eae2000a02116ee118e4..ccff1a6ce3fd1fbfe21638192993bbb5371ec26c 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java
@@ -28,7 +28,6 @@ import org.briarproject.briar.android.threaded.ThreadItemAdapter;
 import org.briarproject.briar.android.threaded.ThreadListActivity;
 import org.briarproject.briar.android.threaded.ThreadListController;
 import org.briarproject.briar.api.forum.Forum;
-import org.briarproject.briar.api.forum.ForumPostHeader;
 
 import javax.annotation.Nullable;
 import javax.inject.Inject;
@@ -41,7 +40,7 @@ import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_BOD
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
 public class ForumActivity extends
-		ThreadListActivity<Forum, ThreadItemAdapter<ForumItem>, ForumItem, ForumPostHeader>
+		ThreadListActivity<Forum, ForumItem, ThreadItemAdapter<ForumItem>>
 		implements ForumListener {
 
 	@Inject
@@ -53,7 +52,7 @@ public class ForumActivity extends
 	}
 
 	@Override
-	protected ThreadListController<Forum, ForumItem, ForumPostHeader> getController() {
+	protected ThreadListController<Forum, ForumItem> getController() {
 		return forumController;
 	}
 
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java
index 3d800e241f4feb2924f275c1bb43fba2d887a3a9..a9e5d661fe201ae6f7d45554b524df29b88e2641 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java
@@ -6,13 +6,11 @@ import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.android.threaded.ThreadListController;
 import org.briarproject.briar.api.forum.Forum;
-import org.briarproject.briar.api.forum.ForumPostHeader;
 
 @NotNullByDefault
-interface ForumController
-		extends ThreadListController<Forum, ForumItem, ForumPostHeader> {
+interface ForumController extends ThreadListController<Forum, ForumItem> {
 
-	interface ForumListener extends ThreadListListener<ForumPostHeader> {
+	interface ForumListener extends ThreadListListener<ForumItem> {
 		@UiThread
 		void onForumLeft(ContactId c);
 	}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java
index f218f4393e917c6a02cbf82714f4e4f1e927a4e9..dde424f7b868d7c2fe0c1d30ae15dbe3bf145ee9 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java
@@ -75,10 +75,10 @@ class ForumControllerImpl extends
 		super.eventOccurred(e);
 
 		if (e instanceof ForumPostReceivedEvent) {
-			ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
-			if (pe.getGroupId().equals(getGroupId())) {
+			ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
+			if (f.getGroupId().equals(getGroupId())) {
 				LOG.info("Forum post received, adding...");
-				onForumPostHeaderReceived(pe.getForumPostHeader());
+				onForumPostReceived(f.getHeader(), f.getBody());
 			}
 		} else if (e instanceof ForumInvitationResponseReceivedEvent) {
 			ForumInvitationResponseReceivedEvent f =
@@ -90,10 +90,10 @@ class ForumControllerImpl extends
 				onForumInvitationAccepted(r.getContactId());
 			}
 		} else if (e instanceof ContactLeftShareableEvent) {
-			ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
-			if (s.getGroupId().equals(getGroupId())) {
+			ContactLeftShareableEvent c = (ContactLeftShareableEvent) e;
+			if (c.getGroupId().equals(getGroupId())) {
 				LOG.info("Forum left by contact");
-				onForumLeft(s.getContactId());
+				onForumLeft(c.getContactId());
 			}
 		}
 	}
@@ -195,11 +195,12 @@ class ForumControllerImpl extends
 		return new ForumItem(header, body);
 	}
 
-	private void onForumPostHeaderReceived(final ForumPostHeader h) {
+	private void onForumPostReceived(ForumPostHeader h, String body) {
+		final ForumItem item = buildItem(h, body);
 		listener.runOnUiThreadUnlessDestroyed(new Runnable() {
 			@Override
 			public void run() {
-				listener.onHeaderReceived(h);
+				listener.onItemReceived(item);
 			}
 		});
 	}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java
index 42f22288d5f7472a8a7d3496eae659bc1ece89b6..4873684ccb579a10d9f23aaace5851dcbf42853f 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java
@@ -254,7 +254,7 @@ public class ForumListFragment extends BaseEventFragment implements
 		} else if (e instanceof ForumPostReceivedEvent) {
 			ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
 			LOG.info("Forum post added, updating item");
-			updateItem(f.getGroupId(), f.getForumPostHeader());
+			updateItem(f.getGroupId(), f.getHeader());
 		} else if (e instanceof ForumInvitationRequestReceivedEvent) {
 			LOG.info("Forum invitation received, reloading available forums");
 			loadAvailableForums();
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java
index c19aa38a26730893fbce3ca2e73119f696688a7e..4a8d1c83fa9a96757d7c0532d308b94bdc9dca00 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java
@@ -29,7 +29,6 @@ import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListAct
 import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
 import org.briarproject.briar.android.threaded.ThreadListActivity;
 import org.briarproject.briar.android.threaded.ThreadListController;
-import org.briarproject.briar.api.privategroup.GroupMessageHeader;
 import org.briarproject.briar.api.privategroup.PrivateGroup;
 import org.briarproject.briar.api.privategroup.Visibility;
 
@@ -44,7 +43,7 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
 public class GroupActivity extends
-		ThreadListActivity<PrivateGroup, GroupMessageAdapter, GroupMessageItem, GroupMessageHeader>
+		ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageAdapter>
 		implements GroupListener, OnClickListener {
 
 	@Inject
@@ -60,7 +59,7 @@ public class GroupActivity extends
 	}
 
 	@Override
-	protected ThreadListController<PrivateGroup, GroupMessageItem, GroupMessageHeader> getController() {
+	protected ThreadListController<PrivateGroup, GroupMessageItem> getController() {
 		return controller;
 	}
 
@@ -276,7 +275,7 @@ public class GroupActivity extends
 	public void onGroupDissolved() {
 		setGroupEnabled(false);
 		AlertDialog.Builder builder =
-			new AlertDialog.Builder(this, R.style.BriarDialogTheme);
+				new AlertDialog.Builder(this, R.style.BriarDialogTheme);
 		builder.setTitle(getString(R.string.groups_dissolved_dialog_title));
 		builder.setMessage(getString(R.string.groups_dissolved_dialog_message));
 		builder.setNeutralButton(R.string.ok, null);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java
index 61817fcb4e52ee2e653c4639932748972e2b5535..5f7cba4d6c51b9c992079f62b2d981583fda44f8 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java
@@ -8,13 +8,11 @@ import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
 import org.briarproject.briar.android.threaded.ThreadListController;
-import org.briarproject.briar.api.privategroup.GroupMessageHeader;
 import org.briarproject.briar.api.privategroup.PrivateGroup;
 import org.briarproject.briar.api.privategroup.Visibility;
 
 public interface GroupController
-		extends
-		ThreadListController<PrivateGroup, GroupMessageItem, GroupMessageHeader> {
+		extends ThreadListController<PrivateGroup, GroupMessageItem> {
 
 	void loadLocalAuthor(
 			ResultExceptionHandler<LocalAuthor, DbException> handler);
@@ -22,7 +20,8 @@ public interface GroupController
 	void isDissolved(
 			ResultExceptionHandler<Boolean, DbException> handler);
 
-	interface GroupListener extends ThreadListListener<GroupMessageHeader> {
+	interface GroupListener extends ThreadListListener<GroupMessageItem> {
+
 		@UiThread
 		void onContactRelationshipRevealed(AuthorId memberId,
 				ContactId contactId, Visibility v);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java
index ebd41767ce59a0c24c492715c8a4b13bd7e15c57..a3e5c60e26882a4a69e77045f0b3bcc9aa2841c0 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java
@@ -80,14 +80,15 @@ class GroupControllerImpl extends
 		super.eventOccurred(e);
 
 		if (e instanceof GroupMessageAddedEvent) {
-			GroupMessageAddedEvent gmae = (GroupMessageAddedEvent) e;
-			if (!gmae.isLocal() && gmae.getGroupId().equals(getGroupId())) {
+			GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
+			if (!g.isLocal() && g.getGroupId().equals(getGroupId())) {
 				LOG.info("Group message received, adding...");
-				final GroupMessageHeader h = gmae.getHeader();
+				final GroupMessageItem item =
+						buildItem(g.getHeader(), g.getBody());
 				listener.runOnUiThreadUnlessDestroyed(new Runnable() {
 					@Override
 					public void run() {
-						listener.onHeaderReceived(h);
+						listener.onItemReceived(item);
 					}
 				});
 			}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/NestedTreeList.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/NestedTreeList.java
index d4fd06b7c3956cf87df2d1d696713ffe5f4c606a..a8517b64b0eb5349f0628cc193aaa0583b15b5de 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/NestedTreeList.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/NestedTreeList.java
@@ -3,7 +3,9 @@ package org.briarproject.briar.android.threaded;
 import android.support.annotation.UiThread;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.MessageTree;
+import org.briarproject.briar.api.client.MessageTree.MessageNode;
 import org.briarproject.briar.client.MessageTreeImpl;
 
 import java.util.ArrayList;
@@ -13,8 +15,7 @@ import java.util.List;
 
 @UiThread
 @NotNullByDefault
-public class NestedTreeList<T extends MessageTree.MessageNode>
-		implements Iterable<T> {
+public class NestedTreeList<T extends MessageNode> implements Iterable<T> {
 
 	private final MessageTree<T> tree = new MessageTreeImpl<>();
 	private List<T> depthFirstCollection = new ArrayList<>();
@@ -38,14 +39,14 @@ public class NestedTreeList<T extends MessageTree.MessageNode>
 		return depthFirstCollection.get(index);
 	}
 
-	public int indexOf(T elem) {
-		return depthFirstCollection.indexOf(elem);
-	}
-
 	public int size() {
 		return depthFirstCollection.size();
 	}
 
+	public boolean contains(MessageId m) {
+		return tree.contains(m);
+	}
+
 	@Override
 	public Iterator<T> iterator() {
 		return depthFirstCollection.iterator();
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java
index 21d0ede14a0ef78d8dcf269331406ef72fa335bf..49140f571cd0bd66aca37749d293b73f07e32490 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java
@@ -1,6 +1,5 @@
 package org.briarproject.briar.android.threaded;
 
-import android.os.Handler;
 import android.support.annotation.UiThread;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
@@ -28,7 +27,6 @@ public class ThreadItemAdapter<I extends ThreadItem>
 	protected final NestedTreeList<I> items = new NestedTreeList<>();
 	private final ThreadItemListener<I> listener;
 	private final LinearLayoutManager layoutManager;
-	private final Handler handler = new Handler();
 
 	private volatile int revision = 0;
 
@@ -104,6 +102,10 @@ public class ThreadItemAdapter<I extends ThreadItem>
 		return NO_POSITION; // Not found
 	}
 
+	boolean contains(MessageId m) {
+		return items.contains(m);
+	}
+
 	/**
 	 * Highlights the item with the given {@link MessageId}
 	 * and disables the highlight for a previously highlighted item, if any.
@@ -184,6 +186,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
 	}
 
 	static class UnreadCount {
+
 		final int top, bottom;
 
 		private UnreadCount(int top, int bottom) {
@@ -193,6 +196,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
 	}
 
 	public interface ThreadItemListener<I> {
+
 		void onUnreadItemVisible(I item);
 
 		void onReplyClick(I item);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java
index 2886607edce8eecc24a62075dfe07b385e21b94b..bfd63fa5bf27147af8f61f409495f1c50c5f0578 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java
@@ -16,6 +16,7 @@ public class ThreadItemListImpl<I extends ThreadItem> extends ArrayList<I>
 		return bottomVisibleItemId;
 	}
 
+	@Override
 	public void setFirstVisibleId(@Nullable MessageId bottomVisibleItemId) {
 		this.bottomVisibleItemId = bottomVisibleItemId;
 	}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java
index dc30cc19b53d8ad5449a07aeb969ad127a33a6bc..829ba292ccec128e6aeb5571efb9496feac77566 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java
@@ -32,7 +32,6 @@ import org.briarproject.briar.android.view.TextInputView;
 import org.briarproject.briar.android.view.TextInputView.TextInputListener;
 import org.briarproject.briar.android.view.UnreadMessageButton;
 import org.briarproject.briar.api.client.NamedGroup;
-import org.briarproject.briar.api.client.PostHeader;
 import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
 
 import java.util.Collection;
@@ -49,9 +48,9 @@ import static org.briarproject.briar.android.threaded.ThreadItemAdapter.UnreadCo
 
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
-public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadItemAdapter<I>, I extends ThreadItem, H extends PostHeader>
+public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, A extends ThreadItemAdapter<I>>
 		extends BriarActivity
-		implements ThreadListListener<H>, TextInputListener, SharingListener,
+		implements ThreadListListener<I>, TextInputListener, SharingListener,
 		ThreadItemListener<I>, ThreadListDataSource {
 
 	protected static final String KEY_REPLY_ID = "replyId";
@@ -68,7 +67,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
 	@Nullable
 	private MessageId replyId;
 
-	protected abstract ThreadListController<G, I, H> getController();
+	protected abstract ThreadListController<G, I> getController();
 
 	@Inject
 	protected SharingController sharingController;
@@ -190,7 +189,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
 							if (items.isEmpty()) {
 								list.showData();
 							} else {
-								initList(items);
+								displayItems(items);
 								updateTextInput();
 							}
 						} else {
@@ -206,7 +205,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
 				});
 	}
 
-	private void initList(final ThreadItemList<I> items) {
+	private void displayItems(final ThreadItemList<I> items) {
 		adapter.setItems(items);
 		MessageId messageId = items.getFirstVisibleItemId();
 		if (messageId != null)
@@ -383,19 +382,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
 	protected abstract int getMaxBodyLength();
 
 	@Override
-	public void onHeaderReceived(H header) {
-		getController().loadItem(header,
-				new UiResultExceptionHandler<I, DbException>(this) {
-					@Override
-					public void onResultUi(final I result) {
-						addItem(result, false);
-					}
-
-					@Override
-					public void onExceptionUi(DbException exception) {
-						handleDbException(exception);
-					}
-				});
+	public void onItemReceived(I item) {
+		addItem(item, false);
 	}
 
 	@Override
@@ -403,8 +391,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
 		supportFinishAfterTransition();
 	}
 
-	protected void addItem(I item, boolean isLocal) {
+	private void addItem(I item, boolean isLocal) {
 		adapter.incrementRevision();
+		MessageId parent = item.getParentId();
+		if (parent != null && !adapter.contains(parent)) {
+			// We've incremented the adapter's revision, so the item will be
+			// loaded when its parent has been loaded
+			LOG.info("Ignoring item with missing parent");
+			return;
+		}
 		adapter.add(item);
 
 		if (isLocal) {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java
index d39c8b453a6c1c5852d9c56663edd1d7ca0fe4fa..f64a707689ed7133a2d97ee47016cde462fca659 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java
@@ -12,14 +12,13 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController;
 import org.briarproject.briar.android.controller.handler.ExceptionHandler;
 import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
 import org.briarproject.briar.api.client.NamedGroup;
-import org.briarproject.briar.api.client.PostHeader;
 
 import java.util.Collection;
 
 import javax.annotation.Nullable;
 
 @NotNullByDefault
-public interface ThreadListController<G extends NamedGroup, I extends ThreadItem, H extends PostHeader>
+public interface ThreadListController<G extends NamedGroup, I extends ThreadItem>
 		extends ActivityLifecycleController {
 
 	void setGroupId(GroupId groupId);
@@ -29,9 +28,8 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
 	void loadSharingContacts(
 			ResultExceptionHandler<Collection<ContactId>, DbException> handler);
 
-	void loadItem(H header, ResultExceptionHandler<I, DbException> handler);
-
-	void loadItems(ResultExceptionHandler<ThreadItemList<I>, DbException> handler);
+	void loadItems(
+			ResultExceptionHandler<ThreadItemList<I>, DbException> handler);
 
 	void markItemRead(I item);
 
@@ -42,9 +40,10 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
 
 	void deleteNamedGroup(ExceptionHandler<DbException> handler);
 
-	interface ThreadListListener<H> extends ThreadListDataSource {
+	interface ThreadListListener<I> extends ThreadListDataSource {
+
 		@UiThread
-		void onHeaderReceived(H header);
+		void onItemReceived(I item);
 
 		@UiThread
 		void onGroupRemoved();
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java
index 5f520611afc688ef2c8928ff64a1d2170b3f6be7..26bb1aff326db89891bfe6d5e6625a8dc8585147 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java
@@ -39,9 +39,9 @@ import static java.util.logging.Level.WARNING;
 
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
-public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends ThreadedMessage, L extends ThreadListListener<H>>
+public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends ThreadedMessage, L extends ThreadListListener<I>>
 		extends DbControllerImpl
-		implements ThreadListController<G, I, H>, EventListener {
+		implements ThreadListController<G, I>, EventListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(ThreadListControllerImpl.class.getName());
@@ -203,35 +203,6 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 	@DatabaseExecutor
 	protected abstract String loadMessageBody(H header) throws DbException;
 
-	@Override
-	public void loadItem(final H header,
-			final ResultExceptionHandler<I, DbException> handler) {
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					String body;
-					if (!bodyCache.containsKey(header.getId())) {
-						body = loadMessageBody(header);
-						bodyCache.put(header.getId(), body);
-					} else {
-						body = bodyCache.get(header.getId());
-					}
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Loading item took " + duration + " ms");
-					I item = buildItem(header, body);
-					handler.onResult(item);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-					handler.onException(e);
-				}
-			}
-		});
-	}
-
 	@Override
 	public void markItemRead(I item) {
 		markItemsRead(Collections.singletonList(item));
diff --git a/briar-android/src/main/res/menu/group_actions.xml b/briar-android/src/main/res/menu/group_actions.xml
index 1336c01de3136b635394fe7ea74fe769fc847cba..eac18cd62195fb493ff53a32f76bf1718f9bbfa4 100644
--- a/briar-android/src/main/res/menu/group_actions.xml
+++ b/briar-android/src/main/res/menu/group_actions.xml
@@ -3,18 +3,18 @@
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto">
 
-	<item
-		android:id="@+id/action_group_member_list"
-		android:icon="@drawable/ic_group_white"
-		android:title="@string/groups_member_list"
-		app:showAsAction="ifRoom"/>
-
 	<item
 		android:id="@+id/action_group_invite"
 		android:icon="@drawable/social_share_white"
 		android:title="@string/groups_invite_members"
 		app:showAsAction="ifRoom"/>
 
+	<item
+		android:id="@+id/action_group_member_list"
+		android:icon="@drawable/ic_group_white"
+		android:title="@string/groups_member_list"
+		app:showAsAction="ifRoom"/>
+
 	<item
 		android:id="@+id/action_group_reveal"
 		android:icon="@drawable/ic_visibility_white"
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTree.java b/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTree.java
index 8d2ed6cd9ac23eac905d26a478094d5440b4f8fd..e850391d05fb6af686fbd273fa9a6f3375912aa9 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTree.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTree.java
@@ -21,6 +21,8 @@ public interface MessageTree<T extends MessageTree.MessageNode> {
 
 	Collection<T> depthFirstOrder();
 
+	boolean contains(MessageId m);
+
 	@NotNullByDefault
 	interface MessageNode {
 
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumPostReceivedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumPostReceivedEvent.java
index 0aef3a23f522b2612d4d640452ba181d346e7425..44a24db8fc38bc6ef33b6f7e34e157dc415702a2 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumPostReceivedEvent.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumPostReceivedEvent.java
@@ -14,21 +14,26 @@ import javax.annotation.concurrent.Immutable;
 @NotNullByDefault
 public class ForumPostReceivedEvent extends Event {
 
-	private final ForumPostHeader forumPostHeader;
 	private final GroupId groupId;
+	private final ForumPostHeader header;
+	private final String body;
 
-	public ForumPostReceivedEvent(ForumPostHeader forumPostHeader,
-			GroupId groupId) {
-
-		this.forumPostHeader = forumPostHeader;
+	public ForumPostReceivedEvent(GroupId groupId, ForumPostHeader header,
+			String body) {
 		this.groupId = groupId;
-	}
-
-	public ForumPostHeader getForumPostHeader() {
-		return forumPostHeader;
+		this.header = header;
+		this.body = body;
 	}
 
 	public GroupId getGroupId() {
 		return groupId;
 	}
+
+	public ForumPostHeader getHeader() {
+		return header;
+	}
+
+	public String getBody() {
+		return body;
+	}
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/event/GroupMessageAddedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/event/GroupMessageAddedEvent.java
index 91d363779ba62cad9bd83728ea4aef29e17e7b4a..ce75850e52bed14daf156da87086de989dfc4974 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/event/GroupMessageAddedEvent.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/event/GroupMessageAddedEvent.java
@@ -17,13 +17,14 @@ public class GroupMessageAddedEvent extends Event {
 
 	private final GroupId groupId;
 	private final GroupMessageHeader header;
+	private final String body;
 	private final boolean local;
 
 	public GroupMessageAddedEvent(GroupId groupId, GroupMessageHeader header,
-			boolean local) {
-
+			String body, boolean local) {
 		this.groupId = groupId;
 		this.header = header;
+		this.body = body;
 		this.local = local;
 	}
 
@@ -35,6 +36,10 @@ public class GroupMessageAddedEvent extends Event {
 		return header;
 	}
 
+	public String getBody() {
+		return body;
+	}
+
 	public boolean isLocal() {
 		return local;
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/client/MessageTreeImpl.java b/briar-core/src/main/java/org/briarproject/briar/client/MessageTreeImpl.java
index 5636c5f751ba50987cc50ab5074cc4bddd2e2e22..41b4410d8a00da111833eed68900cfe89648cd7e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/client/MessageTreeImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/client/MessageTreeImpl.java
@@ -107,4 +107,8 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
 		return orderedList;
 	}
 
+	@Override
+	public boolean contains(MessageId m) {
+		return nodeMap.containsKey(m);
+	}
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
index fef8224bdd2c01b08bc02db6531b1232bae8d463..f9328d2827af19baa1500172370d1cf523270127 100644
--- a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
@@ -45,7 +45,6 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 import javax.inject.Inject;
 
-import static org.briarproject.bramble.api.identity.Author.Status.ANONYMOUS;
 import static org.briarproject.bramble.api.identity.Author.Status.OURSELVES;
 import static org.briarproject.briar.api.forum.ForumConstants.KEY_AUTHOR;
 import static org.briarproject.briar.api.forum.ForumConstants.KEY_ID;
@@ -85,9 +84,10 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 
 		messageTracker.trackIncomingMessage(txn, m);
 
-		ForumPostHeader post = getForumPostHeader(txn, m.getId(), meta);
+		ForumPostHeader header = getForumPostHeader(txn, m.getId(), meta);
+		String postBody = getPostBody(body);
 		ForumPostReceivedEvent event =
-				new ForumPostReceivedEvent(post, m.getGroupId());
+				new ForumPostReceivedEvent(m.getGroupId(), header, postBody);
 		txn.attach(event);
 
 		// share message
@@ -215,14 +215,19 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 	public String getPostBody(MessageId m) throws DbException {
 		try {
 			// Parent ID, author, forum post body, signature
-			BdfList message = clientHelper.getMessageAsList(m);
-			if (message == null) throw new DbException();
-			return message.getString(2);
+			BdfList body = clientHelper.getMessageAsList(m);
+			if (body == null) throw new DbException();
+			return getPostBody(body);
 		} catch (FormatException e) {
 			throw new DbException(e);
 		}
 	}
 
+	private String getPostBody(BdfList body) throws FormatException {
+		// Parent ID, author, forum post body, signature
+		return body.getString(2);
+	}
+
 	@Override
 	public Collection<ForumPostHeader> getPostHeaders(GroupId g)
 			throws DbException {
@@ -294,24 +299,17 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 			throws DbException, FormatException {
 
 		long timestamp = meta.getLong(KEY_TIMESTAMP);
-		Author author = null;
-		Status status = ANONYMOUS;
 		MessageId parentId = null;
 		if (meta.containsKey(KEY_PARENT))
 			parentId = new MessageId(meta.getRaw(KEY_PARENT));
-		// TODO: Remove support for anonymous forum posts
-		BdfDictionary d1 = meta.getDictionary(KEY_AUTHOR, null);
-		if (d1 != null) {
-			AuthorId authorId = new AuthorId(d1.getRaw(KEY_ID));
-			String name = d1.getString(KEY_NAME);
-			byte[] publicKey = d1.getRaw(KEY_PUBLIC_NAME);
-			author = new Author(authorId, name, publicKey);
-			if (statuses.containsKey(authorId)) {
-				status = statuses.get(authorId);
-			} else {
-				status = identityManager.getAuthorStatus(txn, author.getId());
-			}
-		}
+		BdfDictionary authorDict = meta.getDictionary(KEY_AUTHOR);
+		AuthorId authorId = new AuthorId(authorDict.getRaw(KEY_ID));
+		String name = authorDict.getString(KEY_NAME);
+		byte[] publicKey = authorDict.getRaw(KEY_PUBLIC_NAME);
+		Author author = new Author(authorId, name, publicKey);
+		Status status = statuses.get(authorId);
+		if (status == null)
+			status = identityManager.getAuthorStatus(txn, author.getId());
 		boolean read = meta.getBoolean(MSG_KEY_READ);
 
 		return new ForumPostHeader(id, parentId, timestamp, author, status,
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
index cdaf0c928ad300024e752cf9aeff40e48cc3246c..d0becaecd710a91a6c2f15cca60707930abab7f9 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
@@ -307,16 +307,20 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
 	@Override
 	public String getMessageBody(MessageId m) throws DbException {
 		try {
-			// type(0), member_name(1), member_public_key(2), parent_id(3),
-			// previous_message_id(4), content(5), signature(6)
 			BdfList body = clientHelper.getMessageAsList(m);
 			if (body == null) throw new DbException();
-			return body.getString(5);
+			return getMessageBody(body);
 		} catch (FormatException e) {
 			throw new DbException(e);
 		}
 	}
 
+	private String getMessageBody(BdfList body) throws FormatException {
+			// type(0), member_name(1), member_public_key(2), parent_id(3),
+			// previous_message_id(4), content(5), signature(6)
+			return body.getString(5);
+	}
+
 	@Override
 	public Collection<GroupMessageHeader> getHeaders(GroupId g)
 			throws DbException {
@@ -579,21 +583,20 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
 	private void attachGroupMessageAddedEvent(Transaction txn, Message m,
 			BdfDictionary meta, boolean local)
 			throws DbException, FormatException {
-		GroupMessageHeader h =
-				getGroupMessageHeader(txn, m.getGroupId(), m.getId(), meta,
-						Collections.<AuthorId, Status>emptyMap());
-		Event e = new GroupMessageAddedEvent(m.getGroupId(), h, local);
-		txn.attach(e);
+		GroupMessageHeader header = getGroupMessageHeader(txn, m.getGroupId(),
+				m.getId(), meta, Collections.<AuthorId, Status>emptyMap());
+		String body = getMessageBody(clientHelper.toList(m));
+		txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, body,
+				local));
 	}
 
 	private void attachJoinMessageAddedEvent(Transaction txn, Message m,
 			BdfDictionary meta, boolean local, Visibility v)
 			throws DbException, FormatException {
-		JoinMessageHeader h =
-				getJoinMessageHeader(txn, m.getGroupId(), m.getId(), meta,
-						Collections.<AuthorId, Status>emptyMap(), v);
-		Event e = new GroupMessageAddedEvent(m.getGroupId(), h, local);
-		txn.attach(e);
+		JoinMessageHeader header = getJoinMessageHeader(txn, m.getGroupId(),
+				m.getId(), meta, Collections.<AuthorId, Status>emptyMap(), v);
+		txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, "",
+				local));
 	}
 
 	private void addMember(Transaction txn, GroupId g, Author a, Visibility v)