diff --git a/briar-android/res/drawable-hdpi/message_delivered.png b/briar-android/res/drawable-hdpi/message_delivered.png
deleted file mode 100644
index efade63916a2f36a74400bf378d587a763b690a6..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-hdpi/message_delivered.png and /dev/null differ
diff --git a/briar-android/res/drawable-hdpi/message_sent.png b/briar-android/res/drawable-hdpi/message_sent.png
deleted file mode 100644
index 6edef05a95505d6e4605621b5975f179b331c04e..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-hdpi/message_sent.png and /dev/null differ
diff --git a/briar-android/res/drawable-hdpi/message_stored.png b/briar-android/res/drawable-hdpi/message_stored.png
deleted file mode 100644
index 8bf1ee459425236b46f96cc10c1912fec981d739..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-hdpi/message_stored.png and /dev/null differ
diff --git a/briar-android/res/drawable-hdpi/msg_in.9.png b/briar-android/res/drawable-hdpi/msg_in.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..974d60e2d7aeec5229de2b26495af671ddb579ba
Binary files /dev/null and b/briar-android/res/drawable-hdpi/msg_in.9.png differ
diff --git a/briar-android/res/drawable-hdpi/msg_in_unread.9.png b/briar-android/res/drawable-hdpi/msg_in_unread.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..c22cc86322f710ab37c0f56c192f9721271f58d9
Binary files /dev/null and b/briar-android/res/drawable-hdpi/msg_in_unread.9.png differ
diff --git a/briar-android/res/drawable-hdpi/msg_out.9.png b/briar-android/res/drawable-hdpi/msg_out.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..08fd35b4cbdba08150bb224a4981df94431749e7
Binary files /dev/null and b/briar-android/res/drawable-hdpi/msg_out.9.png differ
diff --git a/briar-android/res/drawable-hdpi/social_send_now.png b/briar-android/res/drawable-hdpi/social_send_now.png
deleted file mode 100644
index d6ebbaedd28c504ab516d2cc79641d66dc9e04f6..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-hdpi/social_send_now.png and /dev/null differ
diff --git a/briar-android/res/drawable-mdpi/message_delivered.png b/briar-android/res/drawable-mdpi/message_delivered.png
deleted file mode 100644
index 938c50d909e9dfbf0e931632a4a6282f6fe922b4..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-mdpi/message_delivered.png and /dev/null differ
diff --git a/briar-android/res/drawable-mdpi/message_sent.png b/briar-android/res/drawable-mdpi/message_sent.png
deleted file mode 100644
index 1f3807209e1db00dc0b3f07fa5f2fd0e2bd813ad..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-mdpi/message_sent.png and /dev/null differ
diff --git a/briar-android/res/drawable-mdpi/message_stored.png b/briar-android/res/drawable-mdpi/message_stored.png
deleted file mode 100644
index 01858ffc64ddf763b244d301acaa33657a5ea3d5..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-mdpi/message_stored.png and /dev/null differ
diff --git a/briar-android/res/drawable-mdpi/msg_in.9.png b/briar-android/res/drawable-mdpi/msg_in.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..f9a0267b6e8cc706a1350b1d0fccf6f4a0887b16
Binary files /dev/null and b/briar-android/res/drawable-mdpi/msg_in.9.png differ
diff --git a/briar-android/res/drawable-mdpi/msg_in_unread.9.png b/briar-android/res/drawable-mdpi/msg_in_unread.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e5418856d9fb9ed59be30ccbb013e277ed6345e
Binary files /dev/null and b/briar-android/res/drawable-mdpi/msg_in_unread.9.png differ
diff --git a/briar-android/res/drawable-mdpi/msg_out.9.png b/briar-android/res/drawable-mdpi/msg_out.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..f22c541f7f9087c833dd863c6cf07faab5c1d58e
Binary files /dev/null and b/briar-android/res/drawable-mdpi/msg_out.9.png differ
diff --git a/briar-android/res/drawable-mdpi/social_send_now.png b/briar-android/res/drawable-mdpi/social_send_now.png
deleted file mode 100644
index 90b17a0170b63efad3051c1d15807a77e17f90d4..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-mdpi/social_send_now.png and /dev/null differ
diff --git a/briar-android/res/drawable-xhdpi/message_delivered.png b/briar-android/res/drawable-xhdpi/message_delivered.png
deleted file mode 100644
index 99109113666357edda634c9bbcb9ebd2048e68a8..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-xhdpi/message_delivered.png and /dev/null differ
diff --git a/briar-android/res/drawable-xhdpi/message_sent.png b/briar-android/res/drawable-xhdpi/message_sent.png
deleted file mode 100644
index a40d4d94c0fca5e85e08ce21f3ce99f8c74ceeea..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-xhdpi/message_sent.png and /dev/null differ
diff --git a/briar-android/res/drawable-xhdpi/message_stored.png b/briar-android/res/drawable-xhdpi/message_stored.png
deleted file mode 100644
index b8eb1384a23a5819bde40bf9679643dd477604d6..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-xhdpi/message_stored.png and /dev/null differ
diff --git a/briar-android/res/drawable-xhdpi/msg_in.9.png b/briar-android/res/drawable-xhdpi/msg_in.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..f5db8372dda83faf1082e8700e0c2996c6801fa6
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/msg_in.9.png differ
diff --git a/briar-android/res/drawable-xhdpi/msg_in_unread.9.png b/briar-android/res/drawable-xhdpi/msg_in_unread.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..341ec4f7226f0362bf1cd5d5f354f06f5666845a
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/msg_in_unread.9.png differ
diff --git a/briar-android/res/drawable-xhdpi/msg_out.9.png b/briar-android/res/drawable-xhdpi/msg_out.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7c2816f1339f91a2abe9fa4bd1b72b05a5b5f47
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/msg_out.9.png differ
diff --git a/briar-android/res/drawable-xhdpi/social_send_now.png b/briar-android/res/drawable-xhdpi/social_send_now.png
deleted file mode 100644
index 6fa26d165211460883e53df6953fb117fc5ae76f..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-xhdpi/social_send_now.png and /dev/null differ
diff --git a/briar-android/res/drawable-xxhdpi/message_stored.png b/briar-android/res/drawable-xxhdpi/message_stored.png
deleted file mode 100644
index 153650a1f50238963f883cfeb8990d51d9ac57a0..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-xxhdpi/message_stored.png and /dev/null differ
diff --git a/briar-android/res/drawable-xxhdpi/msg_in.9.png b/briar-android/res/drawable-xxhdpi/msg_in.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..3db9979cf1b13128da38558494e6604e730098b6
Binary files /dev/null and b/briar-android/res/drawable-xxhdpi/msg_in.9.png differ
diff --git a/briar-android/res/drawable-xxhdpi/msg_in_unread.9.png b/briar-android/res/drawable-xxhdpi/msg_in_unread.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a3bb3e7e65841e35b46dbd4c4fc2a7584bf2761
Binary files /dev/null and b/briar-android/res/drawable-xxhdpi/msg_in_unread.9.png differ
diff --git a/briar-android/res/drawable-xxhdpi/msg_out.9.png b/briar-android/res/drawable-xxhdpi/msg_out.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..b7aa02377fd49c6a462c58ed7972719f41ccd978
Binary files /dev/null and b/briar-android/res/drawable-xxhdpi/msg_out.9.png differ
diff --git a/briar-android/res/drawable-xxxhdpi/message_stored.png b/briar-android/res/drawable-xxxhdpi/message_stored.png
deleted file mode 100644
index b3833bc3e76f11b4abe7f1a487bb87025f003075..0000000000000000000000000000000000000000
Binary files a/briar-android/res/drawable-xxxhdpi/message_stored.png and /dev/null differ
diff --git a/briar-android/res/drawable/message_delivered.xml b/briar-android/res/drawable/message_delivered.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7df040f3c06ecd6bed995d92b0a6caa440e518c5
--- /dev/null
+++ b/briar-android/res/drawable/message_delivered.xml
@@ -0,0 +1,5 @@
+<vector android:alpha="0.56" android:height="16dp"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zm4.24,-1.41L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/>
+</vector>
diff --git a/briar-android/res/drawable/message_sent.xml b/briar-android/res/drawable/message_sent.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8a074e0fc85ef1382231f9bc8d30ac90bb237a64
--- /dev/null
+++ b/briar-android/res/drawable/message_sent.xml
@@ -0,0 +1,5 @@
+<vector android:alpha="0.56" android:height="16dp"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>
diff --git a/briar-android/res/drawable/message_stored.xml b/briar-android/res/drawable/message_stored.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b91196f03297bba45c20d43a0549f6c65ffce690
--- /dev/null
+++ b/briar-android/res/drawable/message_stored.xml
@@ -0,0 +1,5 @@
+<vector android:alpha="0.56" android:height="16dp"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillAlpha=".9" android:fillColor="#FF000000" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
+</vector>
diff --git a/briar-android/res/drawable/social_send_now.xml b/briar-android/res/drawable/social_send_now.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bd34e7fc219291f9a17e5c1d0a57f9afc1b5e6cf
--- /dev/null
+++ b/briar-android/res/drawable/social_send_now.xml
@@ -0,0 +1,5 @@
+<vector android:alpha="0.56" android:height="24dp"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
+</vector>
diff --git a/briar-android/res/layout/activity_conversation.xml b/briar-android/res/layout/activity_conversation.xml
index 1a07f07eb738e7d7b31df0edd0130f6eedf816a9..673dd14f9fe1585eb2862fdeea703a48fc5f628d 100644
--- a/briar-android/res/layout/activity_conversation.xml
+++ b/briar-android/res/layout/activity_conversation.xml
@@ -1,12 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
 	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
 	android:id="@+id/layout"
 	android:orientation="vertical"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">
 
