diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 025e452b2a2243482c0816a33547ce75269767eb..43231803db60450db720e7af84c60e3d4c6a4d2d 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -129,16 +129,6 @@
 				/>
 		</activity>
 
-		<activity
-			android:name=".android.forum.ReadForumPostActivity"
-			android:label="@string/app_name"
-			android:parentActivityName=".android.NavDrawerActivity">
-			<meta-data
-				android:name="android.support.PARENT_ACTIVITY"
-				android:value=".android.NavDrawerActivity"
-				/>
-		</activity>
-
 		<activity
 			android:name=".android.forum.ShareForumActivity"
 			android:label="@string/forums_share_toolbar_header"
@@ -159,17 +149,6 @@
 				/>
 		</activity>
 
-		<activity
-			android:name=".android.forum.WriteForumPostActivity"
-			android:label="@string/app_name"
-			android:parentActivityName=".android.NavDrawerActivity"
-			android:windowSoftInputMode="stateVisible">
-			<meta-data
-				android:name="android.support.PARENT_ACTIVITY"
-				android:value=".android.NavDrawerActivity"
-				/>
-		</activity>
-
 		<activity
 			android:name=".android.identity.CreateIdentityActivity"
 			android:label="@string/new_identity_title"
diff --git a/briar-android/res/drawable/chevron48dp_down.xml b/briar-android/res/drawable/chevron48dp_down.xml
new file mode 100644
index 0000000000000000000000000000000000000000..16dd3cd9d7093f9118d7439825363074e6705895
--- /dev/null
+++ b/briar-android/res/drawable/chevron48dp_down.xml
@@ -0,0 +1,4 @@
+<vector android:height="24dp" android:viewportHeight="48.0"
+    android:viewportWidth="48.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M9.1,19.3l14.9,11.8l14.9,-11.8l-1.9,-2.4l-13,10.4l-13,-10.4z"/>
+</vector>
diff --git a/briar-android/res/drawable/chevron48dp_up.xml b/briar-android/res/drawable/chevron48dp_up.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7d59523a92f4030ca1747b1c3941f60e152816a2
--- /dev/null
+++ b/briar-android/res/drawable/chevron48dp_up.xml
@@ -0,0 +1,4 @@
+<vector android:height="24dp" android:viewportHeight="48.0"
+    android:viewportWidth="48.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M38.9,28.7l-14.9,-11.8l-14.9,11.8l1.9,2.4l13,-10.4l13,10.4z"/>
+</vector>
diff --git a/briar-android/res/drawable/level_indicator_circle.xml b/briar-android/res/drawable/level_indicator_circle.xml
new file mode 100644
index 0000000000000000000000000000000000000000..22fc407d3a46ed4d87b0f83431e84f6782e7ef1f
--- /dev/null
+++ b/briar-android/res/drawable/level_indicator_circle.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
+
+	<solid android:color="@color/window_background"/>
+
+	<stroke
+		android:width="2dp"
+		android:color="@color/forum_discussion_nested_line"/>
+</shape>
\ No newline at end of file
diff --git a/briar-android/res/drawable/selector_chevron.xml b/briar-android/res/drawable/selector_chevron.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bc08694ba67a833441046c7841bb7b18db5f7f16
--- /dev/null
+++ b/briar-android/res/drawable/selector_chevron.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:state_selected="true" android:drawable="@drawable/chevron48dp_down"/>
+	<item android:drawable="@drawable/chevron48dp_up"/>
+</selector>
\ No newline at end of file
diff --git a/briar-android/res/layout/activity_forum.xml b/briar-android/res/layout/activity_forum.xml
new file mode 100644
index 0000000000000000000000000000000000000000..946ce34e8a3ac99193729a6f6354ab540935b1ca
--- /dev/null
+++ b/briar-android/res/layout/activity_forum.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	android:orientation="vertical">
+
+	<org.briarproject.android.util.BriarRecyclerView
+		android:id="@+id/forum_discussion_list"
+		android:layout_width="match_parent"
+		android:layout_height="0dp"
+		android:layout_weight="1"
+		app:scrollToEnd="false"/>
+
+	<include
+		layout="@layout/text_input_field"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/forum_discussion_cell.xml b/briar-android/res/layout/forum_discussion_cell.xml
new file mode 100644
index 0000000000000000000000000000000000000000..57383a720623ffc952528c8ab2eadabe2d6b5ae1
--- /dev/null
+++ b/briar-android/res/layout/forum_discussion_cell.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	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">
+
+	<RelativeLayout
+		android:layout_width="wrap_content"
+		android:layout_height="match_parent">
+
+		<View
+			android:id="@+id/nested_line_1"
+			style="@style/DiscussionLevelIndicator"
+			android:layout_width="@dimen/forum_nested_line_width"
+			android:layout_height="match_parent"
+			android:visibility="gone"
+			tools:visibility="showingDescendants"/>
+
+		<View
+			android:id="@+id/nested_line_2"
+			style="@style/DiscussionLevelIndicator"
+			android:layout_width="@dimen/forum_nested_line_width"
+			android:layout_height="match_parent"
+			android:layout_toRightOf="@id/nested_line_1"
+			android:visibility="gone"/>
+
+		<View
+			android:id="@+id/nested_line_3"
+			style="@style/DiscussionLevelIndicator"
+			android:layout_width="@dimen/forum_nested_line_width"
+			android:layout_height="match_parent"
+			android:layout_toRightOf="@id/nested_line_2"
+			android:visibility="gone"/>
+
+		<View
+			android:id="@+id/nested_line_4"
+			style="@style/DiscussionLevelIndicator"
+			android:layout_width="@dimen/forum_nested_line_width"
+			android:layout_height="match_parent"
+			android:layout_toRightOf="@id/nested_line_3"
+			android:visibility="gone"/>
+
+		<View
+			android:id="@+id/nested_line_5"
+			style="@style/DiscussionLevelIndicator"
+			android:layout_width="@dimen/forum_nested_line_width"
+			android:layout_height="match_parent"
+			android:layout_toRightOf="@id/nested_line_4"
+			android:visibility="gone"/>
+
+		<TextView
+			android:id="@+id/nested_line_text"
+			android:layout_width="@dimen/forum_nested_indicator"
+			android:layout_height="@dimen/forum_nested_indicator"
+			android:layout_centerInParent="true"
+			android:background="@drawable/level_indicator_circle"
+			android:gravity="center"
+			android:textSize="@dimen/text_size_small"
+			android:visibility="gone"
+			/>
+
+
+	</RelativeLayout>
+
+	<RelativeLayout
+		android:layout_width="0dp"
+		android:layout_height="wrap_content"
+		android:layout_marginLeft="@dimen/margin_medium"
+		android:layout_weight="1">
+
+		<TextView
+			android:id="@+id/text"
+			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:textIsSelectable="true"
+			android:textSize="@dimen/text_size_medium"
+			tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."/>
+
+		<de.hdodenhof.circleimageview.CircleImageView
+			android:id="@+id/avatar"
+			android:layout_width="@dimen/forum_avatar_size"
+			android:layout_height="@dimen/forum_avatar_size"
+			android:layout_alignLeft="@id/text"
+			android:layout_below="@id/text"
+			android:layout_marginRight="@dimen/margin_small"
+			android:layout_marginTop="@dimen/margin_small"
+			android:src="@drawable/ic_launcher"
+			app:civ_border_color="@color/briar_primary"
+			app:civ_border_width="@dimen/avatar_border_width"
+			tools:src="@drawable/ic_launcher"
+			/>
+
+		<ImageView
+			android:id="@+id/chevron"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignParentRight="true"
+			android:layout_below="@id/text"
+			android:layout_marginRight="@dimen/margin_medium"
+			android:layout_marginTop="@dimen/margin_small"
+			android:clickable="true"
+			android:src="@drawable/selector_chevron"
+			android:tint="@color/briar_button_positive"
+			/>
+
+		<TextView
+			android:id="@+id/btn_reply"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_below="@id/text"
+			android:layout_marginRight="@dimen/margin_medium"
+			android:layout_toLeftOf="@id/chevron"
+			android:background="?attr/selectableItemBackground"
+			android:clickable="true"
+			android:padding="@dimen/margin_medium"
+			android:text="@string/btn_reply"
+			android:textColor="@color/briar_button_positive"
+			android:textSize="@dimen/text_size_tiny"/>
+
+		<TextView
+			android:id="@+id/replies"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignBaseline="@id/btn_reply"
+			android:layout_toLeftOf="@id/btn_reply"
+			android:padding="@dimen/margin_medium"
+			android:textSize="@dimen/text_size_tiny"
+			tools:text="2 replies"/>
+
+		<TextView
+			android:id="@+id/date"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignBaseline="@id/replies"
+			android:layout_toLeftOf="@id/replies"
+			android:layout_toRightOf="@id/avatar"
+			android:ellipsize="end"
+			android:maxLines="1"
+			android:textSize="@dimen/text_size_tiny"
+			tools:text="09:09 John Smith"/>
+
+		<View
+			android:id="@+id/bottom_divider"
+			style="@style/Divider.ForumList"
+			android:layout_width="match_parent"
+			android:layout_height="@dimen/margin_separator"
+			android:layout_alignLeft="@id/text"
+			android:layout_below="@id/btn_reply"/>
+
+	</RelativeLayout>
+
+
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/text_input_field.xml b/briar-android/res/layout/text_input_field.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c59e93c7ebcd3d7e15f8d1fda849b2319f13f4ce
--- /dev/null
+++ b/briar-android/res/layout/text_input_field.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	android:id="@+id/text_input_container"
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	android:background="@color/button_bar_background"
+	android:elevation="@dimen/margin_tiny"
+	android:orientation="horizontal"
+	android:paddingLeft="@dimen/margin_large"
+	android:paddingStart="@dimen/margin_large">
+
+	<EditText
+		android:id="@+id/input_text"
+		android:layout_width="0dp"
+		android:layout_height="wrap_content"
+		android:layout_marginTop="@dimen/margin_small"
+		android:layout_weight="1"
+		android:inputType="textMultiLine|textCapSentences"
+		android:maxLines="5"/>
+
+	<ImageView
+		android:id="@+id/btn_send"
+		android:layout_width="38dp"
+		android:layout_height="38dp"
+		android:layout_margin="@dimen/margin_medium"
+		android:background="?attr/selectableItemBackground"
+		android:clickable="true"
+		android:contentDescription="@string/send"
+		android:onClick="sendMessage"
+		android:src="@drawable/social_send_now_white"
+		android:tint="@color/briar_primary"
+		/>
+
+</LinearLayout>
diff --git a/briar-android/res/values/attrs.xml b/briar-android/res/values/attrs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..380379e9e6b9a8435453b2db254182ebd06c1fbc
--- /dev/null
+++ b/briar-android/res/values/attrs.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<declare-styleable name="BriarRecyclerView">
+		<attr name="scrollToEnd" format="boolean" />
+	</declare-styleable>
+
+</resources>
\ No newline at end of file
diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml
index 3fb50397850e50e9d086b0b5f65a72353f1a131d..14a0938bb09091ae58a05b39bca66308383198b4 100644
--- a/briar-android/res/values/color.xml
+++ b/briar-android/res/values/color.xml
@@ -43,4 +43,6 @@
 
 	<color name="spinner_border">#61000000</color> <!-- 38% Black -->
 	<color name="spinner_arrow">@color/briar_blue_dark</color>
+	<color name="forum_discussion_nested_line">#cfd2d4</color>
+	<color name="forum_cell_highlight">#ffffff</color>
 </resources>
\ No newline at end of file
diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml
index 5ca081c5687a1cf70a1568ac36609a73ddebbf0e..942a2d5bfa535971982113e90be7fe38970cb91e 100644
--- a/briar-android/res/values/dimens.xml
+++ b/briar-android/res/values/dimens.xml
@@ -41,5 +41,8 @@
 	<dimen name="message_bubble_margin_tail">14dp</dimen>
 	<dimen name="message_bubble_margin_non_tail">51dp</dimen>
 	<dimen name="message_bubble_timestamp_margin">15dp</dimen>
