diff --git a/briar-android/res/layout/list_item_forum_post.xml b/briar-android/res/layout/list_item_thread.xml
similarity index 98%
rename from briar-android/res/layout/list_item_forum_post.xml
rename to briar-android/res/layout/list_item_thread.xml
index 2fe00672178eab9dafc8e92dfd062626fec77260..f788c0ce8e6aff841c4d3f4dcae07e423576966c 100644
--- a/briar-android/res/layout/list_item_forum_post.xml
+++ b/briar-android/res/layout/list_item_thread.xml
@@ -1,12 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
+	android:id="@+id/layout"
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
 	xmlns:tools="http://schemas.android.com/tools"
-	android:id="@+id/forum_cell"
 	android:layout_width="match_parent"
 	android:layout_height="wrap_content"
-	android:orientation="horizontal">
+	android:orientation="horizontal"
+	android:baselineAligned="false">
 
 	<RelativeLayout
 		android:layout_width="wrap_content"
diff --git a/briar-android/res/layout/list_item_thread_notice.xml b/briar-android/res/layout/list_item_thread_notice.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2beecd1f2aabf4c2a2b209974e6d383268e3d8b1
--- /dev/null
+++ b/briar-android/res/layout/list_item_thread_notice.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	android:id="@+id/layout"
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	android:layout_marginLeft="@dimen/margin_medium"
+	android:baselineAligned="false"
+	android:orientation="vertical">
+
+	<View
+		android:id="@+id/top_divider"
+		style="@style/Divider.ForumList"
+		android:layout_width="match_parent"
+		android:layout_height="@dimen/margin_separator"/>
+
+	<LinearLayout
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_marginBottom="@dimen/margin_small"
+		android:layout_marginLeft="@dimen/margin_medium"
+		android:layout_marginRight="@dimen/margin_medium"
+		android:layout_marginTop="@dimen/margin_medium"
+		android:orientation="horizontal">
+
+		<org.briarproject.android.view.AuthorView
+			android:id="@+id/author"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			app:persona="commenter"/>
+
+		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
+			android:id="@+id/text"
+			android:layout_width="match_parent"
+			android:layout_height="match_parent"
+			android:layout_marginLeft="@dimen/margin_medium"
+			android:gravity="center_vertical"
+			android:textColor="@color/briar_text_secondary"
+			android:textIsSelectable="true"
+			android:textSize="@dimen/text_size_medium"
+			android:textStyle="italic"
+			tools:text="@string/groups_member_joined"/>
+
+	</LinearLayout>
+
+</LinearLayout>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 878b281e48a5de9ec154e3b7b0ca69a2bf8467fe..132cea768dd5f12ffc8b3c4265d99fb5354eadeb 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -166,6 +166,7 @@
 	<string name="groups_invite_members">Invite Members</string>
 	<string name="groups_leave">Leave Group</string>
 	<string name="groups_dissolve">Dissolve Group</string>