-	<!-- ListView will get inserted here -->
+	<android.support.v7.widget.RecyclerView
+		android:id="@+id/conversationView"
+		android:layout_width="match_parent"
+		android:layout_height="0dp"
+		android:layout_weight="1"
+		android:scrollbars="vertical"/>
 
 	<ProgressBar
 		android:id="@+id/listLoadingProgressBar"
@@ -15,7 +21,8 @@
 		android:layout_gravity="center"
 		android:gravity="center"
 		android:layout_weight="1"
-		android:indeterminate="true"/>
+		android:indeterminate="true"
+		android:visibility="gone"/>
 
 	<TextView
 		android:id="@+id/emptyView"
@@ -26,7 +33,8 @@
 		android:gravity="center"
 		android:padding="@dimen/margin_large"
 		android:textSize="@dimen/text_size_large"
-		android:text="@string/no_private_messages"/>
+		android:text="@string/no_private_messages"
+		android:visibility="gone"/>
 
 	<View
 		android:layout_width="match_parent"
@@ -39,9 +47,7 @@
 		android:layout_height="wrap_content"
 		android:background="@color/button_bar_background"
 		android:paddingLeft="@dimen/margin_medium"
-		android:paddingStart="@dimen/margin_medium"
-		android:paddingRight="@dimen/margin_medium"
-		android:paddingEnd="@dimen/margin_medium">
+		android:paddingStart="@dimen/margin_medium">
 
 		<EditText
 			android:id="@+id/contentView"