+	<dimen name="forum_nested_line_width">2dp</dimen>
+	<dimen name="forum_nested_indicator">24dp</dimen>
+	<dimen name="forum_avatar_size">20dp</dimen>
 
 </resources>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 0e01dde61aa790bafc32222181600420ae1f9018..1e1e8e28c510d4217a6d5bf90606c1df58974223 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -198,6 +198,17 @@
 	<string name="introduction_success_title">Introduced contact was added</string>
 	<string name="introduction_success_text">You have been introduced to %1$s.</string>
 
+	<!-- Forum -->
+	<string name="btn_reply">REPLY</string>
+	<plurals name="message_replies">
+		<item quantity="one">%1$d reply</item>
+		<item quantity="other">%1$d replies</item>
+	</plurals>
+	<string name="forum_new_entry_posted">Forum entry posted</string>
+	<string name="forum_new_entry_received">New forum entry</string>
+	<string name="forum_new_message_hint">New Entry</string>
+	<string name="forum_message_reply_hint">New Reply</string>
+
 	<!-- Dialogs -->
 	<string name="dialog_title_lost_password">Lost Password</string>
 	<string name="dialog_message_lost_password">Password recovery is not possible. Do you want to delete your account?\n\nCaution: This will permanently delete your identities, contacts and messages</string>
@@ -225,4 +236,6 @@
 	<!-- Progress titles -->
 	<string name="progress_title_logout">Signing out of Briar..</string>
 	<string name="progress_title_please_wait">Please wait..</string>
+
+
 </resources>
diff --git a/briar-android/res/values/styles.xml b/briar-android/res/values/styles.xml
index abb3e2457e35dc5cc6bee7f633b07a5a24b27235..38f609e83c61ef764ac06e59a6d488fbe5f05850 100644
--- a/briar-android/res/values/styles.xml
+++ b/briar-android/res/values/styles.xml
@@ -140,4 +140,9 @@
 		<item name="android:layout_marginBottom">16dp</item>
 	</style>
 
+	<style name="DiscussionLevelIndicator">
+		<item name="android:layout_marginLeft">4dp</item>
+		<item name="android:background">?android:attr/listDivider</item>
+	</style>
+
 </resources>
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index b777e38a43bebfc04627891e0b31d2cf9aec44ce..f28335cd8aa86d83163479e534f834067e20554d 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -8,10 +8,8 @@ import org.briarproject.android.forum.ContactSelectorFragment;
 import org.briarproject.android.forum.CreateForumActivity;
 import org.briarproject.android.forum.ForumActivity;
 import org.briarproject.android.forum.ForumSharingStatusActivity;
-import org.briarproject.android.forum.ReadForumPostActivity;
 import org.briarproject.android.forum.ShareForumActivity;
 import org.briarproject.android.forum.ShareForumMessageFragment;
-import org.briarproject.android.forum.WriteForumPostActivity;
 import org.briarproject.android.fragment.BaseFragment;
 import org.briarproject.android.identity.CreateIdentityActivity;
 import org.briarproject.android.introduction.IntroductionActivity;
@@ -54,16 +52,12 @@ public interface ActivityComponent {
 
 	void inject(AvailableForumsActivity activity);
 
-	void inject(WriteForumPostActivity activity);
-
 	void inject(CreateForumActivity activity);
 
 	void inject(ShareForumActivity activity);
 
 	void inject(ForumSharingStatusActivity activity);
 
-	void inject(ReadForumPostActivity activity);
-
 	void inject(ForumActivity activity);
 
 	void inject(SettingsActivity activity);
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 0e13fbd0d7aa59311bf86b0b664a8afd6bfc5f46..6766f38022bfaf9e489cbadc9e17022683cc0545 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -12,6 +12,8 @@ import org.briarproject.android.controller.ConfigController;
 import org.briarproject.android.controller.ConfigControllerImpl;
 import org.briarproject.android.controller.DbController;
 import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.forum.ForumController;
+import org.briarproject.android.forum.ForumControllerImpl;
 import org.briarproject.android.controller.NavDrawerController;
 import org.briarproject.android.controller.NavDrawerControllerImpl;
 import org.briarproject.android.controller.PasswordController;
@@ -21,6 +23,7 @@ import org.briarproject.android.controller.SetupControllerImpl;
 import org.briarproject.android.controller.TransportStateListener;
 import org.briarproject.android.forum.ContactSelectorFragment;
 import org.briarproject.android.forum.ForumListFragment;
+import org.briarproject.android.forum.ForumTestControllerImpl;
 import org.briarproject.android.forum.ShareForumMessageFragment;
 import org.briarproject.android.fragment.BaseFragment;
 import org.briarproject.android.introduction.ContactChooserFragment;
@@ -98,6 +101,22 @@ public class ActivityModule {
 		return dbController;
 	}
 
+	@ActivityScope
+	@Provides
+	protected ForumController provideForumController(
+			ForumControllerImpl forumController) {
+		activity.addLifecycleController(forumController);
+		return forumController;
+	}
+
+	@Named("ForumTestController")
+	@ActivityScope
+	@Provides
+	protected ForumController provideForumTestController(
+			ForumTestControllerImpl forumController) {
+		return forumController;
+	}
+
 	@ActivityScope
 	@Provides
 	protected NavDrawerController provideNavDrawerController(
diff --git a/briar-android/src/org/briarproject/android/AppModule.java b/briar-android/src/org/briarproject/android/AppModule.java
index 68d37071af3aa1476aab1596b89943180e2aa8ca..ed2ef27244176ced083913bb702237a6b1b07e2c 100644
--- a/briar-android/src/org/briarproject/android/AppModule.java
+++ b/briar-android/src/org/briarproject/android/AppModule.java
@@ -4,6 +4,7 @@ import android.app.Application;
 
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.api.ReferenceManager;
+import org.briarproject.android.forum.ForumPersistentData;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.PublicKey;
 import org.briarproject.api.crypto.SecretKey;
@@ -136,4 +137,10 @@ public class AppModule {
 		eventBus.addListener(notificationManager);
 		return notificationManager;
 	}
+
+	@Provides
+	@Singleton
+	ForumPersistentData provideForumPersistence(ForumPersistentData fpd) {
+		return fpd;
+	}
 }
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index cfa26e8c8adf0b7e0fd68a6ec1ed4cbb4a185eba..b99cb9f4733ae8c8a6e8c3799ab58b001d4fe45a 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -3,18 +3,23 @@ package org.briarproject.android.forum;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v4.content.ContextCompat;
 import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.LinearLayout;
-import android.widget.ListView;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.Toast;
 
@@ -22,76 +27,61 @@ import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.BriarActivity;
 import org.briarproject.android.api.AndroidNotificationManager;
-import org.briarproject.android.util.ListLoadingProgressBar;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.NoSuchGroupException;
-import org.briarproject.api.db.NoSuchMessageException;
-import org.briarproject.api.event.Event;
-import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.EventListener;
-import org.briarproject.api.event.GroupRemovedEvent;
-import org.briarproject.api.event.MessageStateChangedEvent;
-import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.forum.ForumPostHeader;
-import org.briarproject.api.identity.Author;
+import org.briarproject.android.controller.handler.ResultHandler;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.util.CustomAnimations;
 import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
+import org.briarproject.util.StringUtils;
 
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
+import im.delight.android.identicons.IdenticonDrawable;
+
 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
 import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
-import static android.support.design.widget.Snackbar.LENGTH_LONG;
-import static android.view.Gravity.CENTER;
-import static android.view.Gravity.CENTER_HORIZONTAL;
 import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
-import static android.widget.LinearLayout.VERTICAL;
 import static android.widget.Toast.LENGTH_SHORT;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.android.forum.ReadForumPostActivity.RESULT_PREV_NEXT;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
-import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 
-public class ForumActivity extends BriarActivity implements EventListener,
-		OnItemClickListener {
+public class ForumActivity extends BriarActivity implements
+		ForumController.ForumPostListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(ForumActivity.class.getName());
 
 	public static final String FORUM_NAME = "briar.FORUM_NAME";
 	public static final String MIN_TIMESTAMP = "briar.MIN_TIMESTAMP";
-
-	private static final int REQUEST_READ = 2;
 	private static final int REQUEST_FORUM_SHARED = 3;
-	private static final Logger LOG =
-			Logger.getLogger(ForumActivity.class.getName());
 
-	@Inject protected AndroidNotificationManager notificationManager;
-	private Map<MessageId, byte[]> bodyCache = new HashMap<>();
-	private TextView empty = null;
-	private ForumAdapter adapter = null;
-	private ListView list = null;
-	private ListLoadingProgressBar loading = null;
+	@Inject
+	protected AndroidNotificationManager notificationManager;
+
+	// uncomment the next line for a test component with dummy data
+//	@Named("ForumTestController")
+	@Inject
+	protected ForumController forumController;
+
+	private BriarRecyclerView recyclerView;
+	private EditText textInput;
+	private ViewGroup inputContainer;
+	private LinearLayoutManager linearLayoutManager;
 
-	// Fields that are accessed from background threads must be volatile
-	@Inject protected volatile ForumManager forumManager;
-	@Inject protected volatile EventBus eventBus;
 	private volatile GroupId groupId = null;
-	private volatile Forum forum = null;
+
+	protected ForumAdapter forumAdapter;
 
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(state);
 
+		setContentView(R.layout.activity_forum);
+
 		Intent i = getIntent();
 		byte[] b = i.getByteArrayExtra(GROUP_ID);
 		if (b == null) throw new IllegalStateException();
@@ -99,32 +89,30 @@ public class ForumActivity extends BriarActivity implements EventListener,
 		String forumName = i.getStringExtra(FORUM_NAME);
 		if (forumName != null) setTitle(forumName);
 
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(MATCH_MATCH);
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-
-		empty = new TextView(this);
-		empty.setLayoutParams(MATCH_WRAP_1);
-		empty.setGravity(CENTER);
-		empty.setTextSize(18);
-		empty.setText(R.string.no_forum_posts);
-		empty.setVisibility(GONE);
-		layout.addView(empty);
-
-		adapter = new ForumAdapter(this);
-		list = new ListView(this);
-		list.setLayoutParams(MATCH_WRAP_1);
-		list.setAdapter(adapter);
-		list.setOnItemClickListener(this);
-		list.setVisibility(GONE);
-		layout.addView(list);
-
-		// Show a progress bar while the list is loading
-		loading = new ListLoadingProgressBar(this);
-		layout.addView(loading);
-
-		setContentView(layout);
+		inputContainer = (ViewGroup) findViewById(R.id.text_input_container);
+		inputContainer.setVisibility(GONE);
+		textInput = (EditText) findViewById(R.id.input_text);
+		recyclerView =
+				(BriarRecyclerView) findViewById(R.id.forum_discussion_list);
+		linearLayoutManager = new LinearLayoutManager(this);
+		recyclerView.setLayoutManager(linearLayoutManager);
+		recyclerView.showProgressBar();
+		forumController
+				.loadForum(groupId, new UiResultHandler<Boolean>(this) {
+					@Override
+					public void onResultUi(Boolean result) {
+						if (result) {
+							setTitle(forumController.getForumName());
+							forumAdapter = new ForumAdapter(
+									forumController.getForumEntries());
+							recyclerView.setAdapter(forumAdapter);
+							recyclerView.showData();
+						} else {
+							// TODO Maybe an error dialog ?
+							finish();
+						}
+					}
+				});
 	}
 
 	@Override
@@ -132,14 +120,20 @@ public class ForumActivity extends BriarActivity implements EventListener,
 		component.inject(this);
 	}
 
+	private void displaySnackbar(int stringId) {
+		Snackbar snackbar =
+				Snackbar.make(recyclerView, stringId, Snackbar.LENGTH_SHORT);
+		snackbar.getView().setBackgroundResource(R.color.briar_primary);
+		snackbar.show();
+	}
+
 	@Override