+	<string name="groups_member_joined">joined the group.</string>
 
 	<!-- Private Group Invitations -->
 	<string name="groups_invitations_title">Group Invitations</string>
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 174a5c94668a193735716965d889c99644626b21..6550d1752b6b05fbd484da2f97d506bfd332fb91 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -120,6 +120,7 @@ public class ActivityModule {
 	@Provides
 	protected GroupController provideGroupController(
 			GroupControllerImpl groupController) {
+		activity.addLifecycleController(groupController);
 		return groupController;
 	}
 
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index 307a7c5e158407d4a0cd173974607da181716181..31816fa8fea8fc2fb4f9678591b507416c827c84 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -18,8 +18,9 @@ import android.widget.Toast;
 import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.controller.handler.UiResultExceptionHandler;
-import org.briarproject.android.sharing.ShareForumActivity;
 import org.briarproject.android.sharing.ForumSharingStatusActivity;
+import org.briarproject.android.sharing.ShareForumActivity;
+import org.briarproject.android.threaded.ThreadItemAdapter;
 import org.briarproject.android.threaded.ThreadListActivity;
 import org.briarproject.android.threaded.ThreadListController;
 import org.briarproject.api.db.DbException;
@@ -35,7 +36,7 @@ import static android.widget.Toast.LENGTH_SHORT;
 import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
 
 public class ForumActivity extends
-		ThreadListActivity<Forum, ForumItem, ForumPostHeader, NestedForumAdapter> {
+		ThreadListActivity<Forum, ForumItem, ForumPostHeader, ThreadItemAdapter<ForumItem>> {
 
 	private static final int REQUEST_FORUM_SHARED = 3;
 
@@ -74,9 +75,9 @@ public class ForumActivity extends
 	}
 
 	@Override
-	protected NestedForumAdapter createAdapter(
+	protected ThreadItemAdapter<ForumItem> createAdapter(
 			LinearLayoutManager layoutManager) {
-		return new NestedForumAdapter(this, layoutManager);
+		return new ThreadItemAdapter<>(this, layoutManager);
 	}
 
 	@Override
diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
index 5da9ee98c7cda5e7812b040470857e1e4707d259..25d8328464377f8bff53ea5b33e018265db19538 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
@@ -88,8 +88,8 @@ public class ForumControllerImpl
 	}
 
 	@Override
-	protected String loadMessageBody(MessageId id) throws DbException {
-		return StringUtils.fromUtf8(forumManager.getPostBody(id));
+	protected String loadMessageBody(ForumPostHeader h) throws DbException {
+		return StringUtils.fromUtf8(forumManager.getPostBody(h.getId()));
 	}
 
 	@Override
diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java b/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
deleted file mode 100644
index 08d68b961301b4883a01e5c1f36ac6eb5ea83795..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.briarproject.android.forum;
-
-import android.support.annotation.UiThread;
-import android.support.v7.widget.LinearLayoutManager;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.briarproject.R;
-import org.briarproject.android.threaded.ThreadItemAdapter;
-
-@UiThread
-class NestedForumAdapter extends ThreadItemAdapter<ForumItem> {
-
-	NestedForumAdapter(ThreadItemListener<ForumItem> listener,
-			LinearLayoutManager layoutManager) {
-		super(listener, layoutManager);
-	}
-
-	@Override
-	public NestedForumHolder onCreateViewHolder(ViewGroup parent,
-			int viewType) {
-		View v = LayoutInflater.from(parent.getContext())
-				.inflate(R.layout.list_item_forum_post, parent, false);
-		return new NestedForumHolder(v);
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java b/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java
deleted file mode 100644
index b73558ff5d668c1adf37f0e8e69a9f971411967c..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.briarproject.android.forum;
-
-import android.view.View;
-
-import org.briarproject.android.threaded.ThreadItemViewHolder;
-
-public class NestedForumHolder extends ThreadItemViewHolder<ForumItem> {
-
-	public NestedForumHolder(View v) {
-		super(v);
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
index 6b34d19cd9dc87043eb0ee7514d3b76afb3e9db1..8d61bbff83dd964ed230113e0053f7b4f1027279 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
@@ -2,6 +2,7 @@ package org.briarproject.android.privategroup.conversation;
 
 import android.support.annotation.Nullable;
 
+import org.briarproject.R;
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.controller.handler.ResultExceptionHandler;
 import org.briarproject.android.threaded.ThreadListControllerImpl;
@@ -17,6 +18,7 @@ import org.briarproject.api.lifecycle.LifecycleManager;
 import org.briarproject.api.privategroup.GroupMessage;
 import org.briarproject.api.privategroup.GroupMessageFactory;
 import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.privategroup.JoinMessageHeader;
 import org.briarproject.api.privategroup.PrivateGroup;
 import org.briarproject.api.privategroup.PrivateGroupManager;
 import org.briarproject.api.sync.MessageId;
@@ -90,8 +92,13 @@ public class GroupControllerImpl extends
 	}
 
 	@Override
-	protected String loadMessageBody(MessageId id) throws DbException {
-		return privateGroupManager.getMessageBody(id);
+	protected String loadMessageBody(GroupMessageHeader header)
+			throws DbException {
+		if (header instanceof JoinMessageHeader) {
+			return listener.getApplicationContext()
+					.getString(R.string.groups_member_joined);
+		}
+		return privateGroupManager.getMessageBody(header.getId());
 	}
 
 	@Override
@@ -162,6 +169,9 @@ public class GroupControllerImpl extends
 	@Override
 	protected GroupMessageItem buildItem(GroupMessageHeader header,
 			String body) {
+		if (header instanceof JoinMessageHeader) {
+			return new JoinMessageItem(header, body);
+		}
 		return new GroupMessageItem(header, body);
 	}
 
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java
index 19ee14adce62158808c7f795e9eb505ec889cd2f..2a9c75a009f1d4e2c79851713dc1256a2ae74236 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java
@@ -7,7 +7,9 @@ import android.view.View;
 import android.view.ViewGroup;
 
 import org.briarproject.R;
+import org.briarproject.android.threaded.BaseThreadItemViewHolder;
 import org.briarproject.android.threaded.ThreadItemAdapter;
+import org.briarproject.android.threaded.ThreadItemViewHolder;
 
 @UiThread
 public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
@@ -18,11 +20,23 @@ public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
 	}
 
 	@Override
-	public GroupMessageViewHolder onCreateViewHolder(ViewGroup parent,
-			int viewType) {
+	public int getItemViewType(int position) {
+		GroupMessageItem item = getVisibleItem(position);
+		if (item instanceof JoinMessageItem) {
+			return R.layout.list_item_thread_notice;
+		}
+		return R.layout.list_item_thread;
+	}
+
+	@Override
+	public BaseThreadItemViewHolder<GroupMessageItem> onCreateViewHolder(
+			ViewGroup parent, int type) {
 		View v = LayoutInflater.from(parent.getContext())
-				.inflate(R.layout.list_item_forum_post, parent, false);
-		return new GroupMessageViewHolder(v);
+				.inflate(type, parent, false);
+		if (type == R.layout.list_item_thread_notice) {
+			return new BaseThreadItemViewHolder<>(v);
+		}
+		return new ThreadItemViewHolder<>(v);
 	}
 
 }
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
index 7bde4a8bbcf183f06f275d7fbb0c49395259aea1..b961fd3f6bd5d34d3c30b4b280015a74dd78f3d5 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
@@ -8,7 +8,7 @@ import org.briarproject.api.sync.MessageId;
 
 class GroupMessageItem extends ThreadItem {
 
-	GroupMessageItem(MessageId messageId, MessageId parentId,
+	private GroupMessageItem(MessageId messageId, MessageId parentId,
 			String text, long timestamp, Author author, Status status,
 			boolean isRead) {
 		super(messageId, parentId, text, timestamp, author, status, isRead);
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java
deleted file mode 100644
index 11825b8056e6ee3002338956a6599c2a4b949b21..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.briarproject.android.privategroup.conversation;
-
-import android.view.View;
-
-import org.briarproject.android.threaded.ThreadItemViewHolder;
-
-public class GroupMessageViewHolder
-		extends ThreadItemViewHolder<GroupMessageItem> {
-
-	public GroupMessageViewHolder(View v) {
-		super(v);
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItem.java b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..e21399127a16c29b07449b15f159da11f08ffa3c
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItem.java
@@ -0,0 +1,22 @@
+package org.briarproject.android.privategroup.conversation;
+
+import org.briarproject.api.privategroup.GroupMessageHeader;
+
+class JoinMessageItem extends GroupMessageItem {
+
+	JoinMessageItem(GroupMessageHeader h,
+			String text) {
+		super(h, text);
+	}
+
+	@Override
+	public int getLevel() {
+		return 0;
+	}
+
+	@Override
+	public boolean hasDescendants() {
+		return false;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/BaseThreadItemViewHolder.java b/briar-android/src/org/briarproject/android/threaded/BaseThreadItemViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..47ed3502fa1dbb7b1b546c5167864273784f524a
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/BaseThreadItemViewHolder.java
@@ -0,0 +1,122 @@
+package org.briarproject.android.threaded;
+
+import android.animation.Animator;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.ColorDrawable;
+import android.support.annotation.CallSuper;
+import android.support.annotation.UiThread;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
+import org.briarproject.android.view.AuthorView;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.util.StringUtils;
+
+@UiThread
+@NotNullByDefault
+public class BaseThreadItemViewHolder<I extends ThreadItem>
+		extends RecyclerView.ViewHolder {
+
+	private final static int ANIMATION_DURATION = 5000;
+
+	private final ViewGroup layout;
+	private final TextView textView;
+	private final AuthorView author;
+	private final View topDivider;
+
+	public BaseThreadItemViewHolder(View v) {
+		super(v);
+
+		layout = (ViewGroup) v.findViewById(R.id.layout);
+		textView = (TextView) v.findViewById(R.id.text);
+		author = (AuthorView) v.findViewById(R.id.author);
+		topDivider = v.findViewById(R.id.top_divider);
+	}
+
+	// TODO improve encapsulation, so we don't need to pass the adapter here
+	@CallSuper
+	public void bind(final ThreadItemAdapter<I> adapter,
+			final ThreadItemListener<I> listener, final I item, int pos) {
+
+		textView.setText(StringUtils.trim(item.getText()));
+
+		if (pos == 0) {
+			topDivider.setVisibility(View.INVISIBLE);
+		} else {
+			topDivider.setVisibility(View.VISIBLE);
+		}
+
+		author.setAuthor(item.getAuthor());
+		author.setDate(item.getTimestamp());
+		author.setAuthorStatus(item.getStatus());
+
+		if (item.equals(adapter.getReplyItem())) {
+			layout.setBackgroundColor(ContextCompat
+					.getColor(getContext(), R.color.forum_cell_highlight));
+		} else if (item.equals(adapter.getAddedItem())) {
+			layout.setBackgroundColor(ContextCompat
+					.getColor(getContext(), R.color.forum_cell_highlight));
+			animateFadeOut(adapter, adapter.getAddedItem());
+			adapter.clearAddedItem();
+		} else {
+			layout.setBackgroundColor(ContextCompat
+					.getColor(getContext(), R.color.window_background));
+		}
+	}
+
+	private void animateFadeOut(final ThreadItemAdapter<I> adapter,
+			final I addedItem) {
+
+		setIsRecyclable(false);
+		ValueAnimator anim = new ValueAnimator();
+		adapter.addAnimatingItem(addedItem, anim);
+		ColorDrawable viewColor = (ColorDrawable) layout.getBackground();
+		anim.setIntValues(viewColor.getColor(), ContextCompat
+				.getColor(getContext(), R.color.window_background));
+		anim.setEvaluator(new ArgbEvaluator());
+		anim.addListener(new Animator.AnimatorListener() {
+			@Override
+			public void onAnimationStart(Animator animation) {
+
+			}
+
+			@Override
+			public void onAnimationEnd(Animator animation) {
+				setIsRecyclable(true);
+				adapter.removeAnimatingItem(addedItem);
+			}
+
+			@Override
+			public void onAnimationCancel(Animator animation) {
+				setIsRecyclable(true);
+				adapter.removeAnimatingItem(addedItem);
+			}
+
+			@Override
+			public void onAnimationRepeat(Animator animation) {
+
+			}
+		});
+		anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+			@Override
+			public void onAnimationUpdate(ValueAnimator valueAnimator) {
+				layout.setBackgroundColor(
+						(Integer) valueAnimator.getAnimatedValue());
+			}
+		});
+		anim.setDuration(ANIMATION_DURATION);
+		anim.start();
+	}
+
+	protected Context getContext() {
+		return textView.getContext();
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItem.java b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
index a5a22075995246e5859671bc7b64995324bb75ea..c4281e9ac1a5ea1b4f96739403151ea868fa738d 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
@@ -1,13 +1,18 @@
 package org.briarproject.android.threaded;
 
+import android.support.annotation.UiThread;
+
 import org.briarproject.api.clients.MessageTree.MessageNode;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.Author.Status;
 import org.briarproject.api.sync.MessageId;
 
+import javax.annotation.concurrent.NotThreadSafe;
+
 import static org.briarproject.android.threaded.ThreadItemAdapter.UNDEFINED;
 
-/* This class is not thread safe */
+@UiThread
+@NotThreadSafe
 public abstract class ThreadItem implements MessageNode {
 
 	private final MessageId messageId;
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
index c8ab7f2ba0bf173f8688939167c40db894683663..9e93f8f0714cf10635060ff28213e154e646c9be 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
@@ -5,7 +5,11 @@ import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
 
+import org.briarproject.R;
 import org.briarproject.android.util.VersionedAdapter;
 import org.briarproject.api.sync.MessageId;
 
@@ -17,8 +21,8 @@ import java.util.Map;
 
 import static android.support.v7.widget.RecyclerView.NO_POSITION;
 
-public abstract class ThreadItemAdapter<I extends ThreadItem>
-		extends RecyclerView.Adapter<ThreadItemViewHolder<I>>
+public class ThreadItemAdapter<I extends ThreadItem>
+		extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>>
 		implements VersionedAdapter {
 
 	static final int UNDEFINED = -1;
@@ -42,7 +46,15 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
 	}
 
 	@Override
-	public void onBindViewHolder(ThreadItemViewHolder<I> ui, int position) {
+	public BaseThreadItemViewHolder<I> onCreateViewHolder(
+			ViewGroup parent, int viewType) {
+		View v = LayoutInflater.from(parent.getContext())
+				.inflate(R.layout.list_item_thread, parent, false);
+		return new ThreadItemViewHolder<>(v);
+	}
+
+	@Override
+	public void onBindViewHolder(BaseThreadItemViewHolder<I> ui, int position) {
 		I item = getVisibleItem(position);
 		if (item == null) return;
 		listener.onItemVisible(item);
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java b/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java
index 0b5a5ddc5b74cc34bee5bb6d63b2534ce2aa0227..c59eac8a48305ecc48a417a1b536ee5fd0c55738 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java
@@ -1,45 +1,30 @@
 package org.briarproject.android.threaded;
 
-import android.animation.Animator;
-import android.animation.ArgbEvaluator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.graphics.drawable.ColorDrawable;
 import android.support.annotation.UiThread;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.widget.RecyclerView;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.TextView;
 
 import org.briarproject.R;
 import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
-import org.briarproject.android.view.AuthorView;
-import org.briarproject.util.StringUtils;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
 import static android.view.View.GONE;
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 
 @UiThread
-public abstract class ThreadItemViewHolder<I extends ThreadItem>
-		extends RecyclerView.ViewHolder {
+@NotNullByDefault
+public class ThreadItemViewHolder<I extends ThreadItem>
+		extends BaseThreadItemViewHolder<I> {
 
-	private final static int ANIMATION_DURATION = 5000;
-
-	private final TextView textView, lvlText, repliesText;
-	private final AuthorView author;
+	private final TextView  lvlText, repliesText;
 	private final View[] lvls;
 	private final View chevron, replyButton;
-	private final ViewGroup cell;
-	private final View topDivider;
 
 	public ThreadItemViewHolder(View v) {
 		super(v);
 
-		textView = (TextView) v.findViewById(R.id.text);
 		lvlText = (TextView) v.findViewById(R.id.nested_line_text);
-		author = (AuthorView) v.findViewById(R.id.author);
 		repliesText = (TextView) v.findViewById(R.id.replies);
 		int[] nestedLineIds = {
 				R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
@@ -51,21 +36,13 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
 		}
 		chevron = v.findViewById(R.id.chevron);
 		replyButton = v.findViewById(R.id.btn_reply);
-		cell = (ViewGroup) v.findViewById(R.id.forum_cell);
-		topDivider = v.findViewById(R.id.top_divider);
 	}
 
 	// TODO improve encapsulation, so we don't need to pass the adapter here
+	@Override
 	public void bind(final ThreadItemAdapter<I> adapter,
 			final ThreadItemListener<I> listener, final I item, int pos) {
-
-		textView.setText(StringUtils.trim(item.getText()));
-
-		if (pos == 0) {
-			topDivider.setVisibility(View.INVISIBLE);
-		} else {
-			topDivider.setVisibility(View.VISIBLE);
-		}
+		super.bind(adapter, listener, item, pos);
 
 		for (int i = 0; i < lvls.length; i++) {
 			lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
@@ -76,9 +53,6 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
 		} else {
 			lvlText.setVisibility(GONE);
 		}
-		author.setAuthor(item.getAuthor());
-		author.setDate(item.getTimestamp());
-		author.setAuthorStatus(item.getStatus());
 
 		int replies = adapter.getReplyCount(item);
 		if (replies == 0) {
@@ -110,18 +84,6 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
 		} else {
 			chevron.setVisibility(INVISIBLE);
 		}
-		if (item.equals(adapter.getReplyItem())) {
-			cell.setBackgroundColor(ContextCompat
-					.getColor(getContext(), R.color.forum_cell_highlight));
-		} else if (item.equals(adapter.getAddedItem())) {
-			cell.setBackgroundColor(ContextCompat
-					.getColor(getContext(), R.color.forum_cell_highlight));
-			animateFadeOut(adapter, adapter.getAddedItem());
-			adapter.clearAddedItem();
-		} else {
-			cell.setBackgroundColor(ContextCompat
-					.getColor(getContext(), R.color.window_background));
-		}
 		replyButton.setOnClickListener(new View.OnClickListener() {
 			@Override
 			public void onClick(View v) {
@@ -131,52 +93,4 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
 		});
 	}
 
-	private void animateFadeOut(final ThreadItemAdapter<I> adapter,
-			final I addedItem) {
-
-		setIsRecyclable(false);
-		ValueAnimator anim = new ValueAnimator();
-		adapter.addAnimatingItem(addedItem, anim);
-		ColorDrawable viewColor = (ColorDrawable) cell.getBackground();
-		anim.setIntValues(viewColor.getColor(), ContextCompat
-				.getColor(getContext(), R.color.window_background));
-		anim.setEvaluator(new ArgbEvaluator());
-		anim.addListener(new Animator.AnimatorListener() {
-			@Override
-			public void onAnimationStart(Animator animation) {
-
-			}
-
-			@Override
-			public void onAnimationEnd(Animator animation) {
-				setIsRecyclable(true);
-				adapter.removeAnimatingItem(addedItem);
-			}
-
-			@Override
-			public void onAnimationCancel(Animator animation) {
-				setIsRecyclable(true);
-				adapter.removeAnimatingItem(addedItem);
-			}
-
-			@Override
-			public void onAnimationRepeat(Animator animation) {
-
-			}
-		});
-		anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-			@Override
-			public void onAnimationUpdate(ValueAnimator valueAnimator) {
-				cell.setBackgroundColor(
-						(Integer) valueAnimator.getAnimatedValue());
-			}
-		});
-		anim.setDuration(ANIMATION_DURATION);
-		anim.start();
-	}
-
-	private Context getContext() {
-		return textView.getContext();
-	}
-
 }
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
index f2e7570a820d89f66cf7e3452d74b3a1a4256514..43ce1d5c283dbe154b17e66976fef62451a4ee02 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
@@ -1,5 +1,6 @@
 package org.briarproject.android.threaded;
 
+import android.content.Context;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 
@@ -39,6 +40,8 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
 
 		@UiThread
 		void onGroupRemoved();
+
+		Context getApplicationContext();
 	}
 
 }
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
index d584d87045ed046f1af4b2f4161cc40db0ecfed7..cf6ffb8d6cfc33657d7b0905da404e9e78a19958 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
@@ -52,7 +52,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 
 	private volatile GroupId groupId;
 
-	protected ThreadListListener<H> listener;
+	protected volatile ThreadListListener<H> listener;
 
 	protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
 			LifecycleManager lifecycleManager, IdentityManager identityManager,
@@ -159,7 +159,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 					for (H header : headers) {
 						if (!bodyCache.containsKey(header.getId())) {
 							bodyCache.put(header.getId(),
-									loadMessageBody(header.getId()));
+									loadMessageBody(header));
 						}
 					}
 					duration = System.currentTimeMillis() - now;
@@ -181,7 +181,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 	protected abstract Collection<H> loadHeaders() throws DbException;
 
 	@DatabaseExecutor
-	protected abstract String loadMessageBody(MessageId id) throws DbException;
+	protected abstract String loadMessageBody(H header) throws DbException;
 
 	@Override
 	public void loadItem(final H header,
@@ -193,7 +193,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 					long now = System.currentTimeMillis();
 					String body;
 					if (!bodyCache.containsKey(header.getId())) {
-						body = loadMessageBody(header.getId());
+						body = loadMessageBody(header);
 						bodyCache.put(header.getId(), body);
 					} else {
 						body = bodyCache.get(header.getId());
diff --git a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
index 991f59f0f26dca492ff6b7204871430f0c0d4928..cedacc77d08850c7dc891bd4647f215150bb9f5f 100644
--- a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
+++ b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
@@ -8,6 +8,7 @@ import org.briarproject.BuildConfig;
 import org.briarproject.TestUtils;
 import org.briarproject.android.TestBriarApplication;
 import org.briarproject.android.controller.handler.UiResultExceptionHandler;
+import org.briarproject.android.threaded.ThreadItemAdapter;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
@@ -111,7 +112,7 @@ public class ForumActivityTest {
 		List<ForumItem> dummyData = getDummyData();
 		verify(mc, times(1)).loadItems(rc.capture());
 		rc.getValue().onResult(dummyData);
-		NestedForumAdapter adapter = forumActivity.getAdapter();
+		ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter();
 		Assert.assertNotNull(adapter);
 		// Cascade close
 		assertEquals(6, adapter.getItemCount());
diff --git a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
index 806fee299bf73a5bb23a034dfd8541765b1b29d3..a77a88cc156c4771d0da6689eca875539f624037 100644
--- a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
+++ b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
 import org.briarproject.android.ActivityModule;
 import org.briarproject.android.controller.BriarController;
 import org.briarproject.android.controller.BriarControllerImpl;
+import org.briarproject.android.threaded.ThreadItemAdapter;
 import org.mockito.Mockito;
 
 /**
@@ -15,7 +16,7 @@ public class TestForumActivity extends ForumActivity {
 		return forumController;
 	}
 
-	public NestedForumAdapter getAdapter() {
+	public ThreadItemAdapter<ForumItem> getAdapter() {
 		return adapter;
 	}
 
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
index 8910913538a21ead4a38be84208f6aae53e06c62..caf8b81ae4a0614cf53129525e9e13faaadae2c4 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
@@ -26,7 +26,6 @@ import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.clients.BdfIncomingMessageHook;
 import org.briarproject.util.StringUtils;
-import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -236,10 +235,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 	@Override
 	public String getMessageBody(MessageId m) throws DbException {
 		try {
-			// TODO remove
-			if (clientHelper.getMessageMetadataAsDictionary(m).getLong(KEY_TYPE) != POST.getInt())
-				return "new member joined";
-
 			// type(0), member_name(1), member_public_key(2), parent_id(3),
 			// previous_message_id(4), content(5), signature(6)
 			return clientHelper.getMessageAsList(m).getString(5);