@@ -53,12 +59,16 @@
 
 		<ImageButton
 			android:id="@+id/sendButton"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
+			android:layout_width="38dp"
+			android:layout_height="38dp"
+			android:layout_gravity="bottom"
 			android:src="@drawable/social_send_now"
-			android:background="@color/button_bar_background"
+			android:background="?attr/selectableItemBackground"
+			android:scaleType="fitEnd"
 			android:contentDescription="@string/send"
-			android:layout_gravity="center"/>
+			android:paddingRight="@dimen/margin_medium"
+			android:paddingEnd="@dimen/margin_medium"
+			android:paddingBottom="@dimen/margin_medium"/>
 	</LinearLayout>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_msg_in.xml b/briar-android/res/layout/list_item_msg_in.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d93d269e7796bbbc767bc020dd647888b9b1927c
--- /dev/null
+++ b/briar-android/res/layout/list_item_msg_in.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	android:orientation="vertical"
+	android:paddingRight="@dimen/margin_medium"
+	android:paddingEnd="@dimen/margin_medium"
+	android:paddingTop="@dimen/margin_small"
+	android:paddingBottom="@dimen/margin_small">
+
+	<RelativeLayout
+		android:id="@+id/msgLayout"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="left|start"
+		android:background="@drawable/msg_in"
+		android:paddingLeft="17dp"
+		android:paddingTop="5dp"
+		android:paddingRight="7dp"
+		android:paddingBottom="5dp">
+
+		<TextView
+			android:id="@+id/msgBody"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:minWidth="80dp"
+			android:textIsSelectable="true"
+			tools:text="Short message"/>
+
+		<TextView
+			android:id="@+id/msgTime"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:textSize="10sp"
+			android:textColor="@color/private_message_date"
+			android:layout_below="@+id/msgBody"
+			tools:text="Dec 24, 13:37"/>
+
+	</RelativeLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_msg_out.xml b/briar-android/res/layout/list_item_msg_out.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a82a07f1e058be89c27bbaa346025775ac888005
--- /dev/null
+++ b/briar-android/res/layout/list_item_msg_out.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	android:orientation="vertical"
+	android:paddingLeft="@dimen/margin_medium"
+	android:paddingStart="@dimen/margin_medium"
+	android:paddingTop="@dimen/margin_small"
+	android:paddingBottom="@dimen/margin_small">
+
+	<RelativeLayout
+		android:id="@+id/msgLayout"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="right|end"
+		android:background="@drawable/msg_out"
+		android:paddingLeft="7dp"
+		android:paddingTop="5dp"
+		android:paddingRight="17dp"
+		android:paddingBottom="5dp">
+
+		<TextView
+			android:id="@+id/msgBody"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:textIsSelectable="true"
+			android:minWidth="80dp"
+			tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
+
+		<TextView
+			android:id="@+id/msgTime"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_below="@+id/msgBody"
+			android:layout_toLeftOf="@+id/msgStatus"
+			android:textSize="10sp"
+			android:textColor="@color/private_message_date"
+			android:singleLine="true"
+			tools:text="Dec 24, 13:37"/>
+
+		<ImageView
+			android:id="@+id/msgStatus"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignBottom="@+id/msgTime"
+			android:layout_alignRight="@+id/msgBody"
+			android:layout_alignEnd="@+id/msgBody"
+			android:layout_marginLeft="3dp"
+			tools:src="@drawable/message_delivered"/>
+
+	</RelativeLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml
index 01b15430a4971d8de351a03ae47085fb9ee1c7d9..0ecc0f1ba7ad2565ad31c956d2d8673066342b9a 100644
--- a/briar-android/res/values/color.xml
+++ b/briar-android/res/values/color.xml
@@ -5,7 +5,6 @@
     <color name="action_bar_background">#2D3E50</color>
     <color name="button_bar_background">#FFFFFF</color>
     <color name="dashboard_background">#FFFFFF</color>