-	public void onResume() {
-		super.onResume();
-		eventBus.addListener(this);
-		notificationManager.blockNotification(groupId);
-		notificationManager.clearForumPostNotification(groupId);
-		loadForum();
-		loadHeaders();
+	protected void onActivityResult(int request, int result, Intent data) {
+		super.onActivityResult(request, result, data);
+
+		if (request == REQUEST_FORUM_SHARED && result == RESULT_OK) {
+			displaySnackbar(R.string.forum_shared_snackbar);
+		}
 	}
 
 	@Override
@@ -151,6 +145,27 @@ public class ForumActivity extends BriarActivity implements EventListener,
 		return super.onCreateOptionsMenu(menu);
 	}
 
+	@Override
+	public void onBackPressed() {
+		if (inputContainer.getVisibility() == VISIBLE) {
+			inputContainer.setVisibility(GONE);
+			forumAdapter.setReplyEntry(null);
+		} else {
+			super.onBackPressed();
+		}
+	}
+
+	private void showTextInput(boolean isNewMessage) {
+		// An animation here would be an overkill because of the keyboard
+		// popping up.
+		inputContainer.setVisibility(View.VISIBLE);
+		textInput.setText("");
+		textInput.requestFocus();
+		textInput.setHint(isNewMessage ? R.string.forum_new_message_hint :
+				R.string.forum_message_reply_hint);
+		showSoftKeyboard(textInput);
+	}
+
 	@Override
 	public boolean onOptionsItemSelected(final MenuItem item) {
 		ActivityOptionsCompat options = ActivityOptionsCompat
@@ -159,11 +174,9 @@ public class ForumActivity extends BriarActivity implements EventListener,
 		// Handle presses on the action bar items
 		switch (item.getItemId()) {
 			case R.id.action_forum_compose_post:
-				Intent i = new Intent(this, WriteForumPostActivity.class);
-				i.putExtra(GROUP_ID, groupId.getBytes());
-				i.putExtra(FORUM_NAME, forum.getName());
-				i.putExtra(MIN_TIMESTAMP, getMinTimestampForNewPost());
-				startActivity(i);
+				if (inputContainer.getVisibility() != VISIBLE) {
+					showTextInput(true);
+				}
 				return true;
 			case R.id.action_forum_share:
 				Intent i2 = new Intent(this, ShareForumActivity.class);
@@ -187,263 +200,406 @@ public class ForumActivity extends BriarActivity implements EventListener,
 		}
 	}
 
-	private void loadForum() {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					forum = forumManager.getForum(groupId);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Loading forum " + duration + " ms");
-					displayForumName();
-				} catch (NoSuchGroupException e) {
-					finishOnUiThread();
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
+	@Override
+	public void onResume() {
+		super.onResume();
+		notificationManager.blockNotification(groupId);
+		notificationManager.clearForumPostNotification(groupId);
 	}
 
-	private void displayForumName() {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				setTitle(forum.getName());
-			}
-		});
+	@Override
+	public void onPause() {
+		super.onPause();
+		notificationManager.unblockNotification(groupId);
 	}
 
-	private void loadHeaders() {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					Collection<ForumPostHeader> headers =
-							forumManager.getPostHeaders(groupId);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Load took " + duration + " ms");
-					displayHeaders(headers);
-				} catch (NoSuchGroupException e) {
-					finishOnUiThread();
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
+	public void sendMessage(View view) {
+		String text = textInput.getText().toString();
+		if (text.trim().length() == 0)
+			return;
+		ForumEntry replyEntry = forumAdapter.getReplyEntry();
+		if (replyEntry == null) {
+			// root post
+			forumController.createPost(StringUtils.toUtf8(text));
+		} else {
+			forumController.createPost(StringUtils.toUtf8(text),
+					replyEntry.getMessageId());
+		}
+		hideSoftKeyboard(textInput);
+		inputContainer.setVisibility(GONE);
+		forumAdapter.setReplyEntry(null);
 	}
 
-	private void displayHeaders(final Collection<ForumPostHeader> headers) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				loading.setVisibility(GONE);
-				adapter.clear();
-				if (headers.isEmpty()) {
-					empty.setVisibility(VISIBLE);
-					list.setVisibility(GONE);
-				} else {
-					empty.setVisibility(GONE);
-					list.setVisibility(VISIBLE);
-					for (ForumPostHeader h : headers) {
-						ForumItem item = new ForumItem(h);
-						byte[] body = bodyCache.get(h.getId());
-						if (body == null) loadPostBody(h);
-						else item.setBody(body);
-						adapter.add(item);
+	private void showUnsubscribeDialog() {
+		DialogInterface.OnClickListener okListener =
+				new DialogInterface.OnClickListener() {
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						forumController.unsubscribe(
+								new UiResultHandler<Boolean>(
+										ForumActivity.this) {
+									@Override
+									public void onResultUi(Boolean result) {
+										if (result) {
+											Toast.makeText(ForumActivity.this,
+													R.string.forum_left_toast,
+													LENGTH_SHORT)
+													.show();
+										}
+									}
+								});
 					}
-					adapter.sort(ForumItemComparator.INSTANCE);
-					// Scroll to the bottom
-					list.setSelection(adapter.getCount() - 1);
-				}
-			}
-		});
+				};
+		AlertDialog.Builder builder =
+				new AlertDialog.Builder(ForumActivity.this,
+						R.style.BriarDialogTheme);
+		builder.setTitle(getString(R.string.dialog_title_leave_forum));
+		builder.setMessage(getString(R.string.dialog_message_leave_forum));
+		builder.setPositiveButton(R.string.dialog_button_leave, okListener);
+		builder.setNegativeButton(android.R.string.cancel, null);
+		builder.show();
 	}
 
-	private void loadPostBody(final ForumPostHeader h) {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					byte[] body = forumManager.getPostBody(h.getId());
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Loading message took " + duration + " ms");
-					displayPost(h.getId(), body);
-				} catch (NoSuchMessageException e) {
-					// The item will be removed when we get the event
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
+	@Override
+	public void addLocalEntry(int index, ForumEntry entry) {
+		forumAdapter.addEntry(index, entry, true);
+		displaySnackbar(R.string.forum_new_entry_posted);
+	}
+
+	@Override
+	public void addForeignEntry(int index, ForumEntry entry) {
+		forumAdapter.addEntry(index, entry, false);
+		displaySnackbar(R.string.forum_new_entry_received);
+	}
+
+	static class ForumViewHolder extends RecyclerView.ViewHolder {
+
+		public final TextView textView, lvlText, dateText, repliesText;
+		public final View[] lvls;
+		public final ImageView avatar;
+		public final View chevron, replyButton;
+		public final ViewGroup cell;
+		public final View bottomDivider;
+
+		public ForumViewHolder(View v) {
+			super(v);
+
+			textView = (TextView) v.findViewById(R.id.text);
+			lvlText = (TextView) v.findViewById(R.id.nested_line_text);
+			dateText = (TextView) v.findViewById(R.id.date);
+			repliesText = (TextView) v.findViewById(R.id.replies);
+			int[] nestedLineIds = {
+					R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
+					R.id.nested_line_4, R.id.nested_line_5
+			};
+			lvls = new View[nestedLineIds.length];
+			for (int i = 0; i < lvls.length; i++) {
+				lvls[i] = v.findViewById(nestedLineIds[i]);
 			}
-		});
+			avatar = (ImageView) v.findViewById(R.id.avatar);
+			chevron = v.findViewById(R.id.chevron);
+			replyButton = v.findViewById(R.id.btn_reply);
+			cell = (ViewGroup) v.findViewById(R.id.forum_cell);
+			bottomDivider = v.findViewById(R.id.bottom_divider);
+		}
 	}
 
-	private void displayPost(final MessageId m, final byte[] body) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				bodyCache.put(m, body);
-				int count = adapter.getCount();
-				for (int i = 0; i < count; i++) {
-					ForumItem item = adapter.getItem(i);
-					if (item.getHeader().getId().equals(m)) {
-						item.setBody(body);
-						adapter.notifyDataSetChanged();
-						// Scroll to the bottom
-						list.setSelection(count - 1);
-						return;
+	public class ForumAdapter extends RecyclerView.Adapter<ForumViewHolder> {
+
+		private final List<ForumEntry> forumEntries;
+		// highlight not depandant on time
+		private ForumEntry replyEntry;
+//		 temporary highlight
+		private ForumEntry addedEntry;
+
+		public ForumAdapter(@NonNull List<ForumEntry> forumEntries) {
+			this.forumEntries = forumEntries;
+		}
+
+		private ForumEntry getReplyEntry() {
+			return replyEntry;
+		}
+
+		public void addEntry(int index, ForumEntry entry,
+				boolean isScrolling) {
+			forumEntries.add(index, entry);
+			boolean isShowingDescendants = false;
+			if (entry.getLevel() > 0) {
+				// update parent and make sure descendants are visible
+				// Note that the parent's visibility is guaranteed (otherwise
+				// the reply button would not be visible)
+				for (int i = index - 1; i >= 0; i--) {
+					ForumEntry higherEntry = forumEntries.get(i);
+					if (higherEntry.getLevel() < entry.getLevel()) {
+						// parent found
+						if (!higherEntry.isShowingDescendants()) {
+							isShowingDescendants = true;
+							showDescendants(higherEntry);
+						}
+						break;
 					}
 				}
 			}
-		});
-	}
+			if (!isShowingDescendants) {
+				int visiblePos = getVisiblePos(entry);
+				notifyItemInserted(visiblePos);
+				if (isScrolling)
+					linearLayoutManager
+							.scrollToPositionWithOffset(visiblePos, 0);
+			}
+			addedEntry = entry;
+		}
 