-    <color name="private_message_background">#FFFFFF</color>
 	<color name="private_message_date">#AAAAAA</color>
     <color name="unread_background">#FFFFFF</color>
 	<color name="horizontal_border">#CCCCCC</color>
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
index 98dcd179f828d0e5a40e8681fda3916076be9d17..f00647318a05c7b4252ef4c90ac6278f5a5db27b 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
@@ -2,30 +2,27 @@ package org.briarproject.android.contact;
 
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.content.res.Resources;
 import android.graphics.PorterDuff;
-import android.graphics.drawable.ColorDrawable;
+import android.os.Build;
 import android.os.Bundle;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
 import android.widget.EditText;
 import android.widget.ImageButton;
-import android.widget.ListView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.Toast;
 
 import org.briarproject.R;
 import org.briarproject.android.BriarActivity;
-import org.briarproject.android.util.LayoutUtils;
 import org.briarproject.api.android.AndroidNotificationManager;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -77,12 +74,11 @@ 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.contact.ReadPrivateMessageActivity.RESULT_PREV_NEXT;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
 import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED;
 import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT;
 
 public class ConversationActivity extends BriarActivity
-implements EventListener, OnClickListener, OnItemClickListener {
+		implements EventListener, OnClickListener {
 
 	private static final int REQUEST_READ = 2;
 	private static final Logger LOG =
@@ -95,7 +91,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
 	private TextView empty = null;
 	private ProgressBar loading = null;
 	private ConversationAdapter adapter = null;
-	private ListView list = null;
+	private RecyclerView list = null;
 	private EditText content = null;
 	private ImageButton sendButton = null;
 
@@ -133,27 +129,29 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		loading.setVisibility(VISIBLE);
 
 		adapter = new ConversationAdapter(this);
-		list = new ListView(this) {
-			@Override
-			public void onSizeChanged(int w, int h, int oldw, int oldh) {
-				// Scroll to the bottom when the keyboard is shown
-				super.onSizeChanged(w, h, oldw, oldh);
-				setSelection(getCount() - 1);
-			}
-		};
-		list.setLayoutParams(MATCH_WRAP_1);
-		int pad = LayoutUtils.getPadding(this);
-		list.setPadding(0, pad, 0, pad);
-		list.setClipToPadding(false);
-		// Make the dividers the same colour as the background
-		Resources res = getResources();
-		int background = res.getColor(android.R.color.transparent);
-		list.setDivider(new ColorDrawable(background));
-		list.setDividerHeight(pad);
+		list = (RecyclerView) findViewById(R.id.conversationView);
+		list.setLayoutManager(new LinearLayoutManager(this));
 		list.setAdapter(adapter);
-		list.setOnItemClickListener(this);
-		list.setEmptyView(loading);
-		layout.addView(list, 0);
+		list.setVisibility(GONE);
+		// scroll down when opening keyboard
+		if (Build.VERSION.SDK_INT >= 11) {
+			list.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+				@Override
+				public void onLayoutChange(View v,
+						int left, int top, int right, int bottom,
+						int oldLeft, int oldTop, int oldRight, int oldBottom) {
+					if (bottom < oldBottom) {
+						list.postDelayed(new Runnable() {
+							@Override
+							public void run() {
+								list.scrollToPosition(
+										adapter.getItemCount() - 1);
+							}
+						}, 100);
+					}
+				}
+			});
+		}
 
 		content = (EditText) findViewById(R.id.contentView);
 		sendButton = (ImageButton) findViewById(R.id.sendButton);
@@ -267,12 +265,10 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				loading.setVisibility(GONE);
-				empty.setVisibility(VISIBLE);
-				list.setEmptyView(empty);
-				displayContactDetails();
 				sendButton.setEnabled(true);
-				adapter.clear();
 				if (!headers.isEmpty()) {
+					list.setVisibility(VISIBLE);
+					empty.setVisibility(GONE);
 					for (PrivateMessageHeader h : headers) {
 						ConversationItem item = new ConversationItem(h);
 						byte[] body = bodyCache.get(h.getId());
@@ -280,11 +276,12 @@ implements EventListener, OnClickListener, OnItemClickListener {
 						else item.setBody(body);
 						adapter.add(item);
 					}
-					adapter.sort(ConversationItemComparator.INSTANCE);
 					// Scroll to the bottom
-					list.setSelection(adapter.getCount() - 1);
+					list.scrollToPosition(adapter.getItemCount() - 1);
+				} else {
+					empty.setVisibility(VISIBLE);
+					list.setVisibility(GONE);
 				}
-				adapter.notifyDataSetChanged();
 			}
 		});
 	}
@@ -313,14 +310,18 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				bodyCache.put(m, body);
-				int count = adapter.getCount();
+				int count = adapter.getItemCount();
+
 				for (int i = 0; i < count; i++) {
 					ConversationItem item = adapter.getItem(i);
+
 					if (item.getHeader().getId().equals(m)) {
 						item.setBody(body);
-						adapter.notifyDataSetChanged();
+						adapter.notifyItemChanged(i);
+
 						// Scroll to the bottom
-						list.setSelection(count - 1);
+						list.scrollToPosition(count - 1);
+
 						return;
 					}
 				}
@@ -333,7 +334,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		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())
+			if (position >= 0 && position < adapter.getItemCount())
 				displayMessage(position);
 		}
 	}