-	@Override
-	protected void onActivityResult(int request, int result, Intent data) {
-		super.onActivityResult(request, result, data);
-		if (request == REQUEST_READ && result == RESULT_PREV_NEXT) {
-			int position = data.getIntExtra("briar.POSITION", -1);
-			if (position >= 0 && position < adapter.getCount())
-				displayPost(position);
+		private boolean hasDescendants(ForumEntry forumEntry) {
+			int i = forumEntries.indexOf(forumEntry);
+			if (i >= 0 && i < forumEntries.size() - 1) {
+				if (forumEntries.get(i + 1).getLevel() >
+						forumEntry.getLevel()) {
+					return true;
+				}
+			}
+			return false;
 		}
-		else if (request == REQUEST_FORUM_SHARED && result == RESULT_OK) {
-			Snackbar s = Snackbar.make(list, R.string.forum_shared_snackbar,
-					LENGTH_LONG);
-			s.getView().setBackgroundResource(R.color.briar_primary);
-			s.show();
+
+		private boolean hasVisibleDescendants(int visiblePos) {
+			int levelLimit = forumEntries.get(visiblePos).getLevel();
+			for (int i = visiblePos + 1; i < getItemCount(); i++) {
+				ForumEntry entry = getVisibleEntry(i);
+				if (entry.getLevel() <= levelLimit)
+					break;
+				return true;
+			}
+			return false;
 		}
-	}
 
-	@Override
-	public void onPause() {
-		super.onPause();
-		eventBus.removeListener(this);
-		notificationManager.unblockNotification(groupId);
-		if (isFinishing()) markPostsRead();
-	}
+		private int getReplyCount(ForumEntry entry) {
+			int counter = 0;
+			int pos = forumEntries.indexOf(entry);
+			if (pos >= 0) {
+				int ancestorLvl = forumEntries.get(pos).getLevel();
+				for (int i = pos + 1; i < forumEntries.size(); i++) {
+					int descendantLvl = forumEntries.get(i).getLevel();
+					if (descendantLvl <= ancestorLvl)
+						break;
+					if (descendantLvl == ancestorLvl + 1)
+						counter++;
+				}
+			}
+			return counter;
+		}
 
-	private void markPostsRead() {
-		List<MessageId> unread = new ArrayList<>();
-		int count = adapter.getCount();
-		for (int i = 0; i < count; i++) {
-			ForumPostHeader h = adapter.getItem(i).getHeader();
-			if (!h.isRead()) unread.add(h.getId());
+		public void setReplyEntry(ForumEntry entry) {
+			if (replyEntry != null) {
+				notifyItemChanged(getVisiblePos(replyEntry));
+			}
+			replyEntry = entry;
+			if (replyEntry != null) {
+				notifyItemChanged(getVisiblePos(replyEntry));
+			}
 		}
-		if (unread.isEmpty()) return;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Marking " + unread.size() + " posts read");
-		markPostsRead(Collections.unmodifiableList(unread));
-	}
 
-	private void markPostsRead(final Collection<MessageId> unread) {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					for (MessageId m : unread)
-						forumManager.setReadFlag(m, true);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Marking read took " + duration + " ms");
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
+		private List<Integer> getSubTreeIndexes(int pos, int levelLimit) {
+			List<Integer> indexList = new ArrayList<>();
+
+			for (int i = pos + 1; i < getItemCount(); i++) {
+				ForumEntry entry = getVisibleEntry(i);
+				if (entry.getLevel() > levelLimit) {
+					indexList.add(i);
+				} else {
+					break;
 				}
 			}
-		});
-	}
+			return indexList;
+		}
 
-	public void eventOccurred(Event e) {
-		if (e instanceof MessageStateChangedEvent) {
-			MessageStateChangedEvent m = (MessageStateChangedEvent) e;
-			if (m.getState() == DELIVERED &&
-					m.getMessage().getGroupId().equals(groupId)) {
-				LOG.info("Message added, reloading");
-				loadHeaders();
+		public void showDescendants(ForumEntry forumEntry) {
+			forumEntry.setShowingDescendants(true);
+			int visiblePos = getVisiblePos(forumEntry);
+			List<Integer> indexList =
+					getSubTreeIndexes(visiblePos, forumEntry.getLevel());
+			if (!indexList.isEmpty()) {
+				if (indexList.size() == 1) {
+					notifyItemInserted(indexList.get(0));
+				} else {
+					notifyItemRangeInserted(indexList.get(0),
+							indexList.size());
+				}
 			}
-		} else if (e instanceof GroupRemovedEvent) {
-			GroupRemovedEvent s = (GroupRemovedEvent) e;
-			if (s.getGroup().getId().equals(groupId)) {
-				LOG.info("Forum removed");
-				finishOnUiThread();
+		}
+
+		public void hideDescendants(ForumEntry forumEntry) {
+			int visiblePos = getVisiblePos(forumEntry);
+			List<Integer> indexList =
+					getSubTreeIndexes(visiblePos, forumEntry.getLevel());
+			if (!indexList.isEmpty()) {
+				if (indexList.size() == 1) {
+					notifyItemRemoved(indexList.get(0));
+				} else {
+					notifyItemRangeRemoved(indexList.get(0),
+							indexList.size());
+				}
 			}
+			forumEntry.setShowingDescendants(false);
 		}
-	}
 
-	private long getMinTimestampForNewPost() {
-		// Don't use an earlier timestamp than the newest post
-		long timestamp = 0;
-		int count = adapter.getCount();
-		for (int i = 0; i < count; i++) {
-			long t = adapter.getItem(i).getHeader().getTimestamp();
-			if (t > timestamp) timestamp = t;
+		public int getVisiblePos(ForumEntry entry) {
+			int visibleCounter = 0;
+			int levelLimit = -1;
+			for (int i = 0; i < forumEntries.size(); i++) {
+				ForumEntry forumEntry = forumEntries.get(i);
+				if (forumEntry.equals(entry)) {
+					return visibleCounter;
+				} else if (levelLimit >= 0 &&
+						levelLimit < forumEntry.getLevel()) {
+					// entry is in a hidden sub-tree
+					continue;
+				}
+				levelLimit = -1;
+				if (!forumEntry.isShowingDescendants()) {
+					levelLimit = forumEntry.getLevel();
+				}
+				visibleCounter++;
+			}
+			return -1;
 		}
-		return timestamp + 1;
-	}
 
-	public void onItemClick(AdapterView<?> parent, View view, int position,
-			long id) {
-		displayPost(position);
-	}
+		@NonNull
+		public ForumEntry getVisibleEntry(int position) {
+			int levelLimit = -1;
+			for (ForumEntry forumEntry : forumEntries) {
+				if (levelLimit >= 0) {
+					if (forumEntry.getLevel() > levelLimit) {
+						continue;
+					}
+					levelLimit = -1;
+				}
+				if (!forumEntry.isShowingDescendants()) {
+					levelLimit = forumEntry.getLevel();
+				}
+				if (position-- == 0) {
+					return forumEntry;
+				}
+			}
+			return null;
+		}
 
-	private void displayPost(int position) {
-		ForumPostHeader header = adapter.getItem(position).getHeader();
-		Intent i = new Intent(this, ReadForumPostActivity.class);
-		i.putExtra(GROUP_ID, groupId.getBytes());
-		i.putExtra(FORUM_NAME, forum.getName());
-		i.putExtra("briar.MESSAGE_ID", header.getId().getBytes());
-		Author author = header.getAuthor();
-		if (author != null) {
-			i.putExtra("briar.AUTHOR_NAME", author.getName());
-			i.putExtra("briar.AUTHOR_ID", author.getId().getBytes());
+		@Override
+		public ForumViewHolder onCreateViewHolder(
+				ViewGroup parent, int viewType) {
+			View v = LayoutInflater.from(parent.getContext())
+					.inflate(R.layout.forum_discussion_cell, parent, false);
+			return new ForumViewHolder(v);
 		}
-		i.putExtra("briar.AUTHOR_STATUS", header.getAuthorStatus().name());
-		i.putExtra("briar.CONTENT_TYPE", header.getContentType());
-		i.putExtra("briar.TIMESTAMP", header.getTimestamp());
-		i.putExtra(MIN_TIMESTAMP, getMinTimestampForNewPost());
-		i.putExtra("briar.POSITION", position);
-		startActivityForResult(i, REQUEST_READ);
-	}
 
-	private void showUnsubscribeDialog() {
-		DialogInterface.OnClickListener okListener =
-				new DialogInterface.OnClickListener() {
+		@Override
+		public void onBindViewHolder(
+				final ForumViewHolder ui, final int position) {
+			final ForumEntry data = getVisibleEntry(position);
+			if (!data.isRead()) {
+				data.setRead(true);
+				forumController.entryRead(data);
+			}
+			ui.textView.setText(data.getText());
+
+			for (int i = 0; i < ui.lvls.length; i++) {
+				ui.lvls[i].setVisibility(i < data.getLevel() ? VISIBLE : GONE);
+			}
+			if (data.getLevel() > 5) {
+				ui.lvlText.setVisibility(VISIBLE);
+				ui.lvlText.setText("" + data.getLevel());
+			} else {
+				ui.lvlText.setVisibility(GONE);
+			}
+			ui.dateText.setText(DateUtils
+					.getRelativeTimeSpanString(ForumActivity.this,
+							data.getTimestamp()) + " " + data.getAuthor());
+
+			int replies = getReplyCount(data);
+			if (replies == 0) {
+				ui.repliesText.setText("");
+			} else {
+				ui.repliesText.setText(getResources()
+						.getQuantityString(R.plurals.message_replies, replies,
+								replies));
+			}
+			ui.avatar.setImageDrawable(
+					new IdenticonDrawable(data.getAuthorId().getBytes()));
+
+			if (hasDescendants(data)) {
+				ui.chevron.setVisibility(VISIBLE);
+				if (hasVisibleDescendants(position)) {
+					ui.chevron.setSelected(false);
+				} else {
+					ui.chevron.setSelected(true);
+				}
+				ui.chevron.setOnClickListener(new View.OnClickListener() {
 					@Override
-					public void onClick(DialogInterface dialog, int which) {
-						unsubscribe(forum);
-						Toast.makeText(ForumActivity.this,
-								R.string.forum_left_toast, LENGTH_SHORT)
-								.show();
+					public void onClick(View v) {
+						ui.chevron.setSelected(!ui.chevron.isSelected());
+						if (ui.chevron.isSelected()) {
+							hideDescendants(data);
+						} else {
+							showDescendants(data);
+						}
 					}
-				};
-		AlertDialog.Builder builder =
-				new AlertDialog.Builder(ForumActivity.this,
-						R.style.BriarDialogTheme);
-		builder.setTitle(getString(R.string.dialog_title_leave_forum));
-		builder.setMessage(getString(R.string.dialog_message_leave_forum));
-		builder.setPositiveButton(R.string.dialog_button_leave, okListener);
-		builder.setNegativeButton(android.R.string.cancel, null);
-		builder.show();
-	}
+				});
+			} else {
+				ui.chevron.setVisibility(INVISIBLE);
+			}
+			if (data.equals(replyEntry)) {
+				ui.cell.setBackgroundColor(ContextCompat
+						.getColor(ForumActivity.this,
+								R.color.forum_cell_highlight));
+			} else if (data.equals(addedEntry)) {
+				CustomAnimations.animateColorTransition(ui.cell, ContextCompat
+								.getColor(ForumActivity.this,
+										R.color.window_background), 3000,
+						new ResultHandler<Void>() {
+							@Override
+							public void onResult(Void result) {
+								ui.setIsRecyclable(true);
+							}
+						});
+				// don't allow cell recycling until the animation finishes
+				ui.setIsRecyclable(false);
+				addedEntry = null;
+			} else {
+				ui.cell.setBackgroundColor(ContextCompat
+						.getColor(ForumActivity.this,
+								R.color.window_background));
+			}
+			ui.replyButton.setOnClickListener(new View.OnClickListener() {
+				@Override
+				public void onClick(View v) {
+					if (inputContainer.getVisibility() != VISIBLE) {
+						showTextInput(false);
+					}
+					setReplyEntry(data);
+					linearLayoutManager
+							.scrollToPositionWithOffset(position, 0);
+				}
+			});
+		}
 
-	private void unsubscribe(final Forum f) {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					forumManager.removeForum(f);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Removing forum took " + duration + " ms");
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
+		@Override
+		public int getItemCount() {
+			int visibleCounter = 0;
+			int levelLimit = -1;
+			for (ForumEntry forumEntry : forumEntries) {
+				if (levelLimit >= 0) {
+					if (forumEntry.getLevel() > levelLimit) {
+						continue;
+					}
+					levelLimit = -1;
+				}
+				if (!forumEntry.isShowingDescendants()) {
+					levelLimit = forumEntry.getLevel();
 				}
+				visibleCounter++;
 			}
-		});
+			return visibleCounter;
+		}
 	}
 
+
 }
diff --git a/briar-android/src/org/briarproject/android/forum/ForumController.java b/briar-android/src/org/briarproject/android/forum/ForumController.java
new file mode 100644
index 0000000000000000000000000000000000000000..2fa4f0dab0c58cbcd7017f717a44468f97bf7317
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumController.java
@@ -0,0 +1,27 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.Collection;
+import java.util.List;
+
+public interface ForumController extends ActivityLifecycleController {
+
+	void loadForum(GroupId groupId, UiResultHandler<Boolean> resultHandler);
+	String getForumName();
+	List<ForumEntry> getForumEntries();
+	void unsubscribe(UiResultHandler<Boolean> resultHandler);
+	void entryRead(ForumEntry forumEntry);
+	void entriesRead(Collection<ForumEntry> messageIds);
+	void createPost(byte[] body);
+	void createPost(byte[] body, MessageId parentId);
+
+	public interface ForumPostListener {
+		void addLocalEntry(int index, ForumEntry entry);
+		void addForeignEntry(int index, ForumEntry entry);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..676f2d51d0c977d41bcd2cc09ab5fbcb7bfeacfd
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
@@ -0,0 +1,371 @@
+package org.briarproject.android.forum;
+
+import android.app.Activity;
+
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.CryptoExecutor;
+import org.briarproject.api.crypto.KeyParser;
+import org.briarproject.api.crypto.PrivateKey;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
+import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumPost;
+import org.briarproject.api.forum.ForumPostFactory;
+import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.util.StringUtils;
+
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Stack;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+
+public class ForumControllerImpl extends DbControllerImpl
+		implements ForumController, EventListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(ForumControllerImpl.class.getName());
+
+	@Inject
+	protected Activity activity;
+	@Inject
+	@CryptoExecutor
+	protected Executor cryptoExecutor;
+	@Inject
+	protected volatile ForumPostFactory forumPostFactory;
+	@Inject
+	protected volatile CryptoComponent crypto;
+	@Inject
+	protected volatile ForumManager forumManager;
+	@Inject
+	protected volatile EventBus eventBus;
+	@Inject
+	protected volatile IdentityManager identityManager;
+	@Inject
+	protected ForumPersistentData data;
+
+	private ForumPostListener listener;
+	private MessageId localAdd = null;
+
+	@Inject
+	ForumControllerImpl() {
+
+	}
+
+	@Override
+	public void onActivityCreate() {
+		if (activity instanceof ForumPostListener) {
+			listener = (ForumPostListener) activity;
+		} else {
+			throw new IllegalStateException(
+					"An activity that injects the ForumController must " +
+							"implement the ForumPostListener");
+		}
+	}
+
+	@Override
+	public void onActivityResume() {
+		eventBus.addListener(this);
+	}
+
+	@Override
+	public void onActivityPause() {
+		eventBus.removeListener(this);
+	}
+
+	@Override
+	public void onActivityDestroy() {
+		if (activity.isFinishing()) {
+			data.clearAll();
+		}
+	}
+
+	private void findSingleNewEntry() {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				List<ForumEntry> oldEntries = getForumEntries();
+				data.clearHeaders();
+				try {
+					loadPosts();
+					List<ForumEntry> allEntries = getForumEntries();
+					int i = 0;
+					for (ForumEntry entry : allEntries) {
+						boolean isNew = true;
+						for (ForumEntry oldEntry : oldEntries) {
+							if (entry.getMessageId()
+									.equals(oldEntry.getMessageId())) {
+								isNew = false;
+								break;
+							}
+						}
+						if (isNew) {
+							if (localAdd != null &&
+									entry.getMessageId().equals(localAdd)) {
+								addLocalEntry(i, entry);
+							} else {
+								addForeignEntry(i, entry);
+							}
+							break;
+						}
+						i++;
+					}
+				} catch (DbException e) {
+					e.printStackTrace();
+				}
+			}
+		});
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof MessageStateChangedEvent) {
+			MessageStateChangedEvent m = (MessageStateChangedEvent) e;
+			if (m.getState() == DELIVERED &&
+					m.getMessage().getGroupId().equals(data.getGroupId())) {
+				LOG.info("Message added, reloading");
+				findSingleNewEntry();
+			}
+		} else if (e instanceof GroupRemovedEvent) {
+			GroupRemovedEvent s = (GroupRemovedEvent) e;
+			if (s.getGroup().getId().equals(data.getGroupId())) {
+				LOG.info("Forum removed");
+				activity.runOnUiThread(new Runnable() {
+					@Override
+					public void run() {
+						activity.finish();
+					}
+				});
+			}
+		}
+	}
+
+	private void loadAuthor() throws DbException {
+		Collection<LocalAuthor> localAuthors =
+				identityManager.getLocalAuthors();
+
+		for (LocalAuthor author : localAuthors) {
+			if (author == null)
+				continue;
+			data.setLocalAuthor(author);
+			break;
+		}
+	}
+
+	private void loadPosts() throws DbException {
+		long now = System.currentTimeMillis();
+		Collection<ForumPostHeader> headers =
+				forumManager.getPostHeaders(data.getGroupId());
+		data.addHeaders(headers);
+		long duration = System.currentTimeMillis() - now;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Loading headers took " + duration + " ms");
+		now = System.currentTimeMillis();
+		for (ForumPostHeader header : headers) {
+			if (data.getBody(header.getId()) == null) {
+				byte[] body = forumManager.getPostBody(header.getId());
+				data.addBody(header.getId(), body);
+			}
+		}
+		duration = System.currentTimeMillis() - now;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Loading bodies took " + duration + " ms");
+	}
+
+	@Override
+	public void loadForum(final GroupId groupId,
+			final UiResultHandler<Boolean> resultHandler) {
+		LOG.info("Loading forum...");
+
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					if (data.getGroupId() == null ||
+							!data.getGroupId().equals(groupId)) {
+						data.setGroupId(groupId);
+						long now = System.currentTimeMillis();
+						data.setForum(forumManager.getForum(groupId));
+						long duration = System.currentTimeMillis() - now;
+						if (LOG.isLoggable(INFO))
+							LOG.info("Loading forum took " + duration +
+									" ms");
+						now = System.currentTimeMillis();
+						loadAuthor();
+						duration = System.currentTimeMillis() - now;
+						if (LOG.isLoggable(INFO))
+							LOG.info("Loading author took " + duration +
+									" ms");
+						loadPosts();
+					}
+					resultHandler.onResult(true);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					resultHandler.onResult(false);
+				}
+			}
+		});
+
+	}
+
+	@Override
+	public String getForumName() {
+		return data.getForum() == null ? null : data.getForum().getName();
+	}
+
+	@Override
+	public List<ForumEntry> getForumEntries() {
+		Collection<ForumPostHeader> headers = data.getHeaders();
+		List<ForumEntry> forumEntries = new ArrayList<>();
+		Stack<MessageId> idStack = new Stack<>();
+
+		for (ForumPostHeader h : headers) {
+			if (h.getParentId() == null) {
+				idStack.clear();
+			} else if (idStack.isEmpty() ||
+					!idStack.contains(h.getParentId())) {
+				idStack.push(h.getParentId());
+			} else if (!h.getParentId().equals(idStack.peek())) {
+				do {
+					idStack.pop();
+				} while (!h.getParentId().equals(idStack.peek()));
+			}
+			forumEntries.add(new ForumEntry(h,
+					StringUtils.fromUtf8(data.getBody(h.getId())),
+					idStack.size()));
+		}
+		return forumEntries;
+	}
+
+	@Override
+	public void unsubscribe(final UiResultHandler<Boolean> resultHandler) {
+		runOnDbThread(new Runnable() {
+			public void run() {
+				try {
+					long now = System.currentTimeMillis();
+					forumManager.removeForum(data.getForum());
+					long duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info("Removing forum took " + duration + " ms");
+					resultHandler.onResult(true);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					resultHandler.onResult(false);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void entryRead(ForumEntry forumEntry) {
+		entriesRead(Collections.singletonList(forumEntry));
+	}
+
+	@Override
+	public void entriesRead(final Collection<ForumEntry> forumEntries) {
+		runOnDbThread(new Runnable() {
+			public void run() {
+				try {
+					long now = System.currentTimeMillis();
+					for (ForumEntry fe : forumEntries) {
+						forumManager.setReadFlag(fe.getMessageId(), true);
+					}
+					long duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info("Marking read took " + duration + " ms");
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void createPost(byte[] body) {
+		createPost(body, null);
+	}
+
+	@Override
+	public void createPost(final byte[] body, final MessageId parentId) {
+		cryptoExecutor.execute(new Runnable() {
+			public void run() {
+				// Don't use an earlier timestamp than the newest post
+				long timestamp = System.currentTimeMillis();
+				ForumPost p;
+				try {
+					KeyParser keyParser = crypto.getSignatureKeyParser();
+					byte[] b = data.getLocalAuthor().getPrivateKey();
+					PrivateKey authorKey = keyParser.parsePrivateKey(b);
+					p = forumPostFactory.createPseudonymousPost(
+							data.getGroupId(), timestamp, parentId,
+							data.getLocalAuthor(), "text/plain", body,
+							authorKey);
+				} catch (GeneralSecurityException | FormatException e) {
+					throw new RuntimeException(e);
+				}
+				storePost(p);
+			}
+		});
+	}
+
+	private void addLocalEntry(final int index, final ForumEntry entry) {
+		activity.runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				listener.addLocalEntry(index, entry);
+			}
+		});
+	}
+
+	private void addForeignEntry(final int index, final ForumEntry entry) {
+		activity.runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				listener.addForeignEntry(index, entry);
+			}
+		});
+	}
+
+	private void storePost(final ForumPost p) {
+		runOnDbThread(new Runnable() {
+			public void run() {
+				try {
+					localAdd = p.getMessage().getId();
+					long now = System.currentTimeMillis();
+					forumManager.addLocalPost(p);
+					long duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info(
+								"Storing message took " + duration + " ms");
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumEntry.java b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3c668f7a052d46c0cadefd3e05e1d524c960fd2
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
@@ -0,0 +1,73 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.MessageId;
+
+public class ForumEntry {
+
+	private final MessageId messageId;
+	private final String text;
+	private final int level;
+	private final long timestamp;
+	private final String author;
+	private final AuthorId authorId;
+	private boolean isShowingDescendants = true;
+	private boolean isRead = true;
+
+	public ForumEntry(ForumPostHeader h, String text, int level) {
+		this(h.getId(), text, level, h.getTimestamp(), h.getAuthor().getName(),
+				h.getAuthor().getId());
+		this.isRead = h.isRead();
+	}
+
+	public ForumEntry(MessageId messageId, String text, int level,
+			long timestamp, String author, AuthorId authorId) {
+		this.messageId = messageId;
+		this.text = text;
+		this.level = level;
+		this.timestamp = timestamp;
+		this.author = author;
+		this.authorId = authorId;
+	}
+
+	public String getText() {
+		return text;
+	}
+
+	public int getLevel() {
+		return level;
+	}
+
+	public long getTimestamp() {
+		return timestamp;
+	}
+
+	public String getAuthor() {
+		return author;
+	}
+
+	public AuthorId getAuthorId() {
+		return authorId;
+	}
+
+	public boolean isShowingDescendants() {
+		return isShowingDescendants;
+	}
+
+	public void setShowingDescendants(boolean showingDescendants) {
+		this.isShowingDescendants = showingDescendants;
+	}
+
+	public MessageId getMessageId() {
+		return messageId;
+	}
+
+	public boolean isRead() {
+		return isRead;
+	}
+
+	public void setRead(boolean read) {
+		isRead = read;
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumPersistentData.java b/briar-android/src/org/briarproject/android/forum/ForumPersistentData.java
new file mode 100644
index 0000000000000000000000000000000000000000..630c371aeadc7bd75034c550f4aaaeddf5d520bd
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumPersistentData.java
@@ -0,0 +1,88 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.api.clients.MessageTree;
+import org.briarproject.api.forum.Forum;
+import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.clients.MessageTreeImpl;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+/**
+ * This class is a singleton that defines the data that should persist, i.e.
+ * still be present in memory after activity restarts. This class is not thread
+ * safe.
+ */
+public class ForumPersistentData {
+
+	protected volatile MessageTree<ForumPostHeader> tree =
+			new MessageTreeImpl<>();
+	private volatile Map<MessageId, byte[]> bodyCache = new HashMap<>();
+	private volatile LocalAuthor localAuthor;
+	private volatile Forum forum;
+	private volatile GroupId groupId;
+
+	@Inject
+	public ForumPersistentData() {
+
+	}
+
+	public void clearAll() {
+		tree.clear();
+		bodyCache.clear();
+		localAuthor = null;
+		forum = null;
+		groupId = null;
+	}
+
+	public void clearHeaders() {
+		tree.clear();
+	}
+
+	public void addHeaders(Collection<ForumPostHeader> headers) {
+		tree.add(headers);
+	}
+
+	public Collection<ForumPostHeader> getHeaders() {
+		return tree.depthFirstOrder();
+	}
+
+	public void addBody(MessageId messageId, byte[] body) {
+		bodyCache.put(messageId, body);
+	}
+
+	public byte[] getBody(MessageId messageId) {
+		return bodyCache.get(messageId);
+	}
+
+	public LocalAuthor getLocalAuthor() {
+		return localAuthor;
+	}
+
+	public void setLocalAuthor(
+			LocalAuthor localAuthor) {
+		this.localAuthor = localAuthor;
+	}
+
+	public Forum getForum() {
+		return forum;
+	}
+
+	public void setForum(Forum forum) {
+		this.forum = forum;
+	}
+
+	public GroupId getGroupId() {
+		return groupId;
+	}
+
+	public void setGroupId(GroupId groupId) {
+		this.groupId = groupId;
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec1470ed85ceeaa6a34436bd6b5b0cc1a1c3de5d
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java
@@ -0,0 +1,179 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.UniqueId;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+public class ForumTestControllerImpl implements ForumController {
+
+	private static final Logger LOG =
+			Logger.getLogger(ForumControllerImpl.class.getName());
+
+	private final static String[] AUTHORS = {
+			"Guðmundur",
+			"Jónas",
+			"Geir Þorsteinn Gísli Máni Halldórsson Guðjónsson Mogensen",
+			"Baldur Friðrik",
+			"Anna Katrín",
+			"Þór",
+			"Anna Þorbjörg",
+			"Guðrún",
+			"Helga",
+			"Haraldur"
+	};
+
+	private final static AuthorId[] AUTHOR_ID = new AuthorId[AUTHORS.length];
+
+	static {
+		SecureRandom random = new SecureRandom();
+		for (int i = 0; i < AUTHOR_ID.length; i++) {
+			byte[] b = new byte[UniqueId.LENGTH];
+			random.nextBytes(b);
+			AUTHOR_ID[i] = new AuthorId(b);
+
+		}
+	}
+
+	private final static String SAGA =
+			"Það er upphaf á sögu þessari að Hákon konungur " +
+					"Aðalsteinsfóstri réð fyrir Noregi og var þetta á ofanverðum " +
+					"hans dögum. Þorkell hét maður; hann var kallaður skerauki; " +
+					"hann bjó í Súrnadal og var hersir að nafnbót. Hann átti sér " +
+					"konu er Ísgerður hét og sonu þrjá barna; hét einn Ari, annar " +
+					"Gísli, þriðji Þorbjörn, hann var þeirra yngstur, og uxu allir " +
+					"upp heima þar. " +
+					"Maður er nefndur Ísi; hann bjó í firði er Fibuli heitir á " +
+					"Norðmæri; kona hans hét Ingigerður en Ingibjörg dóttir. Ari, " +
+					"sonur Þorkels Sýrdæls, biður hennar og var hún honum gefin " +
+					"með miklu fé. Kolur hét þræll er í brott fór með henni.";
+
+	private ForumEntry[] forumEntries;
+
+	@Inject
+	public ForumTestControllerImpl() {
+
+	}
+
+	private void textRandomize(SecureRandom random, int[] i) {
+		for (int e = 0; e < forumEntries.length; e++) {
+			// select a random white-space for the cut-off
+			do {
+				i[e] = Math.abs(random.nextInt() % (SAGA.length()));
+			} while (SAGA.charAt(i[e]) != ' ');
+		}
+	}
+
+	private int levelRandomize(SecureRandom random, int[] l) {
+		int maxl = 0;
+		int lastl = 0;
+		l[0] = 0;
+		for (int e = 1; e < forumEntries.length; e++) {
+			// select random level 1-10
+			do {
+				l[e] = Math.abs(random.nextInt() % 10);
+			} while (l[e] > lastl + 1);
+			lastl = l[e];
+			if (lastl > maxl)
+				maxl = lastl;
+		}
+		return maxl;
+	}
+
+	@Override
+	public void loadForum(GroupId groupId,
+			UiResultHandler<Boolean> resultHandler) {
+		SecureRandom random = new SecureRandom();
+		forumEntries = new ForumEntry[100];
+		// string cut off index
+		int[] i = new int[forumEntries.length];
+		// entry discussion level
+		int[] l = new int[forumEntries.length];
+
+		textRandomize(random, i);
+		int maxLevel;
+		// make sure we get a deep discussion
+		do {
+			maxLevel = levelRandomize(random, l);
+		} while (maxLevel < 6);
+		for (int e = 0; e < forumEntries.length; e++) {
+			int authorIndex = Math.abs(random.nextInt() % AUTHORS.length);
+			long timestamp =
+					System.currentTimeMillis() - Math.abs(random.nextInt());
+			byte[] b = new byte[UniqueId.LENGTH];
+			random.nextBytes(b);
+			forumEntries[e] =
+					new ForumEntry(new MessageId(b), SAGA.substring(0, i[e]),
+							l[e], timestamp, AUTHORS[authorIndex],
+							AUTHOR_ID[authorIndex]);
+		}
+		LOG.info("forum entries: " + forumEntries.length);
+		resultHandler.onResult(true);
+	}
+
+	@Override
+	public String getForumName() {
+		return "SAGA";
+	}
+
+	@Override
+	public List<ForumEntry> getForumEntries() {
+		return forumEntries == null ? null :
+				new ArrayList<ForumEntry>(Arrays.asList(forumEntries));
+	}
+
+	@Override
+	public void unsubscribe(UiResultHandler<Boolean> resultHandler) {
+
+	}
+
+	@Override
+	public void entryRead(ForumEntry forumEntry) {
+
+	}
+
+	@Override
+	public void entriesRead(Collection<ForumEntry> messageIds) {
+
+	}
+
+	@Override
+	public void createPost(byte[] body) {
+
+	}
+
+	@Override
+	public void createPost(byte[] body, MessageId parentId) {
+
+	}
+
+	@Override
+	public void onActivityCreate() {
+
+	}
+
+	@Override
+	public void onActivityResume() {
+
+	}
+
+	@Override
+	public void onActivityPause() {
+
+	}
+
+	@Override
+	public void onActivityDestroy() {
+
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ReadForumPostActivity.java b/briar-android/src/org/briarproject/android/forum/ReadForumPostActivity.java
deleted file mode 100644
index cfc80ace2a4a4363327a4c88b3115bdc6cecbf64..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/forum/ReadForumPostActivity.java
+++ /dev/null
@@ -1,249 +0,0 @@
-package org.briarproject.android.forum;
-
-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.LinearLayout;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import org.briarproject.R;
-import org.briarproject.android.ActivityComponent;
-import org.briarproject.android.AndroidComponent;
-import org.briarproject.android.BriarActivity;
-import org.briarproject.android.util.AuthorView;
-import org.briarproject.android.util.ElasticHorizontalSpace;
-import org.briarproject.android.util.HorizontalBorder;
-import org.briarproject.android.util.LayoutUtils;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.NoSuchMessageException;
-import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.identity.Author;
-import org.briarproject.api.identity.AuthorId;
-import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
-import org.briarproject.util.StringUtils;
-
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
-import static android.view.Gravity.CENTER;
-import static android.view.Gravity.CENTER_VERTICAL;
-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 org.briarproject.android.forum.ForumActivity.FORUM_NAME;
-import static org.briarproject.android.forum.ForumActivity.MIN_TIMESTAMP;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
-import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
-
-public class ReadForumPostActivity extends BriarActivity
-implements OnClickListener {
-
-	static final int RESULT_REPLY = RESULT_FIRST_USER;
-	static final int RESULT_PREV_NEXT = RESULT_FIRST_USER + 1;
-
-	private static final Logger LOG =
-			Logger.getLogger(ReadForumPostActivity.class.getName());
-
-	private GroupId groupId = null;
-	private String forumName = null;
-	private long minTimestamp = -1;
-	private ImageButton prevButton = null, nextButton = null;
-	private ImageButton replyButton = null;
-	private TextView content = null;
-	private int position = -1;
-
-	// Fields that are accessed from background threads must be volatile
-	@Inject protected volatile ForumManager forumManager;
-	private volatile MessageId messageId = null;
-
-	@Override
-	public void onCreate(Bundle state) {
-		super.onCreate(state);
-
-		Intent i = getIntent();
-		byte[] b = i.getByteArrayExtra(GROUP_ID);
-		if (b == null) throw new IllegalStateException();
-		groupId = new GroupId(b);
-		forumName = i.getStringExtra(FORUM_NAME);
-		if (forumName == null) throw new IllegalStateException();
-		setTitle(forumName);
-		b = i.getByteArrayExtra("briar.MESSAGE_ID");
-		if (b == null) throw new IllegalStateException();
-		messageId = new MessageId(b);
-		String contentType = i.getStringExtra("briar.CONTENT_TYPE");
-		if (contentType == null) throw new IllegalStateException();
-		long timestamp = i.getLongExtra("briar.TIMESTAMP", -1);
-		if (timestamp == -1) throw new IllegalStateException();
-		minTimestamp = i.getLongExtra(MIN_TIMESTAMP, -1);
-		if (minTimestamp == -1) throw new IllegalStateException();
-		position = i.getIntExtra("briar.POSITION", -1);
-		if (position == -1) throw new IllegalStateException();
-		String authorName = i.getStringExtra("briar.AUTHOR_NAME");
-		AuthorId authorId = null;
-		b = i.getByteArrayExtra("briar.AUTHOR_ID");
-		if (b != null) authorId = new AuthorId(b);
-		String s = i.getStringExtra("briar.AUTHOR_STATUS");
-		if (s == null) throw new IllegalStateException();
-		Author.Status authorStatus = Author.Status.valueOf(s);
-
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(MATCH_MATCH);
-		layout.setOrientation(VERTICAL);
-
-		ScrollView scrollView = new ScrollView(this);
-		scrollView.setLayoutParams(MATCH_WRAP_1);
-
-		LinearLayout message = new LinearLayout(this);
-		message.setOrientation(VERTICAL);
-
-		LinearLayout header = new LinearLayout(this);
-		header.setLayoutParams(MATCH_WRAP);
-		header.setOrientation(HORIZONTAL);
-		header.setGravity(CENTER_VERTICAL);
-
-		int pad = LayoutUtils.getPadding(this);
-
-		AuthorView authorView = new AuthorView(this);
-		authorView.setPadding(0, pad, pad, pad);
-		authorView.setLayoutParams(WRAP_WRAP_1);
-		authorView.init(authorName, authorId, authorStatus);
-		header.addView(authorView);
-
-		TextView date = new TextView(this);
-		date.setPadding(pad, pad, pad, pad);
-		date.setText(DateUtils.getRelativeTimeSpanString(this, timestamp));
-		header.addView(date);
-		message.addView(header);
-
-		if (contentType.equals("text/plain")) {
-			// Load and display the message body
-			content = new TextView(this);
-			content.setPadding(pad, 0, pad, pad);
-			message.addView(content);
-			loadPostBody();
-		}
-		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);
-		Resources res = getResources();
-		footer.setBackgroundColor(res.getColor(R.color.button_bar_background));
-
-		prevButton = new ImageButton(this);
-		prevButton.setBackgroundResource(0);
-		prevButton.setImageResource(R.drawable.navigation_previous_item);
-		prevButton.setOnClickListener(this);
-		footer.addView(prevButton);
-		footer.addView(new ElasticHorizontalSpace(this));
-
-		nextButton = new ImageButton(this);
-		nextButton.setBackgroundResource(0);
-		nextButton.setImageResource(R.drawable.navigation_next_item);
-		nextButton.setOnClickListener(this);
-		footer.addView(nextButton);
-		footer.addView(new ElasticHorizontalSpace(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);
-	}
-
-	@Override
-	public void injectActivity(ActivityComponent component) {
-		component.inject(this);
-	}
-
-	@Override
-	public void onPause() {
-		super.onPause();
-		if (isFinishing()) markPostRead();
-	}
-
-	private void markPostRead() {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					forumManager.setReadFlag(messageId, true);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Marking read took " + duration + " ms");
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	private void loadPostBody() {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					byte[] body = forumManager.getPostBody(messageId);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Loading post took " + duration + " ms");
-					displayPostBody(StringUtils.fromUtf8(body));
-				} catch (NoSuchMessageException e) {
-					finishOnUiThread();
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	private void displayPostBody(final String body) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				content.setText(body);
-			}
-		});
-	}
-
-	public void onClick(View view) {
-		if (view == prevButton) {
-			Intent i = new Intent();
-			i.putExtra("briar.POSITION", position - 1);
-			setResult(RESULT_PREV_NEXT, i);
-			finish();
-		} else if (view == nextButton) {
-			Intent i = new Intent();
-			i.putExtra("briar.POSITION", position + 1);
-			setResult(RESULT_PREV_NEXT, i);
-			finish();
-		} else if (view == replyButton) {
-			Intent i = new Intent(this, WriteForumPostActivity.class);
-			i.putExtra(GROUP_ID, groupId.getBytes());
-			i.putExtra(FORUM_NAME, forumName);
-			i.putExtra("briar.PARENT_ID", messageId.getBytes());
-			i.putExtra(MIN_TIMESTAMP, minTimestamp);
-			startActivity(i);
-			setResult(RESULT_REPLY);
-			finish();
-		}
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java b/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java
deleted file mode 100644
index d2185aa4630df3d1dcd024fd322a6c4bcdd154dd..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java
+++ /dev/null
@@ -1,317 +0,0 @@
-package org.briarproject.android.forum;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.text.InputType;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
-import android.widget.EditText;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-import android.widget.Spinner;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import org.briarproject.R;
-import org.briarproject.android.ActivityComponent;
-import org.briarproject.android.AndroidComponent;
-import org.briarproject.android.BriarActivity;
-import org.briarproject.android.identity.CreateIdentityActivity;
-import org.briarproject.android.identity.LocalAuthorItem;
-import org.briarproject.android.identity.LocalAuthorItemComparator;
-import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
-import org.briarproject.android.util.CommonLayoutParams;
-import org.briarproject.android.util.LayoutUtils;
-import org.briarproject.api.FormatException;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.CryptoExecutor;
-import org.briarproject.api.crypto.KeyParser;
-import org.briarproject.api.crypto.PrivateKey;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.forum.ForumPost;
-import org.briarproject.api.forum.ForumPostFactory;
-import org.briarproject.api.identity.AuthorId;
-import org.briarproject.api.identity.IdentityManager;
-import org.briarproject.api.identity.LocalAuthor;
-import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
-import org.briarproject.util.StringUtils;
-
-import java.security.GeneralSecurityException;
-import java.util.Collection;
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
-import static android.text.InputType.TYPE_CLASS_TEXT;
-import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
-import static android.widget.LinearLayout.VERTICAL;
-import static android.widget.RelativeLayout.ALIGN_PARENT_LEFT;
-import static android.widget.RelativeLayout.ALIGN_PARENT_RIGHT;
-import static android.widget.RelativeLayout.CENTER_VERTICAL;
-import static android.widget.RelativeLayout.LEFT_OF;
-import static android.widget.RelativeLayout.RIGHT_OF;
-import static android.widget.Toast.LENGTH_LONG;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
-import static org.briarproject.android.forum.ForumActivity.MIN_TIMESTAMP;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
-
-public class WriteForumPostActivity extends BriarActivity
-implements OnItemSelectedListener, OnClickListener {
-
-	private static final int REQUEST_CREATE_IDENTITY = 2;
-	private static final Logger LOG =
-			Logger.getLogger(WriteForumPostActivity.class.getName());
-
-	@Inject @CryptoExecutor protected Executor cryptoExecutor;
-	private LocalAuthorSpinnerAdapter adapter = null;
-	private Spinner spinner = null;
-	private ImageButton sendButton = null;
-	private EditText content = null;
-	private AuthorId localAuthorId = null;
-	private GroupId groupId = null;
-
-	// Fields that are accessed from background threads must be volatile
-	@Inject protected volatile IdentityManager identityManager;
-	@Inject protected volatile ForumManager forumManager;
-	@Inject protected volatile ForumPostFactory forumPostFactory;
-	@Inject protected volatile CryptoComponent crypto;
-	private volatile MessageId parentId = null;
-	private volatile long minTimestamp = -1;
-	private volatile LocalAuthor localAuthor = null;
-	private volatile Forum forum = null;
-
-	@Override
-	public void onCreate(Bundle state) {
-		super.onCreate(state);
-
-		Intent i = getIntent();
-		byte[] b = i.getByteArrayExtra(GROUP_ID);
-		if (b == null) throw new IllegalStateException();
-		groupId = new GroupId(b);
-		String forumName = i.getStringExtra(FORUM_NAME);
-		if (forumName == null) throw new IllegalStateException();
-		setTitle(forumName);
-		minTimestamp = i.getLongExtra(MIN_TIMESTAMP, -1);
-		if (minTimestamp == -1) throw new IllegalStateException();
-		b = i.getByteArrayExtra("briar.PARENT_ID");
-		if (b != null) parentId = new MessageId(b);
-
-		if (state != null) {
-			b = state.getByteArray("briar.LOCAL_AUTHOR_ID");
-			if (b != null) localAuthorId = new AuthorId(b);
-		}
-
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(MATCH_WRAP);
-		layout.setOrientation(VERTICAL);
-		int pad = LayoutUtils.getPadding(this);
-		layout.setPadding(pad, 0, pad, pad);
-
-		RelativeLayout header = new RelativeLayout(this);
-
-		TextView from = new TextView(this);
-		from.setId(1);
-		from.setTextSize(18);
-		from.setText(R.string.from);
-		RelativeLayout.LayoutParams left = CommonLayoutParams.relative();
-		left.addRule(ALIGN_PARENT_LEFT);
-		left.addRule(CENTER_VERTICAL);
-		header.addView(from, left);
-
-		adapter = new LocalAuthorSpinnerAdapter(this, true);
-		spinner = new Spinner(this);
-		spinner.setId(2);
-		spinner.setAdapter(adapter);
-		spinner.setOnItemSelectedListener(this);
-		RelativeLayout.LayoutParams between = CommonLayoutParams.relative();
-		between.addRule(CENTER_VERTICAL);
-		between.addRule(RIGHT_OF, 1);
-		between.addRule(LEFT_OF, 3);
-		header.addView(spinner, between);
-
-		sendButton = new ImageButton(this);
-		sendButton.setId(3);
-		sendButton.setBackgroundResource(0);
-		sendButton.setImageResource(R.drawable.social_send_now);
-		sendButton.setEnabled(false); // Enabled after loading the forum
-		sendButton.setOnClickListener(this);
-		RelativeLayout.LayoutParams right = CommonLayoutParams.relative();
-		right.addRule(ALIGN_PARENT_RIGHT);
-		right.addRule(CENTER_VERTICAL);
-		header.addView(sendButton, right);
-		layout.addView(header);
-
-		content = new EditText(this);
-		content.setId(4);
-		int inputType = TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE
-				| TYPE_TEXT_FLAG_CAP_SENTENCES;
-		content.setInputType(inputType);
-		content.setHint(R.string.forum_post_hint);
-		layout.addView(content);
-
-		setContentView(layout);
-	}
-
-	@Override
-	public void injectActivity(ActivityComponent component) {
-		component.inject(this);
-	}
-
-	@Override
-	public void onResume() {
-		super.onResume();
-		loadAuthorsAndForum();
-	}
-
-	private void loadAuthorsAndForum() {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					Collection<LocalAuthor> localAuthors =
-							identityManager.getLocalAuthors();
-					forum = forumManager.getForum(groupId);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Load took " + duration + " ms");
-					displayAuthorsAndForum(localAuthors);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	private void displayAuthorsAndForum(
-			final Collection<LocalAuthor> localAuthors) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				if (localAuthors.isEmpty()) throw new IllegalStateException();
-				adapter.clear();
-				for (LocalAuthor a : localAuthors)
-					adapter.add(new LocalAuthorItem(a));
-				adapter.sort(LocalAuthorItemComparator.INSTANCE);
-				int count = adapter.getCount();
-				for (int i = 0; i < count; i++) {
-					LocalAuthorItem item = adapter.getItem(i);
-					if (item == LocalAuthorItem.ANONYMOUS) continue;
-					if (item == LocalAuthorItem.NEW) continue;
-					if (item.getLocalAuthor().getId().equals(localAuthorId)) {
-						localAuthor = item.getLocalAuthor();
-						spinner.setSelection(i);
-						break;
-					}
-				}
-				setTitle(forum.getName());
-				sendButton.setEnabled(true);
-			}
-		});
-	}
-
-	@Override
-	public void onSaveInstanceState(Bundle state) {
-		super.onSaveInstanceState(state);
-		if (localAuthorId != null) {
-			byte[] b =  localAuthorId.getBytes();
-			state.putByteArray("briar.LOCAL_AUTHOR_ID", b);
-		}
-	}
-
-	@Override
-	protected void onActivityResult(int request, int result, Intent data) {
-		super.onActivityResult(request, result, data);
-		if (request == REQUEST_CREATE_IDENTITY && result == RESULT_OK) {
-			byte[] b = data.getByteArrayExtra("briar.LOCAL_AUTHOR_ID");
-			if (b == null) throw new IllegalStateException();
-			localAuthorId = new AuthorId(b);
-			loadAuthorsAndForum();
-		}
-	}
-
-	public void onItemSelected(AdapterView<?> parent, View view, int position,
-			long id) {
-		LocalAuthorItem item = adapter.getItem(position);
-		if (item == LocalAuthorItem.ANONYMOUS) {
-			localAuthor = null;
-			localAuthorId = null;
-		} else if (item == LocalAuthorItem.NEW) {
-			localAuthor = null;
-			localAuthorId = null;
-			Intent i = new Intent(this, CreateIdentityActivity.class);
-			startActivityForResult(i, REQUEST_CREATE_IDENTITY);
-		} else {
-			localAuthor = item.getLocalAuthor();
-			localAuthorId = localAuthor.getId();
-		}
-	}
-
-	public void onNothingSelected(AdapterView<?> parent) {
-		localAuthor = null;
-		localAuthorId = null;
-	}
-
-	public void onClick(View view) {
-		if (forum == null) throw new IllegalStateException();
-		String body = content.getText().toString();
-		if (body.equals("")) return;
-		createPost(StringUtils.toUtf8(body));
-		Toast.makeText(this, R.string.post_sent_toast, LENGTH_LONG).show();
-		finish();
-	}
-
-	private void createPost(final byte[] body) {
-		cryptoExecutor.execute(new Runnable() {
-			public void run() {
-				// Don't use an earlier timestamp than the newest post
-				long timestamp = System.currentTimeMillis();
-				timestamp = Math.max(timestamp, minTimestamp);
-				ForumPost p;
-				try {
-					if (localAuthor == null) {
-						p = forumPostFactory.createAnonymousPost(groupId,
-								timestamp, parentId, "text/plain", body);
-					} else {
-						KeyParser keyParser = crypto.getSignatureKeyParser();
-						byte[] b = localAuthor.getPrivateKey();
-						PrivateKey authorKey = keyParser.parsePrivateKey(b);
-						p = forumPostFactory.createPseudonymousPost(groupId,
-								timestamp, parentId, localAuthor, "text/plain",
-								body, authorKey);
-					}
-				} catch (GeneralSecurityException e) {
-					throw new RuntimeException(e);
-				} catch (FormatException e) {
-					throw new RuntimeException(e);
-				}
-				storePost(p);
-			}
-		});
-	}
-
-	private void storePost(final ForumPost p) {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					forumManager.addLocalPost(p);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Storing message took " + duration + " ms");
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
index 93eeb669c177492cbb203c61c3c0448e7fbf0907..e399ecda03ff693693a86c8cfa30b2c6e11bf3dc 100644
--- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
+++ b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
@@ -1,6 +1,7 @@
 package org.briarproject.android.util;
 
 import android.content.Context;
+import android.content.res.TypedArray;
 import android.os.Build;
 import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
@@ -18,6 +19,7 @@ public class BriarRecyclerView extends FrameLayout {
 	private TextView emptyView;
 	private ProgressBar progressBar;
 	private RecyclerView.AdapterDataObserver emptyObserver;
+	private boolean isScrollingToEnd = false;
 
 	public BriarRecyclerView(Context context) {
 		super(context);
@@ -25,6 +27,11 @@ public class BriarRecyclerView extends FrameLayout {
 
 	public BriarRecyclerView(Context context, AttributeSet attrs) {
 		super(context, attrs);
+
+		TypedArray attributes = context.obtainStyledAttributes(attrs,
+				R.styleable.BriarRecyclerView);
+		isScrollingToEnd = attributes
+				.getBoolean(R.styleable.BriarRecyclerView_scrollToEnd, true);
 	}
 
 	public BriarRecyclerView(Context context, AttributeSet attrs,
@@ -44,7 +51,7 @@ public class BriarRecyclerView extends FrameLayout {
 		showProgressBar();
 
 		// scroll down when opening keyboard
-		if (Build.VERSION.SDK_INT >= 11) {
+		if (isScrollingToEnd && Build.VERSION.SDK_INT >= 11) {
 			recyclerView.addOnLayoutChangeListener(
 					new View.OnLayoutChangeListener() {
 						@Override
diff --git a/briar-android/src/org/briarproject/android/util/CustomAnimations.java b/briar-android/src/org/briarproject/android/util/CustomAnimations.java
index 07dec13243138a1cec3ea150b6d84e4e85f66868..6705194a4f4ee42233f1d4097df0dbcb64e79fbd 100644
--- a/briar-android/src/org/briarproject/android/util/CustomAnimations.java
+++ b/briar-android/src/org/briarproject/android/util/CustomAnimations.java
@@ -1,11 +1,16 @@
 package org.briarproject.android.util;
 
 import android.animation.Animator;
+import android.animation.ArgbEvaluator;
 import android.animation.ValueAnimator;
 import android.annotation.SuppressLint;
+import android.graphics.drawable.ColorDrawable;
 import android.os.Build;
+import android.view.View;
 import android.view.ViewGroup;
 
+import org.briarproject.android.controller.handler.ResultHandler;
+
 import static android.view.View.GONE;
 import static android.view.View.MeasureSpec.UNSPECIFIED;
 import static android.view.View.VISIBLE;
@@ -21,6 +26,49 @@ public class CustomAnimations {
 		}
 	}
 
+	@SuppressLint("NewApi")
+	public static void animateColorTransition(final View view, int color,
+			int duration, final ResultHandler<Void> finishedCallback) {
+		// No soup for Gingerbread
+		if (Build.VERSION.SDK_INT < 11) {
+			return;
+		}
+		ValueAnimator anim = new ValueAnimator();
+		ColorDrawable viewColor = (ColorDrawable) view.getBackground();
+		anim.setIntValues(viewColor.getColor(), color);
+		anim.setEvaluator(new ArgbEvaluator());
+		anim.addListener(new Animator.AnimatorListener() {
+			@Override
+			public void onAnimationStart(Animator animation) {
+
+			}
+
+			@Override
+			public void onAnimationEnd(Animator animation) {
+				if (finishedCallback != null) finishedCallback.onResult(null);
+			}
+
+			@Override
+			public void onAnimationCancel(Animator animation) {
+
+			}
+
+			@Override
+			public void onAnimationRepeat(Animator animation) {
+
+			}
+		});
+		anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+			@Override
+			public void onAnimationUpdate(ValueAnimator valueAnimator) {
+				view.setBackgroundColor((Integer)valueAnimator.getAnimatedValue());
+			}
+		});
+		anim.setDuration(duration);
+
+		anim.start();
+	}
+
 	private static void animateHeightGingerbread(ViewGroup viewGroup,
 			boolean isExtending) {
 		// No animations for Gingerbread
diff --git a/briar-android/test/java/briarproject/activity/ForumActivityTest.java b/briar-android/test/java/briarproject/activity/ForumActivityTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ee044b112a25337de5cf1d9416264a688facdc55
--- /dev/null
+++ b/briar-android/test/java/briarproject/activity/ForumActivityTest.java
@@ -0,0 +1,125 @@
+package briarproject.activity;
+
+import android.content.Intent;
+
+import junit.framework.Assert;
+
+import org.briarproject.BuildConfig;
+import org.briarproject.TestUtils;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.forum.ForumActivity;
+import org.briarproject.android.forum.ForumController;
+import org.briarproject.android.forum.ForumEntry;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = BuildConfig.class, sdk = 21,
+		application = TestBriarApplication.class)
+public class ForumActivityTest {
+
+	private final static String AUTHOR_1 = "Author 1";
+	private final static String AUTHOR_2 = "Author 2";
+	private final static String AUTHOR_3 = "Author 3";
+	private final static String AUTHOR_4 = "Author 4";
+	private final static String AUTHOR_5 = "Author 5";
+	private final static String AUTHOR_6 = "Author 6";
+
+	private final static String[] AUTHORS = {
+			AUTHOR_1, AUTHOR_2, AUTHOR_3, AUTHOR_4, AUTHOR_5, AUTHOR_6
+	};
+
+	/*
+		1
+		-> 2
+		   -> 3
+			  -> 4
+		   5
+		6
+	 */
+	private final static int[] LEVELS = {
+			0, 1, 2, 3, 1, 0
+	};
+
+	private TestForumActivity forumActivity;
+	@Captor
+	private ArgumentCaptor<UiResultHandler<Boolean>> rc;
+
+	@Before
+	public void setUp() {
+		MockitoAnnotations.initMocks(this);
+		Intent intent = new Intent();
+		intent.putExtra("briar.GROUP_ID", TestUtils.getRandomId());
+		forumActivity = Robolectric.buildActivity(TestForumActivity.class)
+				.withIntent(intent).create().resume().get();
+	}
+
+
+	private List<ForumEntry> getDummyData() {
+		ForumEntry[] forumEntries = new ForumEntry[6];
+		for (int i = 0; i < forumEntries.length; i++) {
+			forumEntries[i] =
+					new ForumEntry(new MessageId(TestUtils.getRandomId()),
+							AUTHORS[i], LEVELS[i], System.currentTimeMillis(),
+							AUTHORS[i], new AuthorId(TestUtils.getRandomId()));
+		}
+		return new ArrayList<ForumEntry>(Arrays.asList(forumEntries));
+	}
+
+	@Test
+	public void testNestedEntries() {
+		ForumController mc = forumActivity.getController();
+		List<ForumEntry> dummyData = getDummyData();
+		Mockito.when(mc.getForumEntries()).thenReturn(dummyData);
+		// Verify that the forum load is called once
+		verify(mc, times(1))
+				.loadForum(Mockito.any(GroupId.class), rc.capture());
+		rc.getValue().onResult(true);
+		verify(mc, times(1)).getForumEntries();
+		ForumActivity.ForumAdapter adapter = forumActivity.getAdapter();
+		Assert.assertNotNull(adapter);
+		// Cascade close
+		assertEquals(6, adapter.getItemCount());
+		adapter.hideDescendants(dummyData.get(2));
+		assertEquals(5, adapter.getItemCount());
+		adapter.hideDescendants(dummyData.get(1));
+		assertEquals(4, adapter.getItemCount());
+		adapter.hideDescendants(dummyData.get(0));
+		assertEquals(2, adapter.getItemCount());
+		assertTrue(dummyData.get(0).getText()
+				.equals(adapter.getVisibleEntry(0).getText()));
+		assertTrue(dummyData.get(5).getText()
+				.equals(adapter.getVisibleEntry(1).getText()));
+		// Cascade re-open
+		adapter.showDescendants(dummyData.get(0));
+		assertEquals(4, adapter.getItemCount());
+		adapter.showDescendants(dummyData.get(1));
+		assertEquals(5, adapter.getItemCount());
+		adapter.showDescendants(dummyData.get(2));
+		assertEquals(6, adapter.getItemCount());
+		assertTrue(dummyData.get(2).getText()
+				.equals(adapter.getVisibleEntry(2).getText()));
+		assertTrue(dummyData.get(4).getText()
+				.equals(adapter.getVisibleEntry(4).getText()));
+	}
+}
diff --git a/briar-android/test/java/briarproject/activity/TestForumActivity.java b/briar-android/test/java/briarproject/activity/TestForumActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..f73d83e18f66d12e8b549b9ba5584210c90a76b2
--- /dev/null
+++ b/briar-android/test/java/briarproject/activity/TestForumActivity.java
@@ -0,0 +1,42 @@
+package briarproject.activity;
+
+import org.briarproject.android.ActivityModule;
+import org.briarproject.android.controller.BriarController;
+import org.briarproject.android.controller.BriarControllerImpl;
+import org.briarproject.android.forum.ForumActivity;
+import org.briarproject.android.forum.ForumController;
+import org.briarproject.android.forum.ForumControllerImpl;
+import org.mockito.Mockito;
+
+/**
+ * This class exposes the SetupController and offers the possibility to
+ * override it.
+ */
+public class TestForumActivity extends ForumActivity {
+
+	public ForumController getController() {
+		return forumController;
+	}
+
+	public ForumAdapter getAdapter() {
+		return forumAdapter;
+	}
+
+	protected ActivityModule getActivityModule() {
+		return new ActivityModule(this) {
+			@Override
+			protected BriarController provideBriarController(
+					BriarControllerImpl briarControllerImpl) {
+				BriarController c = Mockito.mock(BriarController.class);
+				Mockito.when(c.hasEncryptionKey()).thenReturn(true);
+				return c;
+			}
+
+			@Override
+			protected ForumController provideForumController(
+					ForumControllerImpl forumController) {
+				return Mockito.mock(ForumController.class);
+			}
+		};
+	}
+}
diff --git a/briar-core/src/org/briarproject/clients/MessageTreeImpl.java b/briar-core/src/org/briarproject/clients/MessageTreeImpl.java
index 55f69cf54008adbcb57e57788a24afe25f0284d7..4759f39fdb9ec291ef08f9912b6048caba7067c6 100644
--- a/briar-core/src/org/briarproject/clients/MessageTreeImpl.java
+++ b/briar-core/src/org/briarproject/clients/MessageTreeImpl.java
@@ -11,8 +11,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import javax.inject.Inject;
-
 public class MessageTreeImpl<T extends MessageTree.MessageNode>
 		implements MessageTree<T> {
 
@@ -26,11 +24,6 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
 		}
 	};
 
-	@Inject
-	public MessageTreeImpl() {
-
-	}
-
 	@Override
 	public void clear() {
 		roots.clear();
diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java
index 04d934a21ccccaabdae511448eee6d6fa55d5d4d..e7494850e2f95c88aa851ec6abc60d8d4ad770f5 100644
--- a/briar-core/src/org/briarproject/forum/ForumModule.java
+++ b/briar-core/src/org/briarproject/forum/ForumModule.java
@@ -2,7 +2,6 @@ package org.briarproject.forum;
 
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.MessageQueueManager;
-import org.briarproject.api.clients.MessageTree;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.MetadataEncoder;
@@ -10,14 +9,12 @@ import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.forum.ForumFactory;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPostFactory;
-import org.briarproject.api.forum.ForumPostHeader;
 import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.lifecycle.LifecycleManager;
 import org.briarproject.api.sync.GroupFactory;
 import org.briarproject.api.sync.ValidationManager;
 import org.briarproject.api.system.Clock;
-import org.briarproject.clients.MessageTreeImpl;
 
 import java.security.SecureRandom;
 
@@ -104,10 +101,4 @@ public class ForumModule {
 		return forumSharingManager;
 	}
 
-	@Provides
-	@Singleton
-	MessageTree<ForumPostHeader> provideForumMessageTree(
-			MessageTreeImpl<ForumPostHeader> messageTree) {
-		return messageTree;
-	}
 }