@@ -348,7 +349,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
 	private void markMessagesRead() {
 		notificationManager.clearPrivateMessageNotification(contactId);
 		List<MessageId> unread = new ArrayList<MessageId>();
-		int count = adapter.getCount();
+		int count = adapter.getItemCount();
 		for (int i = 0; i < count; i++) {
 			PrivateMessageHeader h = adapter.getItem(i).getHeader();
 			if (!h.isRead()) unread.add(h.getId());
@@ -388,6 +389,8 @@ implements EventListener, OnClickListener, OnItemClickListener {
 			GroupId g = ((MessageAddedEvent) e).getGroupId();
 			if (g.equals(groupId)) {
 				LOG.info("Message added, reloading");
+				// TODO: find a way of not needing to reload the entire
+				// conversation just because one message was added
 				loadHeaders();
 			}
 		} else if (e instanceof MessagesSentEvent) {
@@ -424,16 +427,14 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				Set<MessageId> messages = new HashSet<MessageId>(messageIds);
-				boolean changed = false;
-				int count = adapter.getCount();
+				int count = adapter.getItemCount();
 				for (int i = 0; i < count; i++) {
 					ConversationItem item = adapter.getItem(i);
 					if (messages.contains(item.getHeader().getId())) {
 						item.setStatus(status);
-						changed = true;
+						adapter.notifyItemChanged(i);
 					}
 				}
-				if (changed) adapter.notifyDataSetChanged();
 			}
 		});
 	}
@@ -451,7 +452,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
 	private long getMinTimestampForNewMessage() {
 		// Don't use an earlier timestamp than the newest message
 		long timestamp = 0;
-		int count = adapter.getCount();
+		int count = adapter.getItemCount();
 		for (int i = 0; i < count; i++) {
 			long t = adapter.getItem(i).getHeader().getTimestamp();
 			if (t > timestamp) timestamp = t;
@@ -492,11 +493,6 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		});
 	}
 
-	public void onItemClick(AdapterView<?> parent, View view, int position,
-			long id) {
-		displayMessage(position);
-	}
-
 	private void displayMessage(int position) {
 		ConversationItem item = adapter.getItem(position);
 		PrivateMessageHeader header = item.getHeader();
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
index a95b00e47574f78f119492ca232e871efdfcd001..0e183a63b571f706e403cf2b81d6ae9cc9c9f158 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
@@ -1,115 +1,199 @@
 package org.briarproject.android.contact;
 
 import android.content.Context;
-import android.content.res.Resources;
+import android.support.v7.util.SortedList;
+import android.support.v7.widget.RecyclerView;
 import android.text.format.DateUtils;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.ImageButton;
 import android.widget.ImageView;
-import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import org.briarproject.R;
-import org.briarproject.android.util.ElasticHorizontalSpace;
-import org.briarproject.android.util.LayoutUtils;
 import org.briarproject.api.messaging.PrivateMessageHeader;
 import org.briarproject.util.StringUtils;
 
-import java.util.ArrayList;
-
-import static android.view.Gravity.BOTTOM;
-import static android.view.Gravity.LEFT;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.LinearLayout.VERTICAL;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
 import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED;
 import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT;
 
-class ConversationAdapter extends ArrayAdapter<ConversationItem> {
+class ConversationAdapter extends
+		RecyclerView.Adapter<ConversationAdapter.MessageHolder> {
+
+	private static final int MSG_OUT = 0;
+	private static final int MSG_IN = 1;
+	private static final int MSG_IN_UNREAD = 2;
+
+	private SortedList<ConversationItem> messages =
+			new SortedList<ConversationItem>(ConversationItem.class,
+					new SortedList.Callback<ConversationItem>() {
+						@Override
+						public void onInserted(int position, int count) {
+							notifyItemRangeInserted(position, count);
+						}
+
+						@Override
+						public void onChanged(int position, int count) {
+							notifyItemRangeChanged(position, count);
+						}
+
+						@Override
+						public void onMoved(int fromPosition, int toPosition) {
+							notifyItemMoved(fromPosition, toPosition);
+						}
+
+						@Override
+						public void onRemoved(int position, int count) {
+							notifyItemRangeRemoved(position, count);
+						}
+
+						@Override
+						public int compare(ConversationItem c1,
+								ConversationItem c2) {
+							long time1 = c1.getHeader().getTimestamp();
+							long time2 = c2.getHeader().getTimestamp();
+							if (time1 < time2) return -1;
+							if (time1 > time2) return 1;
+							return 0;
+						}
+
+						@Override
+						public boolean areItemsTheSame(ConversationItem c1,
+								ConversationItem c2) {
+							return c1.getHeader().getId()
+									.equals(c2.getHeader().getId());
+						}
+
+						@Override
+						public boolean areContentsTheSame(ConversationItem c1,
+								ConversationItem c2) {
+							return c1.equals(c2);
+						}
+					});
+	private Context ctx;
+
+	public ConversationAdapter(Context context) {
+		ctx = context;
+	}
 
-	private final int pad;
+	@Override
+	public int getItemViewType(int position) {
+		// return different type for incoming and outgoing (local) messages
+		PrivateMessageHeader header = getItem(position).getHeader();
+		if (header.isLocal()) {
+			return MSG_OUT;
+		} else if (header.isRead()) {
+			return MSG_IN;
+		} else {
+			return MSG_IN_UNREAD;
+		}
+	}
+
+	@Override
+	public MessageHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
+		View v;
 
-	ConversationAdapter(Context ctx) {
-		super(ctx, android.R.layout.simple_expandable_list_item_1,
-				new ArrayList<ConversationItem>());
-		pad = LayoutUtils.getPadding(ctx);
+		// outgoing message (local)
+		if (type == MSG_OUT) {
+			v = LayoutInflater.from(viewGroup.getContext())
+					.inflate(R.layout.list_item_msg_out, viewGroup, false);
+		}
+		// incoming message (non-local)
+		else {
+			v = LayoutInflater.from(viewGroup.getContext())
+					.inflate(R.layout.list_item_msg_in, viewGroup, false);
+		}
+
+		return new MessageHolder(v, type);
 	}
 
 	@Override
-	public View getView(int position, View convertView, ViewGroup parent) {
+	public void onBindViewHolder(final MessageHolder ui, final int position) {
 		ConversationItem item = getItem(position);
 		PrivateMessageHeader header = item.getHeader();
-		Context ctx = getContext();
-		Resources res = ctx.getResources();
-
-		LinearLayout layout = new LinearLayout(ctx);
-		layout.setOrientation(VERTICAL);
-		if (header.isLocal()) layout.setPadding(3 * pad, 0, 0, 0);
-		else layout.setPadding(0, 0, 3 * pad, 0);
 
-		int background = res.getColor(R.color.private_message_background);
+		if (header.isLocal()) {
+			if (item.getStatus() == DELIVERED) {
+				ui.status.setImageResource(R.drawable.message_delivered);
+			} else if (item.getStatus() == SENT) {
+				ui.status.setImageResource(R.drawable.message_sent);
+			} else {
+				ui.status.setImageResource(R.drawable.message_stored);
+			}
+		} else if (!header.isRead()) {
+			int bottom = ui.layout.getPaddingBottom();
+			int top = ui.layout.getPaddingTop();
+			int right = ui.layout.getPaddingRight();
+			int left = ui.layout.getPaddingLeft();
+
+			// show unread messages in different color to not miss them
+			ui.layout.setBackgroundResource(R.drawable.msg_in_unread);
+
+			// re-apply the previous padding due to bug in some Android versions
+			// see: https://code.google.com/p/android/issues/detail?id=17885
+			ui.layout.setPadding(left, top, right, bottom);
+		}
 
-		View content;
 		if (item.getBody() == null) {
-			TextView ellipsis = new TextView(ctx);
-			ellipsis.setText("\u2026");
-			content = ellipsis;
+			ui.body.setText("\u2026");
 		} else if (header.getContentType().equals("text/plain")) {
-			TextView text = new TextView(ctx);
-			text.setText(StringUtils.fromUtf8(item.getBody()));
-			content = text;
+			ui.body.setText(StringUtils.fromUtf8(item.getBody()));
 		} else {
-			ImageButton attachment = new ImageButton(ctx);
-			attachment.setImageResource(R.drawable.content_attachment);
-			content = attachment;
+			// TODO support other content types
 		}
-		content.setLayoutParams(MATCH_WRAP);
-		content.setBackgroundColor(background);
-		content.setPadding(pad, pad, pad, 0);
-		layout.addView(content);
 
-		if (header.isLocal()) {
-			LinearLayout footer = new LinearLayout(ctx);
-			footer.setLayoutParams(MATCH_WRAP);
-			footer.setOrientation(HORIZONTAL);
-			footer.setGravity(BOTTOM);
-			footer.setPadding(pad, 0, pad, pad);
-			footer.setBackgroundColor(background);
+		long timestamp = header.getTimestamp();
+		ui.date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
+	}
 
-			footer.addView(new ElasticHorizontalSpace(ctx));
+	@Override
+	public int getItemCount() {
+		return messages == null ? 0 : messages.size();
+	}
 
-			ImageView status = new ImageView(ctx);
-			status.setPadding(0, 0, pad, 0);
-			if (item.getStatus() == DELIVERED) {
-				status.setImageResource(R.drawable.message_delivered);
-			} else if (item.getStatus() == SENT) {
-				status.setImageResource(R.drawable.message_sent);
-			} else {
-				status.setImageResource(R.drawable.message_stored);
-			}
-			footer.addView(status);
+	public boolean isEmpty() {
+		return messages == null || messages.size() == 0;
+	}
 
-			TextView date = new TextView(ctx);
-			date.setTextColor(res.getColor(R.color.private_message_date));
-			long timestamp = header.getTimestamp();
-			date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
-			footer.addView(date);
+	public ConversationItem getItem(int position) {
+		return messages.get(position);
+	}
 
-			layout.addView(footer);
-		} else {
-			TextView date = new TextView(ctx);
-			date.setLayoutParams(MATCH_WRAP);
-			date.setGravity(LEFT);
-			date.setTextColor(res.getColor(R.color.private_message_date));
-			date.setBackgroundColor(background);
-			date.setPadding(pad, 0, pad, pad);
-			long timestamp = header.getTimestamp();
-			date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
-			layout.addView(date);
+	public void add(final ConversationItem message) {
+		this.messages.add(message);
+	}
+
+	public void remove(final ConversationItem message) {
+		this.messages.remove(message);
+	}
+
+	public void clear() {
+		this.messages.beginBatchedUpdates();
+
+		while(messages.size() != 0) {
+			messages.removeItemAt(0);
 		}
 
-		return layout;
+		this.messages.endBatchedUpdates();
+	}
+
+	public static class MessageHolder extends RecyclerView.ViewHolder {
+		public ViewGroup layout;
+		public TextView body;
+		public TextView date;
+		public ImageView status;
+
+		public MessageHolder(View v, int type) {
+			super(v);
+
+			layout = (ViewGroup) v.findViewById(R.id.msgLayout);
+			body = (TextView) v.findViewById(R.id.msgBody);
+			date = (TextView) v.findViewById(R.id.msgTime);
+
+			// outgoing message (local)
+			if (type == MSG_OUT) {
+				status = (ImageView) v.findViewById(R.id.msgStatus);
+			}
+		}
 	}
 }
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java b/briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java
deleted file mode 100644
index 76eb1d28dd31932cbfdf6e15ce9c235d20b63b43..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.briarproject.android.contact;
-
-import java.util.Comparator;
-
-class ConversationItemComparator implements Comparator<ConversationItem> {
-
-	static final ConversationItemComparator INSTANCE =
-			new ConversationItemComparator();
-
-	public int compare(ConversationItem a, ConversationItem b) {
-		// The oldest message comes first
-		long aTime = a.getHeader().getTimestamp();
-		long bTime = b.getHeader().getTimestamp();
-		if (aTime < bTime) return -1;
-		if (aTime > bTime) return 1;
-		return 0;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/contact/ReadPrivateMessageActivity.java b/briar-android/src/org/briarproject/android/contact/ReadPrivateMessageActivity.java
index 7ece03b027193c23925f04f8dcd79659c315af2a..01ba0030336eb2ae066eca735caa921fe5b447bf 100644
--- a/briar-android/src/org/briarproject/android/contact/ReadPrivateMessageActivity.java
+++ b/briar-android/src/org/briarproject/android/contact/ReadPrivateMessageActivity.java
@@ -40,6 +40,7 @@ import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
 import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
 import static org.briarproject.api.identity.Author.Status.VERIFIED;
 
+@Deprecated
 public class ReadPrivateMessageActivity extends BriarActivity
 implements OnClickListener {