diff --git a/briar-android/res/layout/list_item_msg_in.xml b/briar-android/res/layout/list_item_conversation_msg_in.xml
similarity index 92%
rename from briar-android/res/layout/list_item_msg_in.xml
rename to briar-android/res/layout/list_item_conversation_msg_in.xml
index 3c18ca1a88c15d4e3f2c22a97c109343c68c6f43..ef5c7a953b04987859556f9939050b6e02a5c897 100644
--- a/briar-android/res/layout/list_item_msg_in.xml
+++ b/briar-android/res/layout/list_item_conversation_msg_in.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
-	android:id="@+id/msgLayout"
+	android:id="@+id/layout"
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:tools="http://schemas.android.com/tools"
 	android:layout_width="wrap_content"
@@ -11,7 +11,7 @@
 	android:orientation="vertical">
 
 	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-		android:id="@+id/msgBody"
+		android:id="@+id/text"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:textColor="@color/briar_text_primary"
@@ -20,7 +20,7 @@
 		tools:text="Short message"/>
 
 	<TextView
-		android:id="@+id/msgTime"
+		android:id="@+id/time"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_gravity="right|end"
diff --git a/briar-android/res/layout/list_item_msg_out.xml b/briar-android/res/layout/list_item_conversation_msg_out.xml
similarity index 84%
rename from briar-android/res/layout/list_item_msg_out.xml
rename to briar-android/res/layout/list_item_conversation_msg_out.xml
index 7b7f4f65cee607313a5bee6079cd26806337d3e3..dd9c7b1f8ec0b288ad6bbef262ea6773db1a03eb 100644
--- a/briar-android/res/layout/list_item_msg_out.xml
+++ b/briar-android/res/layout/list_item_conversation_msg_out.xml
@@ -7,7 +7,7 @@
 	android:orientation="vertical">
 
 	<RelativeLayout
-		android:id="@+id/msgLayout"
+		android:id="@+id/layout"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_gravity="right|end"
@@ -16,7 +16,7 @@
 		android:background="@drawable/msg_out">
 
 		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-			android:id="@+id/msgBody"
+			android:id="@+id/text"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
 			android:textColor="@color/briar_text_primary_inverse"
@@ -25,12 +25,12 @@
 			tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
 
 		<TextView
-			android:id="@+id/msgTime"
+			android:id="@+id/time"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
 			android:layout_alignParentLeft="true"
 			android:layout_alignParentStart="true"
-			android:layout_below="@+id/msgBody"
+			android:layout_below="@+id/text"
 			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
 			android:maxLines="1"
 			android:textColor="@color/private_message_date_inverse"
@@ -38,13 +38,13 @@
 			tools:text="Dec 24, 13:37"/>
 
 		<ImageView
-			android:id="@+id/msgStatus"
+			android:id="@+id/status"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:layout_alignBottom="@+id/msgTime"
+			android:layout_alignBottom="@+id/time"
 			android:layout_marginLeft="@dimen/margin_medium"
-			android:layout_toEndOf="@+id/msgTime"
-			android:layout_toRightOf="@+id/msgTime"
+			android:layout_toEndOf="@+id/time"
+			android:layout_toRightOf="@+id/time"
 			tools:ignore="ContentDescription"
 			tools:src="@drawable/message_delivered_white"/>
 
diff --git a/briar-android/res/layout/list_item_shareable_invitation_in.xml b/briar-android/res/layout/list_item_conversation_notice_in.xml
similarity index 71%
rename from briar-android/res/layout/list_item_shareable_invitation_in.xml
rename to briar-android/res/layout/list_item_conversation_notice_in.xml
index b7cc1bb36655ec47bffc0306847ca1f341ec3c5b..073edf537c67a9d86d955d59e7a581dd3544240a 100644
--- a/briar-android/res/layout/list_item_shareable_invitation_in.xml
+++ b/briar-android/res/layout/list_item_conversation_notice_in.xml
@@ -7,7 +7,7 @@
 	android:orientation="vertical">
 
 	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-		android:id="@+id/msgBody"
+		android:id="@+id/msgText"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
 		android:layout_gravity="left|start"
@@ -20,7 +20,7 @@
 		tools:text="Short message"/>
 
 	<RelativeLayout
-		android:id="@+id/noticeLayout"
+		android:id="@+id/layout"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
 		android:layout_marginLeft="@dimen/message_bubble_margin_tail"
@@ -28,7 +28,7 @@
 		android:background="@drawable/notice_in_bottom">
 
 		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-			android:id="@+id/introductionText"
+			android:id="@+id/text"
 			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
 			android:minWidth="80dp"
@@ -39,28 +39,17 @@
 			tools:text="@string/forum_invitation_received"/>
 
 		<TextView
-			android:id="@+id/introductionTime"
+			android:id="@+id/time"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:layout_alignEnd="@+id/introductionText"
-			android:layout_alignRight="@+id/introductionText"
-			android:layout_below="@+id/showInvitationsButton"
+			android:layout_alignEnd="@+id/text"
+			android:layout_alignRight="@+id/text"
+			android:layout_below="@+id/text"
 			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
 			android:textColor="@color/private_message_date"
 			android:textSize="@dimen/text_size_tiny"
 			tools:text="Dec 24, 13:37"/>
 
-		<Button
-			android:id="@+id/showInvitationsButton"
-			style="@style/BriarButtonFlat.Positive"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_alignEnd="@+id/introductionText"
-			android:layout_alignRight="@+id/introductionText"
-			android:layout_below="@+id/introductionText"
-			android:layout_marginBottom="-15dp"
-			tools:text="@string/forum_show_invitations"/>
-
 	</RelativeLayout>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_msg_notice_out.xml b/briar-android/res/layout/list_item_conversation_notice_out.xml
similarity index 84%
rename from briar-android/res/layout/list_item_msg_notice_out.xml
rename to briar-android/res/layout/list_item_conversation_notice_out.xml
index 0d328fe5c02963444f80853a6bbe3d160e5632a8..9ea5bd96772f6ffedf371bc7a70700e6a8ed68b3 100644
--- a/briar-android/res/layout/list_item_msg_notice_out.xml
+++ b/briar-android/res/layout/list_item_conversation_notice_out.xml
@@ -7,7 +7,7 @@
 	android:orientation="vertical">
 
 	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-		android:id="@+id/msgBody"
+		android:id="@+id/msgText"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
 		android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
@@ -19,7 +19,7 @@
 		tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
 
 	<RelativeLayout
-		android:id="@+id/noticeLayout"
+		android:id="@+id/layout"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
 		android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
@@ -27,7 +27,7 @@
 		android:background="@drawable/notice_out_bottom">
 
 		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-			android:id="@+id/introductionText"
+			android:id="@+id/text"
 			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
 			android:textColor="@color/briar_text_secondary"
@@ -37,25 +37,25 @@
 			tools:text="@string/introduction_request_received"/>
 
 		<TextView
-			android:id="@+id/introductionTime"
+			android:id="@+id/time"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
 			android:layout_alignParentLeft="true"
 			android:layout_alignParentStart="true"
-			android:layout_below="@+id/introductionText"
+			android:layout_below="@+id/text"
 			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
 			android:textColor="@color/private_message_date"
 			android:textSize="@dimen/text_size_tiny"
 			tools:text="Dec 24, 13:37"/>
 
 		<ImageView
-			android:id="@+id/introductionStatus"
+			android:id="@+id/status"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:layout_alignBottom="@+id/introductionTime"
+			android:layout_alignBottom="@+id/time"
 			android:layout_marginLeft="@dimen/margin_medium"
-			android:layout_toEndOf="@+id/introductionTime"
-			android:layout_toRightOf="@+id/introductionTime"
+			android:layout_toEndOf="@+id/time"
+			android:layout_toRightOf="@+id/time"
 			tools:ignore="ContentDescription"
 			tools:src="@drawable/message_delivered"/>
 
diff --git a/briar-android/res/layout/list_item_introduction_in.xml b/briar-android/res/layout/list_item_conversation_request.xml
similarity index 84%
rename from briar-android/res/layout/list_item_introduction_in.xml
rename to briar-android/res/layout/list_item_conversation_request.xml
index 9094a1fc3c53203cd07fbf153735c4cbb33c608d..3932e82e535394583f6956d405f60281d95df874 100644
--- a/briar-android/res/layout/list_item_introduction_in.xml
+++ b/briar-android/res/layout/list_item_conversation_request.xml
@@ -7,7 +7,7 @@
 	android:orientation="vertical">
 
 	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-		android:id="@+id/msgBody"
+		android:id="@+id/msgText"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
 		android:layout_marginLeft="@dimen/message_bubble_margin_tail"
@@ -19,7 +19,7 @@
 		tools:text="Short message"/>
 
 	<RelativeLayout
-		android:id="@+id/noticeLayout"
+		android:id="@+id/layout"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
 		android:layout_marginLeft="@dimen/message_bubble_margin_tail"
@@ -27,7 +27,7 @@
 		android:background="@drawable/notice_in_bottom">
 
 		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-			android:id="@+id/introductionText"
+			android:id="@+id/text"
 			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
 			android:minWidth="80dp"
@@ -38,11 +38,11 @@
 			tools:text="@string/introduction_request_received"/>
 
 		<TextView
-			android:id="@+id/introductionTime"
+			android:id="@+id/time"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:layout_alignEnd="@+id/introductionText"
-			android:layout_alignRight="@+id/introductionText"
+			android:layout_alignEnd="@+id/text"
+			android:layout_alignRight="@+id/text"
 			android:layout_below="@+id/declineButton"
 			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
 			android:textColor="@color/private_message_date"
@@ -54,9 +54,9 @@
 			style="@style/BriarButtonFlat.Positive"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:layout_alignEnd="@+id/introductionText"
-			android:layout_alignRight="@+id/introductionText"
-			android:layout_below="@+id/introductionText"
+			android:layout_alignEnd="@+id/text"
+			android:layout_alignRight="@+id/text"
+			android:layout_below="@+id/text"
 			android:text="@string/accept"/>
 
 		<Button
@@ -64,7 +64,7 @@
 			style="@style/BriarButtonFlat.Negative"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:layout_below="@+id/introductionText"
+			android:layout_below="@+id/text"
 			android:layout_marginBottom="-15dp"
 			android:layout_toLeftOf="@+id/acceptButton"
 			android:layout_toStartOf="@+id/acceptButton"
diff --git a/briar-android/res/layout/list_item_notice_in.xml b/briar-android/res/layout/list_item_notice_in.xml
deleted file mode 100644
index 13994b47422388050c8f289ea309b5982cee43b8..0000000000000000000000000000000000000000
--- a/briar-android/res/layout/list_item_notice_in.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?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="wrap_content"
-	android:layout_height="wrap_content"
-	android:background="@drawable/notice_in"
-	android:orientation="vertical"
-	android:layout_marginLeft="@dimen/message_bubble_margin_tail"
-	android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
-
-	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-		android:id="@+id/noticeText"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:textIsSelectable="true"
-		android:textSize="@dimen/text_size_medium"
-		android:textStyle="italic"
-		android:textColor="@color/briar_text_secondary"
-		tools:text="@string/introduction_response_accepted_received"/>
-
-	<TextView
-		android:id="@+id/noticeTime"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_gravity="right|end"
-		android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
-		android:maxLines="1"
-		android:textColor="@color/private_message_date"
-		android:textSize="@dimen/text_size_tiny"
-		tools:text="Dec 24, 13:37"/>
-
-</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_notice_out.xml b/briar-android/res/layout/list_item_notice_out.xml
deleted file mode 100644
index ae327bb9959cdeea6e2e338ce17d1e5be5a092e5..0000000000000000000000000000000000000000
--- a/briar-android/res/layout/list_item_notice_out.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?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">
-
-	<RelativeLayout
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_gravity="right|end"
-		android:background="@drawable/notice_out"
-		android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
-		android:layout_marginRight="@dimen/message_bubble_margin_tail">
-
-		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
-			android:id="@+id/noticeText"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:textIsSelectable="true"
-			android:textSize="@dimen/text_size_medium"
-			android:textStyle="italic"
-			android:textColor="@color/briar_text_secondary"
-			tools:text="@string/introduction_response_accepted_sent"/>
-
-		<TextView
-			android:id="@+id/noticeTime"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
-			android:layout_alignParentLeft="true"
-			android:layout_alignParentStart="true"
-			android:layout_below="@+id/noticeText"
-			android:textColor="@color/private_message_date"
-			android:textSize="@dimen/text_size_tiny"
-			tools:text="Dec 24, 13:37"/>
-
-		<ImageView
-			android:id="@+id/noticeStatus"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_alignBottom="@+id/noticeTime"
-			android:layout_marginLeft="@dimen/margin_medium"
-			android:layout_toEndOf="@+id/noticeTime"
-			android:layout_toRightOf="@+id/noticeTime"
-			tools:ignore="ContentDescription"
-			tools:src="@drawable/message_delivered"/>
-
-	</RelativeLayout>
-
-</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 2b04c8b8cf816d3560bd1e6247d65ae71e3232f7..bd35788a27f2bcfc2936ab94eeeac6bdf3450447 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -133,7 +133,6 @@
 	<string name="introduction_request_sent">You have asked to introduce %1$s to %2$s.</string>
 	<string name="introduction_request_received">%1$s has asked to introduce you to %2$s. Do you want to add %2$s to your contact list?</string>
 	<string name="introduction_request_exists_received">%1$s has asked to introduce you to %2$s, but %2$s is already in your contact list. Since %1$s might not know that, you can still respond:</string>
-	<string name="introduction_request_for_our_identity_received">%1$s has asked to introduce you to %2$s, but %2$s is one of your other identities, so you cannot accept the introduction:</string>
 	<string name="introduction_request_answered_received">%1$s has asked to introduce you to %2$s.</string>
 	<string name="introduction_response_accepted_sent">You accepted the introduction to %1$s.</string>
 	<string name="introduction_response_declined_sent">You declined the introduction to %1$s.</string>
@@ -215,7 +214,6 @@
 	<string name="forum_share_error">There was an error sharing this forum.</string>
 	<string name="forum_invitation_received">%1$s has shared the forum \"%2$s\" with you.</string>
 	<string name="forum_invitation_sent">You have shared the forum \"%1$s\" with %2$s.</string>
-	<string name="forum_show_invitations">Show Forum Invitations</string>
 	<string name="forum_invitations_title">Forum Invitations</string>
 	<string name="forum_invitation_exists">You accepted an invitation to this forum already. Accepting more invitations will grow and strengthen the communication in the forum.</string>
 	<string name="forum_joined_toast">Joined Forum</string>
@@ -279,7 +277,6 @@
 	<string name="blogs_sharing_response_declined_received">%s declined the blog invitation.</string>
 	<string name="blogs_sharing_invitation_received">%1$s has shared the personal blog of %2$s with you.</string>
 	<string name="blogs_sharing_invitation_sent">You have shared the personal blog of %1$s with %2$s.</string>
-	<string name="blogs_sharing_show_invitations">Show Blog Invitations</string>
 	<string name="blogs_sharing_invitations_title">Blog Invitations</string>
 	<string name="blogs_sharing_exists">You are subscribed to this blog already. Accepting again can lead to faster blog post delivery.</string>
 	<string name="blogs_sharing_joined_toast">Subscribed to Blog</string>
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
index c7114857c62297168ea4ddc3757996d58b9d44c7..deb7944037c494ab3f7890e4013305247a47c103 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
@@ -276,24 +276,28 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 			IntroductionRequestReceivedEvent m =
 					(IntroductionRequestReceivedEvent) e;
 			IntroductionRequest ir = m.getIntroductionRequest();
-			updateItem(m.getContactId(), ConversationItem.from(ir));
+			updateItem(m.getContactId(),
+					ConversationItem.from(getContext(), "", ir));
 		} else if (e instanceof IntroductionResponseReceivedEvent) {
 			LOG.info("Introduction response received, updating item");
 			IntroductionResponseReceivedEvent m =
 					(IntroductionResponseReceivedEvent) e;
 			IntroductionResponse ir = m.getIntroductionResponse();
-			updateItem(m.getContactId(), ConversationItem.from(ir));
+			updateItem(m.getContactId(),
+					ConversationItem.from(getContext(), "", ir));
 		} else if (e instanceof InvitationRequestReceivedEvent) {
 			LOG.info("Invitation request received, updating item");
 			InvitationRequestReceivedEvent m = (InvitationRequestReceivedEvent) e;
 			InvitationRequest ir = m.getRequest();
-			updateItem(m.getContactId(), ConversationItem.from(ir));
+			updateItem(m.getContactId(),
+					ConversationItem.from(getContext(), "", ir));
 		} else if (e instanceof InvitationResponseReceivedEvent) {
 			LOG.info("Invitation response received, updating item");
 			InvitationResponseReceivedEvent m =
 					(InvitationResponseReceivedEvent) e;
 			InvitationResponse ir = m.getResponse();
-			updateItem(m.getContactId(), ConversationItem.from(ir));
+			updateItem(m.getContactId(),
+					ConversationItem.from(getContext(), "", ir));
 		}
 	}
 
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListItem.java b/briar-android/src/org/briarproject/android/contact/ContactListItem.java
index e90cdf939b14f6a582594bcc36dd36d4b2aa7909..d764266380f5043389798a7ac5abe864abc5e43f 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListItem.java
@@ -6,8 +6,6 @@ import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.GroupId;
 import org.jetbrains.annotations.NotNull;
 
-import static org.briarproject.android.contact.ConversationItem.IncomingItem;
-
 // This class is NOT thread-safe
 public class ContactListItem {
 
@@ -34,8 +32,8 @@ public class ContactListItem {
 		empty = empty && message == null;
 		if (message != null) {
 			if (message.getTime() > timestamp) timestamp = message.getTime();
-			if (message instanceof IncomingItem &&
-					!((IncomingItem) message).isRead())
+			if (message instanceof ConversationInItem &&
+					!((ConversationInItem) message).isRead())
 				unread++;
 		}
 	}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
index a5496d9c709b464113bfc49999552d9427ab8b2f..5eac456b6e33eb6cdb00a2716beafa69d8a237a3 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
@@ -3,6 +3,7 @@ package org.briarproject.android.contact;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.annotation.UiThread;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.ActivityOptionsCompat;
@@ -27,7 +28,8 @@ import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.BriarActivity;
 import org.briarproject.android.api.AndroidNotificationManager;
-import org.briarproject.android.contact.ConversationAdapter.IntroductionHandler;
+import org.briarproject.android.contact.ConversationAdapter.RequestListener;
+import org.briarproject.android.contact.ConversationItem.PartialItem;
 import org.briarproject.android.introduction.IntroductionActivity;
 import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.android.view.TextInputView;
@@ -39,6 +41,7 @@ import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.crypto.CryptoExecutor;
+import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.NoSuchContactException;
 import org.briarproject.api.event.ContactConnectedEvent;
@@ -92,16 +95,15 @@ import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
 import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.OnHidePromptListener;
 
 import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+import static android.support.v7.util.SortedList.INVALID_POSITION;
 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.ConversationItem.IncomingItem;
-import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
 import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE;
 import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
 
 public class ConversationActivity extends BriarActivity
-		implements EventListener, IntroductionHandler, TextInputListener {
+		implements EventListener, RequestListener, TextInputListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(ConversationActivity.class.getName());
@@ -117,7 +119,7 @@ public class ConversationActivity extends BriarActivity
 	@CryptoExecutor
 	Executor cryptoExecutor;
 
-	private final Map<MessageId, byte[]> bodyCache = new ConcurrentHashMap<>();
+	private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>();
 
 	private ConversationAdapter adapter;
 	private Toolbar toolbar;
@@ -325,7 +327,6 @@ public class ConversationActivity extends BriarActivity
 					toolbarStatus
 							.setContentDescription(getString(R.string.offline));
 				}
-				adapter.setContactName(contactName);
 			}
 		});
 	}
@@ -399,33 +400,42 @@ public class ConversationActivity extends BriarActivity
 			Collection<InvitationMessage> invitations) {
 		int size = headers.size() + introductions.size() + invitations.size();
 		List<ConversationItem> items = new ArrayList<>(size);
-		for (PrivateMessageHeader h : headers) {
-			ConversationMessageItem item = ConversationItem.from(h);
-			byte[] body = bodyCache.get(h.getId());
-			if (body == null) loadMessageBody(h.getId());
-			else item.setBody(body);
-			items.add(item);
-		}
-		for (IntroductionMessage im : introductions) {
-			if (im instanceof IntroductionRequest) {
-				IntroductionRequest ir = (IntroductionRequest) im;
-				items.add(ConversationItem.from(ir));
-			} else {
-				IntroductionResponse ir = (IntroductionResponse) im;
-				items.add(ConversationItem.from(ConversationActivity.this,
-								contactName, ir));
+
+			for (PrivateMessageHeader h : headers) {
+				ConversationItem item = ConversationItem.from(h);
+				String body = bodyCache.get(h.getId());
+				if (body == null) loadMessageBody(h.getId());
+				else ((PartialItem) item).setText(body);
+				items.add(item);
 			}
-		}
-		for (InvitationMessage im : invitations) {
-			if (im instanceof InvitationRequest) {
-				InvitationRequest ir = (InvitationRequest) im;
-				items.add(ConversationItem.from(ir));
-			} else if (im instanceof InvitationResponse) {
-				InvitationResponse ir = (InvitationResponse) im;
-				items.add(ConversationItem.from(ConversationActivity.this,
-								contactName, ir));
+			for (IntroductionMessage m : introductions) {
+				ConversationItem item;
+				if (m instanceof IntroductionRequest) {
+					item = ConversationItem
+							.from(ConversationActivity.this,
+									contactName,
+									(IntroductionRequest) m);
+				} else {
+					item = ConversationItem
+							.from(ConversationActivity.this,
+									contactName,
+									(IntroductionResponse) m);
+				}
+				items.add(item);
+			}
+			for (InvitationMessage i : invitations) {
+				if (i instanceof InvitationRequest) {
+					InvitationRequest r = (InvitationRequest) i;
+					items.add(ConversationItem
+							.from(ConversationActivity.this,
+									contactName, r));
+				} else if (i instanceof InvitationResponse) {
+					InvitationResponse r = (InvitationResponse) i;
+					items.add(ConversationItem
+							.from(ConversationActivity.this,
+									contactName, r));
+				}
 			}
-		}
 		return items;
 	}
 
@@ -439,7 +449,7 @@ public class ConversationActivity extends BriarActivity
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Loading body took " + duration + " ms");
-					displayMessageBody(m, body);
+					displayMessageBody(m, StringUtils.fromUtf8(body));
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -448,17 +458,17 @@ public class ConversationActivity extends BriarActivity
 		});
 	}
 
-	private void displayMessageBody(final MessageId m, final byte[] body) {
+	private void displayMessageBody(final MessageId m, final String body) {
 		runOnUiThreadUnlessDestroyed(new Runnable() {
 			@Override
 			public void run() {
 				bodyCache.put(m, body);
-				SparseArray<ConversationMessageItem> messages =
+				SparseArray<ConversationItem> messages =
 						adapter.getPrivateMessages();
 				for (int i = 0; i < messages.size(); i++) {
-					ConversationMessageItem item = messages.valueAt(i);
+					ConversationItem item = messages.valueAt(i);
 					if (item.getId().equals(m)) {
-						item.setBody(body);
+						((PartialItem) item).setText(body);
 						adapter.notifyItemChanged(messages.keyAt(i));
 						list.scrollToPosition(adapter.getItemCount() - 1);
 						return;
@@ -482,9 +492,9 @@ public class ConversationActivity extends BriarActivity
 
 	private void markMessagesRead() {
 		Map<MessageId, GroupId> unread = new HashMap<>();
-		SparseArray<IncomingItem> list = adapter.getIncomingMessages();
+		SparseArray<ConversationInItem> list = adapter.getIncomingMessages();
 		for (int i = 0; i < list.size(); i++) {
-			IncomingItem item = list.valueAt(i);
+			ConversationInItem item = list.valueAt(i);
 			if (!item.isRead())
 				unread.put(item.getId(), item.getGroupId());
 		}
@@ -561,7 +571,8 @@ public class ConversationActivity extends BriarActivity
 			if (event.getContactId().equals(contactId)) {
 				LOG.info("Introduction request received, adding...");
 				IntroductionRequest ir = event.getIntroductionRequest();
-				ConversationItem item = new ConversationIntroductionInItem(ir);
+				ConversationItem item =
+						ConversationRequestItem.from(this, contactName, ir);
 				addConversationItem(item);
 			}
 		} else if (e instanceof IntroductionResponseReceivedEvent) {
@@ -580,7 +591,8 @@ public class ConversationActivity extends BriarActivity
 			if (event.getContactId().equals(contactId)) {
 				LOG.info("Invitation received, adding...");
 				InvitationRequest ir = event.getRequest();
-				ConversationItem item = ConversationItem.from(ir);
+				ConversationItem item =
+						ConversationItem.from(this, contactName, ir);
 				addConversationItem(item);
 			}
 		} else if (e instanceof InvitationResponseReceivedEvent) {
@@ -603,9 +615,10 @@ public class ConversationActivity extends BriarActivity
 			public void run() {
 				adapter.incrementRevision();
 				Set<MessageId> messages = new HashSet<>(messageIds);
-				SparseArray<OutgoingItem> list = adapter.getOutgoingMessages();
+				SparseArray<ConversationOutItem> list =
+						adapter.getOutgoingMessages();
 				for (int i = 0; i < list.size(); i++) {
-					OutgoingItem item = list.valueAt(i);
+					ConversationOutItem item = list.valueAt(i);
 					if (messages.contains(item.getId())) {
 						item.setSent(sent);
 						item.setSeen(seen);
@@ -622,7 +635,7 @@ public class ConversationActivity extends BriarActivity
 		text = StringUtils.truncateUtf8(text, MAX_PRIVATE_MESSAGE_BODY_LENGTH);
 		long timestamp = System.currentTimeMillis();
 		timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
-		createMessage(StringUtils.toUtf8(text), timestamp);
+		createMessage(text, timestamp);
 		textInputView.setText("");
 	}
 
@@ -632,14 +645,14 @@ public class ConversationActivity extends BriarActivity
 		return item == null ? 0 : item.getTime() + 1;
 	}
 
-	private void createMessage(final byte[] body, final long timestamp) {
+	private void createMessage(final String body, final long timestamp) {
 		cryptoExecutor.execute(new Runnable() {
 			@Override
 			public void run() {
 				try {
 					storeMessage(privateMessageFactory.createPrivateMessage(
-							groupId, timestamp, null, "text/plain", body),
-							body);
+							groupId, timestamp, null, "text/plain",
+							StringUtils.toUtf8(body)), body);
 				} catch (FormatException e) {
 					throw new RuntimeException(e);
 				}
@@ -647,7 +660,7 @@ public class ConversationActivity extends BriarActivity
 		});
 	}
 
-	private void storeMessage(final PrivateMessage m, final byte[] body) {
+	private void storeMessage(final PrivateMessage m, final String body) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
@@ -661,8 +674,8 @@ public class ConversationActivity extends BriarActivity
 					PrivateMessageHeader h = new PrivateMessageHeader(id,
 							groupId, m.getMessage().getTimestamp(),
 							m.getContentType(), true, false, false, false);
-					ConversationMessageItem item = ConversationItem.from(h);
-					item.setBody(body);
+					ConversationItem item = ConversationItem.from(h);
+					((PartialItem) item).setText(body);
 					bodyCache.put(id, body);
 					addConversationItem(item);
 				} catch (DbException e) {
@@ -812,23 +825,35 @@ public class ConversationActivity extends BriarActivity
 		});
 	}
 
+	@UiThread
 	@Override
-	public void respondToIntroduction(final SessionId sessionId,
+	public void respondToRequest(final ConversationRequestItem item,
 			final boolean accept) {
+		int position = adapter.findItemPosition(item);
+		if (position != INVALID_POSITION) {
+			adapter.notifyItemChanged(position, item);
+		}
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
 				long timestamp = System.currentTimeMillis();
 				timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
 				try {
-					if (accept) {
-						introductionManager.acceptIntroduction(contactId,
-								sessionId, timestamp);
-					} else {
-						introductionManager.declineIntroduction(contactId,
-								sessionId, timestamp);
+					switch (item.getRequestType()) {
+						case INTRODUCTION:
+							respondToIntroductionRequest(item.getSessionId(),
+								accept, timestamp);
+							break;
+						case FORUM:
+							respondToForumRequest(item.getSessionId(), accept);
+							break;
+						case BLOG:
+							respondToBlogRequest(item.getSessionId(), accept);
+							break;
+						default:
+							throw new IllegalArgumentException(
+									"Unknown Request Type");
 					}
-					loadMessages();
 				} catch (DbException | FormatException e) {
 					introductionResponseError();
 					if (LOG.isLoggable(WARNING))
@@ -839,6 +864,31 @@ public class ConversationActivity extends BriarActivity
 		});
 	}
 
+	@DatabaseExecutor
+	private void respondToIntroductionRequest(SessionId sessionId,
+			boolean accept, long time) throws DbException, FormatException {
+		if (accept) {
+			introductionManager.acceptIntroduction(contactId, sessionId, time);
+		} else {
+			introductionManager.declineIntroduction(contactId, sessionId, time);
+		}
+		loadMessages();
+	}
+
+	@DatabaseExecutor
+	private void respondToForumRequest(SessionId id, boolean accept)
+			throws DbException {
+		forumSharingManager.respondToInvitation(id, accept);
+		loadMessages();
+	}
+
+	@DatabaseExecutor
+	private void respondToBlogRequest(SessionId id, boolean accept)
+			throws DbException {
+		blogSharingManager.respondToInvitation(id, accept);
+		loadMessages();
+	}
+
 	private void introductionResponseError() {
 		runOnUiThreadUnlessDestroyed(new Runnable() {
 			@Override
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
index 3fa184bf2f8cd47da961d30e0bd083e65c7e814e..85711eb6b2f682f5c80bcd624b6f4c799a2ebe3d 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
@@ -1,365 +1,74 @@
 package org.briarproject.android.contact;
 
 import android.content.Context;
-import android.content.Intent;
+import android.support.annotation.LayoutRes;
 import android.support.annotation.Nullable;
-import android.support.v7.widget.RecyclerView;
 import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
 
 import org.briarproject.R;
-import org.briarproject.android.sharing.InvitationsBlogActivity;
-import org.briarproject.android.sharing.InvitationsForumActivity;
-import org.briarproject.android.util.AndroidUtils;
 import org.briarproject.android.util.BriarAdapter;
-import org.briarproject.api.blogs.BlogInvitationRequest;
-import org.briarproject.api.clients.SessionId;
-import org.briarproject.api.forum.ForumInvitationRequest;
-import org.briarproject.api.introduction.IntroductionRequest;
-import org.briarproject.api.messaging.PrivateMessageHeader;
-import org.briarproject.api.sharing.InvitationRequest;
-import org.briarproject.util.StringUtils;
 
-import static android.support.v7.widget.RecyclerView.ViewHolder;
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-import static org.briarproject.android.contact.ConversationItem.BLOG_INVITATION_IN;
-import static org.briarproject.android.contact.ConversationItem.BLOG_INVITATION_OUT;
-import static org.briarproject.android.contact.ConversationItem.FORUM_INVITATION_IN;
-import static org.briarproject.android.contact.ConversationItem.FORUM_INVITATION_OUT;
-import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_IN;
-import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_OUT;
-import static org.briarproject.android.contact.ConversationItem.IncomingItem;
-import static org.briarproject.android.contact.ConversationItem.MSG_IN_UNREAD;
-import static org.briarproject.android.contact.ConversationItem.MSG_OUT;
-import static org.briarproject.android.contact.ConversationItem.NOTICE_IN;
-import static org.briarproject.android.contact.ConversationItem.NOTICE_OUT;
-import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
+class ConversationAdapter
+		extends BriarAdapter<ConversationItem, ConversationItemViewHolder> {
 
-class ConversationAdapter extends BriarAdapter<ConversationItem, ViewHolder> {
+	private RequestListener listener;
 
-	private IntroductionHandler intro;
-	private String contactName;
-
-	ConversationAdapter(Context ctx, IntroductionHandler introductionHandler) {
+	ConversationAdapter(Context ctx, RequestListener requestListener) {
 		super(ctx, ConversationItem.class);
-		intro = introductionHandler;
-	}
-
-	void setContactName(String contactName) {
-		this.contactName = contactName;
-		notifyDataSetChanged();
+		listener = requestListener;
 	}
 
+	@LayoutRes
 	@Override
 	public int getItemViewType(int position) {
-		return items.get(position).getType();
-	}
-
-	@Override
-	public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
-		View v;
-
-		// outgoing message (local)
-		if (type == MSG_OUT) {
-			v = LayoutInflater.from(viewGroup.getContext()).inflate(
-					R.layout.list_item_msg_out, viewGroup, false);
-			return new MessageHolder(v, type);
-		} else if (type == INTRODUCTION_IN) {
-			v = LayoutInflater.from(viewGroup.getContext()).inflate(
-					R.layout.list_item_introduction_in, viewGroup, false);
-			return new IntroductionHolder(v, type);
-		} else if (type == INTRODUCTION_OUT) {
-			v = LayoutInflater.from(viewGroup.getContext()).inflate(
-					R.layout.list_item_msg_notice_out, viewGroup, false);
-			return new IntroductionHolder(v, type);
-		} else if (type == NOTICE_IN) {
-			v = LayoutInflater.from(viewGroup.getContext()).inflate(
-					R.layout.list_item_notice_in, viewGroup, false);
-			return new NoticeHolder(v, type);
-		} else if (type == NOTICE_OUT) {
-			v = LayoutInflater.from(viewGroup.getContext()).inflate(
-					R.layout.list_item_notice_out, viewGroup, false);
-			return new NoticeHolder(v, type);
-		} else if (type == FORUM_INVITATION_IN || type == BLOG_INVITATION_IN) {
-			v = LayoutInflater.from(viewGroup.getContext()).inflate(
-					R.layout.list_item_shareable_invitation_in, viewGroup,
-					false);
-			return new InvitationHolder(v, type);
-		} else if (type == FORUM_INVITATION_OUT ||
-				type == BLOG_INVITATION_OUT) {
-			v = LayoutInflater.from(viewGroup.getContext()).inflate(
-					R.layout.list_item_msg_notice_out, viewGroup, false);
-			return new InvitationHolder(v, type);
-		}
-		// 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 void onBindViewHolder(ViewHolder ui, int position) {
-		ConversationItem item = getItemAt(position);
-		if (item instanceof ConversationMessageItem) {
-			bindMessage((MessageHolder) ui, (ConversationMessageItem) item);
-		} else if (item instanceof ConversationIntroductionOutItem) {
-			bindIntroduction((IntroductionHolder) ui,
-					(ConversationIntroductionOutItem) item, position);
-		} else if (item instanceof ConversationIntroductionInItem) {
-			bindIntroduction((IntroductionHolder) ui,
-					(ConversationIntroductionInItem) item, position);
+		ConversationItem item = items.get(position);
+		if (item instanceof ConversationRequestItem) {
+			return R.layout.list_item_conversation_request;
 		} else if (item instanceof ConversationNoticeOutItem) {
-			bindNotice((NoticeHolder) ui, (ConversationNoticeOutItem) item);
+			return R.layout.list_item_conversation_notice_out;
 		} else if (item instanceof ConversationNoticeInItem) {
-			bindNotice((NoticeHolder) ui, (ConversationNoticeInItem) item);
-		} else if (item instanceof ConversationShareableInvitationOutItem) {
-			bindInvitation((InvitationHolder) ui,
-					(ConversationShareableInvitationOutItem) item);
-		} else if (item instanceof ConversationShareableInvitationInItem) {
-			bindInvitation((InvitationHolder) ui,
-					(ConversationShareableInvitationInItem) item);
-		} else {
-			throw new IllegalArgumentException("Unhandled Conversation Item");
-		}
-	}
-
-	private void bindMessage(MessageHolder ui, ConversationMessageItem item) {
-		PrivateMessageHeader header = item.getHeader();
-
-		if (item instanceof ConversationItem.OutgoingItem) {
-			if (((OutgoingItem) item).isSeen()) {
-				ui.status.setImageResource(R.drawable.message_delivered_white);
-			} else if (((OutgoingItem) item).isSent()) {
-				ui.status.setImageResource(R.drawable.message_sent_white);
-			} else {
-				ui.status.setImageResource(R.drawable.message_stored_white);
-			}
-		} else {
-			if (item.getType() == MSG_IN_UNREAD) {
-				// TODO implement new unread message highlight according to #232
-/*				int left = ui.layout.getPaddingLeft();
-				int top = ui.layout.getPaddingTop();
-				int right = ui.layout.getPaddingRight();
-				int bottom = ui.layout.getPaddingBottom();
-
-				// 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);
-*/
-			}
-		}
-
-		if (item.getBody() == null) {
-			ui.body.setText("\u2026");
-		} else if (header.getContentType().equals("text/plain")) {
-			ui.body.setText(
-					StringUtils.trim(StringUtils.fromUtf8(item.getBody())));
+			return R.layout.list_item_conversation_notice_in;
+		} else if (item instanceof ConversationMessageOutItem) {
+			return R.layout.list_item_conversation_msg_out;
+		} else if (item instanceof ConversationMessageInItem) {
+			return R.layout.list_item_conversation_msg_in;
 		} else {
-			// TODO support other content types
-		}
-
-		long timestamp = header.getTimestamp();
-		ui.date.setText(AndroidUtils.formatDate(ctx, timestamp));
-	}
-
-	private void bindIntroduction(IntroductionHolder ui,
-			final ConversationIntroductionItem item, final int position) {
-
-		final IntroductionRequest ir = item.getIntroductionRequest();
-		int backgroundRes;
-
-		String message = ir.getMessage();
-		if (StringUtils.isNullOrEmpty(message)) {
-			ui.message.setVisibility(GONE);
-			if (item instanceof ConversationIntroductionOutItem) {
-				backgroundRes = R.drawable.notice_out;
-			} else {
-				backgroundRes = R.drawable.notice_in;
-			}
-		} else {
-			ui.message.setText(StringUtils.trim(message));
-			ui.message.setVisibility(VISIBLE);
-			if (item instanceof ConversationIntroductionOutItem) {
-				backgroundRes = R.drawable.notice_out_bottom;
-			} else {
-				backgroundRes = R.drawable.notice_in_bottom;
-			}
+			throw new IllegalArgumentException("Unknown ConversationItem");
 		}
-
-		// Outgoing Introduction Request
-		if (item instanceof ConversationIntroductionOutItem) {
-			ui.text.setText(ctx.getString(R.string.introduction_request_sent,
-					contactName, ir.getName()));
-			ConversationIntroductionOutItem i =
-					(ConversationIntroductionOutItem) item;
-			if (i.isSeen()) {
-				//noinspection ConstantConditions
-				ui.status.setImageResource(R.drawable.message_delivered);
-			} else if (i.isSent()) {
-				//noinspection ConstantConditions
-				ui.status.setImageResource(R.drawable.message_sent);
-			} else {
-				//noinspection ConstantConditions
-				ui.status.setImageResource(R.drawable.message_stored);
-			}
-		}
-		// Incoming Introduction Request (Answered)
-		else if (item.wasAnswered()) {
-			ui.text.setText(ctx.getString(
-					R.string.introduction_request_answered_received,
-					contactName, ir.getName()));
-			ui.acceptButton.setVisibility(GONE);
-			ui.declineButton.setVisibility(GONE);
-		}
-		// Incoming Introduction Request (Not Answered)
-		else {
-			if (item.getIntroductionRequest().contactExists()) {
-				ui.text.setText(ctx.getString(
-						R.string.introduction_request_exists_received,
-						contactName, ir.getName()));
-			} else {
-				ui.text.setText(
-						ctx.getString(R.string.introduction_request_received,
-								contactName, ir.getName()));
-			}
-
-			if (item.getIntroductionRequest().doesIntroduceOtherIdentity()) {
-				// don't allow accept when one of our identities is introduced
-				ui.acceptButton.setVisibility(GONE);
-				ui.text.setText(ctx.getString(
-						R.string.introduction_request_for_our_identity_received,
-						contactName, ir.getName()));
-			} else {
-				ui.acceptButton.setVisibility(VISIBLE);
-				ui.acceptButton.setOnClickListener(new View.OnClickListener() {
-					@Override
-					public void onClick(View v) {
-						intro.respondToIntroduction(ir.getSessionId(), true);
-						item.setAnswered(true);
-						notifyItemChanged(position);
-					}
-				});
-			}
-			ui.declineButton.setVisibility(VISIBLE);
-			ui.declineButton.setOnClickListener(new View.OnClickListener() {
-				@Override
-				public void onClick(View v) {
-					intro.respondToIntroduction(ir.getSessionId(), false);
-					item.setAnswered(true);
-					notifyItemChanged(position);
-				}
-			});
-		}
-		ui.date.setText(AndroidUtils.formatDate(ctx, item.getTime()));
-		ui.notice.setBackgroundResource(backgroundRes);
 	}
 
-	private void bindNotice(NoticeHolder ui, ConversationNoticeItem item) {
-		ui.text.setText(item.getText());
-		ui.date.setText(AndroidUtils.formatDate(ctx, item.getTime()));
-
-		if (item instanceof ConversationNoticeOutItem) {
-			ConversationNoticeOutItem n = (ConversationNoticeOutItem) item;
-			if (n.isSeen()) {
-				//noinspection ConstantConditions
-				ui.status.setImageResource(R.drawable.message_delivered);
-			} else if (n.isSent()) {
-				//noinspection ConstantConditions
-				ui.status.setImageResource(R.drawable.message_sent);
-			} else {
-				//noinspection ConstantConditions
-				ui.status.setImageResource(R.drawable.message_stored);
-			}
+	@Override
+	public ConversationItemViewHolder onCreateViewHolder(ViewGroup viewGroup,
+			@LayoutRes int type) {
+		View v = LayoutInflater.from(viewGroup.getContext()).inflate(
+				type, viewGroup, false);
+		switch (type) {
+			case R.layout.list_item_conversation_msg_in:
+				return new ConversationItemViewHolder(v);
+			case R.layout.list_item_conversation_msg_out:
+				return new ConversationMessageOutViewHolder(v);
+			case R.layout.list_item_conversation_notice_in:
+				return new ConversationNoticeInViewHolder(v);
+			case R.layout.list_item_conversation_notice_out:
+				return new ConversationNoticeOutViewHolder(v);
+			case R.layout.list_item_conversation_request:
+				return new ConversationRequestViewHolder(v);
+			default:
+				throw new IllegalArgumentException("Unknown ConversationItem");
 		}
 	}
 
-	private void bindInvitation(InvitationHolder ui,
-			final ConversationShareableInvitationItem item) {
-
-		final InvitationRequest ir = item.getInvitationRequest();
-		String name = "";
-		int receivedRes =  0, sentRes = 0, buttonRes = 0, backgroundRes;
-		if (ir instanceof ForumInvitationRequest) {
-			name = ((ForumInvitationRequest) ir).getForumName();
-			receivedRes = R.string.forum_invitation_received;
-			sentRes = R.string.forum_invitation_sent;
-			buttonRes = R.string.forum_show_invitations;
-		} else if (ir instanceof BlogInvitationRequest) {
-			name = ((BlogInvitationRequest) ir).getBlogAuthorName();
-			receivedRes = R.string.blogs_sharing_invitation_received;
-			sentRes = R.string.blogs_sharing_invitation_sent;
-			buttonRes = R.string.blogs_sharing_show_invitations;
-		}
-
-		String message = ir.getMessage();
-		if (StringUtils.isNullOrEmpty(message)) {
-			ui.message.setVisibility(GONE);
-			if (item instanceof ConversationShareableInvitationOutItem) {
-				backgroundRes = R.drawable.notice_out;
-			} else {
-				backgroundRes = R.drawable.notice_in;
-			}
+	@Override
+	public void onBindViewHolder(ConversationItemViewHolder ui, int position) {
+		ConversationItem item = items.get(position);
+		if (item instanceof ConversationRequestItem) {
+			((ConversationRequestViewHolder) ui).bind(item, listener);
 		} else {
-			ui.message.setVisibility(VISIBLE);
-			ui.message.setText(StringUtils.trim(message));
-			if (item instanceof ConversationShareableInvitationOutItem) {
-				backgroundRes = R.drawable.notice_out_bottom;
-			} else {
-				backgroundRes = R.drawable.notice_in_bottom;
-			}
-		}
-
-		// Outgoing Invitation
-		if (item instanceof ConversationShareableInvitationOutItem) {
-			ui.text.setText(ctx.getString(sentRes, name, contactName));
-			ConversationShareableInvitationOutItem i =
-					(ConversationShareableInvitationOutItem) item;
-			if (i.isSeen()) {
-				//noinspection ConstantConditions
-				ui.status.setImageResource(R.drawable.message_delivered);
-			} else if (i.isSent()) {
-				//noinspection ConstantConditions
-				ui.status.setImageResource(R.drawable.message_sent);
-			} else {
-				//noinspection ConstantConditions
-				ui.status.setImageResource(R.drawable.message_stored);
-			}
-		}
-		// Incoming Invitation
-		else {
-			ui.text.setText(ctx.getString(receivedRes, contactName, name));
-
-			if (ir.isAvailable()) {
-				final Class c = ir instanceof ForumInvitationRequest ?
-						InvitationsForumActivity.class :
-						InvitationsBlogActivity.class;
-				ui.showInvitationsButton.setText(ctx.getString(buttonRes));
-				ui.showInvitationsButton.setVisibility(VISIBLE);
-				ui.showInvitationsButton
-						.setOnClickListener(new View.OnClickListener() {
-							@Override
-							public void onClick(View v) {
-								Intent i = new Intent(ctx, c);
-								ctx.startActivity(i);
-							}
-						});
-			} else {
-				ui.showInvitationsButton.setVisibility(GONE);
-			}
+			ui.bind(item);
 		}
-		ui.date.setText(AndroidUtils.formatDate(ctx, item.getTime()));
-		ui.notice.setBackgroundResource(backgroundRes);
 	}
 
 	@Override
@@ -393,138 +102,46 @@ class ConversationAdapter extends BriarAdapter<ConversationItem, ViewHolder> {
 		}
 	}
 
-	SparseArray<IncomingItem> getIncomingMessages() {
-		SparseArray<IncomingItem> messages = new SparseArray<>();
+	SparseArray<ConversationInItem> getIncomingMessages() {
+		SparseArray<ConversationInItem> messages = new SparseArray<>();
 
 		for (int i = 0; i < items.size(); i++) {
 			ConversationItem item = items.get(i);
-			if (item instanceof IncomingItem) {
-				messages.put(i, (IncomingItem) item);
+			if (item instanceof ConversationInItem) {
+				messages.put(i, (ConversationInItem) item);
 			}
 		}
 		return messages;
 	}
 
-	SparseArray<OutgoingItem> getOutgoingMessages() {
-		SparseArray<OutgoingItem> messages = new SparseArray<>();
+	SparseArray<ConversationOutItem> getOutgoingMessages() {
+		SparseArray<ConversationOutItem> messages = new SparseArray<>();
 
 		for (int i = 0; i < items.size(); i++) {
 			ConversationItem item = items.get(i);
-			if (item instanceof OutgoingItem) {
-				messages.put(i, (OutgoingItem) item);
+			if (item instanceof ConversationOutItem) {
+				messages.put(i, (ConversationOutItem) item);
 			}
 		}
 		return messages;
 	}
 
-	SparseArray<ConversationMessageItem> getPrivateMessages() {
-		SparseArray<ConversationMessageItem> messages = new SparseArray<>();
+	SparseArray<ConversationItem> getPrivateMessages() {
+		SparseArray<ConversationItem> messages = new SparseArray<>();
 
 		for (int i = 0; i < items.size(); i++) {
 			ConversationItem item = items.get(i);
-			if (item instanceof ConversationMessageItem) {
-				messages.put(i, (ConversationMessageItem) item);
+			if (item instanceof ConversationMessageInItem) {
+				messages.put(i, item);
+			} else if (item instanceof ConversationMessageOutItem) {
+				messages.put(i, item);
 			}
 		}
 		return messages;
 	}
 
-	private static class MessageHolder extends RecyclerView.ViewHolder {
-
-		public ViewGroup layout;
-		public TextView body;
-		private TextView date;
-		public ImageView status;
-
-		private 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);
-			}
-		}
+	interface RequestListener {
+		void respondToRequest(ConversationRequestItem item, boolean accept);
 	}
 
-	private static class IntroductionHolder extends RecyclerView.ViewHolder {
-
-		private final TextView message;
-		private final ViewGroup notice;
-		private final TextView text;
-		private final Button acceptButton;
-		private final Button declineButton;
-		private final TextView date;
-		private final ImageView status;
-
-		private IntroductionHolder(View v, int type) {
-			super(v);
-
-			message = (TextView) v.findViewById(R.id.msgBody);
-			notice = (ViewGroup) v.findViewById(R.id.noticeLayout);
-			text = (TextView) v.findViewById(R.id.introductionText);
-			acceptButton = (Button) v.findViewById(R.id.acceptButton);
-			declineButton = (Button) v.findViewById(R.id.declineButton);
-			date = (TextView) v.findViewById(R.id.introductionTime);
-
-			if (type == INTRODUCTION_OUT) {
-				status = (ImageView) v.findViewById(R.id.introductionStatus);
-			} else {
-				status = null;
-			}
-		}
-	}
-
-	private static class NoticeHolder extends RecyclerView.ViewHolder {
-
-		private final TextView text;
-		private final TextView date;
-		private final ImageView status;
-
-		private NoticeHolder(View v, int type) {
-			super(v);
-
-			text = (TextView) v.findViewById(R.id.noticeText);
-			date = (TextView) v.findViewById(R.id.noticeTime);
-
-			if (type == NOTICE_OUT) {
-				status = (ImageView) v.findViewById(R.id.noticeStatus);
-			} else {
-				status = null;
-			}
-		}
-	}
-
-	private static class InvitationHolder extends RecyclerView.ViewHolder {
-
-		private final TextView message;
-		private final View notice;
-		private final TextView text;
-		private final Button showInvitationsButton;
-		private final TextView date;
-		private final ImageView status;
-
-		private InvitationHolder(View v, int type) {
-			super(v);
-
-			message = (TextView) v.findViewById(R.id.msgBody);
-			text = (TextView) v.findViewById(R.id.introductionText);
-			notice = v.findViewById(R.id.noticeLayout);
-			showInvitationsButton = (Button) v.findViewById(R.id.showInvitationsButton);
-			date = (TextView) v.findViewById(R.id.introductionTime);
-
-			if (type == FORUM_INVITATION_OUT || type == BLOG_INVITATION_OUT) {
-				status = (ImageView) v.findViewById(R.id.introductionStatus);
-			} else {
-				status = null;
-			}
-		}
-	}
-
-	interface IntroductionHandler {
-		void respondToIntroduction(SessionId sessionId, boolean accept);
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationInItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..3d3ab2a1b3d337c6732b6d6f6f43f4e3d9360a73
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationInItem.java
@@ -0,0 +1,31 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+@NotNullByDefault
+abstract class ConversationInItem extends ConversationItem {
+
+	private boolean read;
+
+	ConversationInItem(MessageId id, GroupId groupId, @Nullable String text,
+			long time, boolean read) {
+		super(id, groupId, text, time);
+
+		this.read = read;
+	}
+
+	public boolean isRead() {
+		return read;
+	}
+
+	public void setRead(boolean read) {
+		this.read = read;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java
deleted file mode 100644
index 6959d214b10abb5599c5d1e04d439ecc7ae1f296..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.briarproject.android.contact;
-
-import org.briarproject.android.contact.ConversationItem.IncomingItem;
-import org.briarproject.api.introduction.IntroductionRequest;
-import org.jetbrains.annotations.NotNull;
-
-// This class is not thread-safe
-class ConversationIntroductionInItem extends ConversationIntroductionItem
-		implements IncomingItem {
-
-	private boolean read;
-
-	ConversationIntroductionInItem(@NotNull IntroductionRequest ir) {
-		super(ir);
-
-		this.read = ir.isRead();
-	}
-
-	@Override
-	int getType() {
-		return INTRODUCTION_IN;
-	}
-
-	@Override
-	public boolean isRead() {
-		return read;
-	}
-
-	@Override
-	public void setRead(boolean read) {
-		this.read = read;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
deleted file mode 100644
index d2cdef596ffdadd4c4b38a280f5536ce0d4ead6a..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.briarproject.android.contact;
-
-import org.briarproject.api.introduction.IntroductionRequest;
-import org.jetbrains.annotations.NotNull;
-
-// This class is not thread-safe
-abstract class ConversationIntroductionItem extends ConversationItem {
-
-	private final IntroductionRequest ir;
-	private boolean answered;
-
-	ConversationIntroductionItem(@NotNull IntroductionRequest ir) {
-		super(ir.getMessageId(), ir.getGroupId(), ir.getTimestamp());
-
-		this.ir = ir;
-		this.answered = ir.wasAnswered();
-	}
-
-	@NotNull
-	IntroductionRequest getIntroductionRequest() {
-		return ir;
-	}
-
-	boolean wasAnswered() {
-		return answered;
-	}
-
-	void setAnswered(boolean answered) {
-		this.answered = answered;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java
deleted file mode 100644
index 2729fea0cd549748c7cfccceb43f12813503505d..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.briarproject.android.contact;
-
-import org.briarproject.api.introduction.IntroductionRequest;
-
-/**
- * This class is needed and can not be replaced by an ConversationNoticeOutItem,
- * because it carries the optional introduction message
- * to be displayed as a regular private message.
- *
- *  This class is not thread-safe
- */
-class ConversationIntroductionOutItem extends ConversationIntroductionItem
-		implements ConversationItem.OutgoingItem {
-
-	private boolean sent, seen;
-
-	ConversationIntroductionOutItem(IntroductionRequest ir) {
-		super(ir);
-		this.sent = ir.isSent();
-		this.seen = ir.isSeen();
-	}
-
-	@Override
-	int getType() {
-		return INTRODUCTION_OUT;
-	}
-
-	@Override
-	public boolean isSent() {
-		return sent;
-	}
-
-	@Override
-	public void setSent(boolean sent) {
-		this.sent = sent;
-	}
-
-	@Override
-	public boolean isSeen() {
-		return seen;
-	}
-
-	@Override
-	public void setSeen(boolean seen) {
-		this.seen = seen;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
index d8c5e5aa746900f212744ae1423f811e5e09f818..9d1749ae8796e9fcc5e055e9e3a6dfb495fba538 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
@@ -1,65 +1,64 @@
 package org.briarproject.android.contact;
 
 import android.content.Context;
+import android.support.annotation.StringRes;
 
 import org.briarproject.R;
-import org.briarproject.api.blogs.BlogInvitationResponse;
+import org.briarproject.android.contact.ConversationRequestItem.RequestType;
+import org.briarproject.api.blogs.BlogInvitationRequest;
+import org.briarproject.api.forum.ForumInvitationRequest;
 import org.briarproject.api.forum.ForumInvitationResponse;
-import org.briarproject.api.introduction.IntroductionMessage;
 import org.briarproject.api.introduction.IntroductionRequest;
 import org.briarproject.api.introduction.IntroductionResponse;
 import org.briarproject.api.messaging.PrivateMessageHeader;
-import org.briarproject.api.sharing.InvitationMessage;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sharing.InvitationRequest;
 import org.briarproject.api.sharing.InvitationResponse;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
-import org.jetbrains.annotations.NotNull;
-
-// This class is not thread-safe
-public abstract class ConversationItem {
-
-	// this is needed for RecyclerView adapter which requires an int type
-	final static int MSG_IN = 0;
-	final static int MSG_IN_UNREAD = 1;
-	final static int MSG_OUT = 2;
-	final static int INTRODUCTION_IN = 3;
-	final static int INTRODUCTION_OUT = 4;
-	final static int NOTICE_IN = 5;
-	final static int NOTICE_OUT = 6;
-	final static int FORUM_INVITATION_IN = 7;
-	final static int FORUM_INVITATION_OUT = 8;
-	final static int BLOG_INVITATION_IN = 9;
-	final static int BLOG_INVITATION_OUT = 10;
+import org.jetbrains.annotations.Nullable;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import static org.briarproject.android.contact.ConversationRequestItem.RequestType.BLOG;
+import static org.briarproject.android.contact.ConversationRequestItem.RequestType.FORUM;
+import static org.briarproject.android.contact.ConversationRequestItem.RequestType.INTRODUCTION;
+
+@NotThreadSafe
+@NotNullByDefault
+abstract class ConversationItem {
 
 	final private MessageId id;
 	final private GroupId groupId;
+	protected @Nullable String text;
 	final private long time;
 
-	public ConversationItem(@NotNull MessageId id, @NotNull GroupId groupId,
-			long time) {
+	ConversationItem(MessageId id, GroupId groupId,
+			@Nullable String text, long time) {
 		this.id = id;
 		this.groupId = groupId;
+		this.text = text;
 		this.time = time;
 	}
 
-	abstract int getType();
-
-	@NotNull
-	public MessageId getId() {
+	MessageId getId() {
 		return id;
 	}
 
-	@NotNull
-	public GroupId getGroupId() {
+	GroupId getGroupId() {
 		return groupId;
 	}
 
+	@Nullable
+	public String getText() {
+		return text;
+	}
+
 	long getTime() {
 		return time;
 	}
 
-	public static ConversationMessageItem from(PrivateMessageHeader h) {
+	static ConversationItem from(PrivateMessageHeader h) {
 		if (h.isLocal()) {
 			return new ConversationMessageOutItem(h);
 		} else {
@@ -67,17 +66,40 @@ public abstract class ConversationItem {
 		}
 	}
 
-	public static ConversationIntroductionItem from(IntroductionRequest ir) {
+	static ConversationItem from(Context ctx, String contactName,
+			IntroductionRequest ir) {
 		if (ir.isLocal()) {
-			return new ConversationIntroductionOutItem(ir);
+			String text = ctx.getString(R.string.introduction_request_sent,
+					contactName, ir.getName());
+			return new ConversationNoticeOutItem(ir.getMessageId(),
+					ir.getGroupId(), text, ir.getMessage(), ir.getTimestamp(),
+					ir.isSent(), ir.isSeen());
 		} else {
-			return new ConversationIntroductionInItem(ir);
+			String text;
+			if (ir.wasAnswered()) {
+				text = ctx.getString(
+						R.string.introduction_request_answered_received,
+						contactName, ir.getName());
+				return new ConversationNoticeInItem(ir.getMessageId(),
+						ir.getGroupId(), text, ir.getMessage(), ir.getTimestamp(),
+						ir.isRead());
+			} else if (ir.contactExists()){
+				text = ctx.getString(
+						R.string.introduction_request_exists_received,
+						contactName, ir.getName());
+			} else {
+				text = ctx.getString(R.string.introduction_request_received,
+						contactName, ir.getName());
+			}
+			return new ConversationRequestItem(ir.getMessageId(),
+					ir.getGroupId(), INTRODUCTION, ir.getSessionId(), text,
+					ir.getMessage(), ir.getTimestamp(), ir.isRead(),
+					ir.wasAnswered());
 		}
 	}
 
-	public static ConversationNoticeItem from(Context ctx, String contactName,
+	static ConversationItem from(Context ctx, String contactName,
 			IntroductionResponse ir) {
-
 		if (ir.isLocal()) {
 			String text;
 			if (ir.wasAccepted()) {
@@ -90,7 +112,7 @@ public abstract class ConversationItem {
 						ir.getName());
 			}
 			return new ConversationNoticeOutItem(ir.getMessageId(),
-					ir.getGroupId(), text, ir.getTimestamp(), ir.isSent(),
+					ir.getGroupId(), text, null, ir.getTimestamp(), ir.isSent(),
 					ir.isSeen());
 		} else {
 			String text;
@@ -110,143 +132,99 @@ public abstract class ConversationItem {
 				}
 			}
 			return new ConversationNoticeInItem(ir.getMessageId(),
-					ir.getGroupId(), text, ir.getTimestamp(), ir.isRead());
-		}
-	}
-
-	public static ConversationShareableInvitationItem from(
-			InvitationRequest fim) {
-		if (fim.isLocal()) {
-			return new ConversationShareableInvitationOutItem(fim);
-		} else {
-			return new ConversationShareableInvitationInItem(fim);
-		}
-	}
-
-	public static ConversationNoticeItem from(Context ctx, String contactName,
-			InvitationResponse ir) {
-
-		if (ir instanceof ForumInvitationResponse) {
-			return from(ctx, contactName, (ForumInvitationResponse) ir);
-		} else if (ir instanceof BlogInvitationResponse) {
-			return from(ctx, contactName, (BlogInvitationResponse) ir);
-		} else {
-			throw new IllegalArgumentException("Unknown Invitation Response.");
+					ir.getGroupId(), text, null, ir.getTimestamp(),
+					ir.isRead());
 		}
 	}
 
-	private static ConversationNoticeItem from(Context ctx, String contactName,
-			ForumInvitationResponse fir) {
-
-		if (fir.isLocal()) {
+	static ConversationItem from(Context ctx, String contactName,
+			InvitationRequest ir) {
+		if (ir.isLocal()) {
 			String text;
-			if (fir.wasAccepted()) {
-				text = ctx.getString(
-						R.string.forum_invitation_response_accepted_sent,
+			if (ir instanceof ForumInvitationRequest) {
+				text = ctx.getString(R.string.forum_invitation_sent,
+						((ForumInvitationRequest) ir).getForumName(),
 						contactName);
 			} else {
-				text = ctx.getString(
-						R.string.forum_invitation_response_declined_sent,
+				text = ctx.getString(R.string.blogs_sharing_invitation_sent,
+						((BlogInvitationRequest) ir).getBlogAuthorName(),
 						contactName);
 			}
-			return new ConversationNoticeOutItem(fir.getId(), fir.getGroupId(),
-					text, fir.getTimestamp(), fir.isSent(), fir.isSeen());
+			return new ConversationNoticeOutItem(ir.getId(), ir.getGroupId(),
+					text, ir.getMessage(), ir.getTimestamp(), ir.isSent(),
+					ir.isSeen());
 		} else {
 			String text;
-			if (fir.wasAccepted()) {
-				text = ctx.getString(
-						R.string.forum_invitation_response_accepted_received,
-						contactName);
+			RequestType type;
+			if (ir instanceof ForumInvitationRequest) {
+				text = ctx.getString(R.string.forum_invitation_received,
+						contactName,
+						((ForumInvitationRequest) ir).getForumName());
+				type = FORUM;
 			} else {
-				text = ctx.getString(
-						R.string.forum_invitation_response_declined_received,
-						contactName);
+				text = ctx.getString(R.string.blogs_sharing_invitation_received,
+						contactName,
+						((BlogInvitationRequest) ir).getBlogAuthorName());
+				type = BLOG;
+			}
+			if (!ir.isAvailable()) {
+				return new ConversationNoticeInItem(ir.getId(), ir.getGroupId(),
+						text, ir.getMessage(), ir.getTimestamp(), ir.isRead());
 			}
-			return new ConversationNoticeInItem(fir.getId(), fir.getGroupId(),
-					text, fir.getTimestamp(), fir.isRead());
+			return new ConversationRequestItem(ir.getId(),
+					ir.getGroupId(), type, ir.getSessionId(), text,
+					ir.getMessage(), ir.getTimestamp(), ir.isRead(),
+					!ir.isAvailable());
 		}
 	}
 
-	private static ConversationNoticeItem from(Context ctx, String contactName,
-			BlogInvitationResponse fir) {
-
-		if (fir.isLocal()) {
-			String text;
-			if (fir.wasAccepted()) {
-				text = ctx.getString(
-						R.string.blogs_sharing_response_accepted_sent,
-						contactName);
+	static ConversationItem from(Context ctx, String contactName,
+			InvitationResponse ir) {
+		@StringRes int res;
+		if (ir.isLocal()) {
+			if (ir.wasAccepted()) {
+				if (ir instanceof ForumInvitationResponse) {
+					res = R.string.forum_invitation_response_accepted_sent;
+				} else {
+					res = R.string.blogs_sharing_response_accepted_sent;
+				}
 			} else {
-				text = ctx.getString(
-						R.string.blogs_sharing_response_declined_sent,
-						contactName);
+				if (ir instanceof ForumInvitationResponse) {
+					res = R.string.forum_invitation_response_declined_sent;
+				} else {
+					res = R.string.blogs_sharing_response_declined_sent;
+				}
 			}
-			return new ConversationNoticeOutItem(fir.getId(), fir.getGroupId(),
-					text, fir.getTimestamp(), fir.isSent(), fir.isSeen());
+			String text = ctx.getString(res, contactName);
+			return new ConversationNoticeOutItem(ir.getId(), ir.getGroupId(),
+					text, null, ir.getTimestamp(), ir.isSent(), ir.isSeen());
 		} else {
-			String text;
-			if (fir.wasAccepted()) {
-				text = ctx.getString(
-						R.string.blogs_sharing_response_accepted_received,
-						contactName);
+			if (ir.wasAccepted()) {
+				if (ir instanceof ForumInvitationResponse) {
+					res = R.string.forum_invitation_response_accepted_received;
+				} else {
+					res = R.string.blogs_sharing_response_accepted_received;
+				}
 			} else {
-				text = ctx.getString(
-						R.string.blogs_sharing_response_declined_received,
-						contactName);
+					if (ir instanceof ForumInvitationResponse) {
+						res = R.string.forum_invitation_response_declined_received;
+					} else {
+						res = R.string.blogs_sharing_response_declined_received;
+					}
 			}
-			return new ConversationNoticeInItem(fir.getId(), fir.getGroupId(),
-					text, fir.getTimestamp(), fir.isRead());
+			String text = ctx.getString(res, contactName);
+			return new ConversationNoticeInItem(ir.getId(), ir.getGroupId(),
+					text, null, ir.getTimestamp(), ir.isRead());
 		}
 	}
 
-	/**
-	 * This method should not be used to get user-facing objects,
-	 * Its purpose is only to provide data for the contact list.
-	 */
-	public static ConversationItem from(IntroductionMessage im) {
-		if (im.isLocal())
-			return new ConversationNoticeOutItem(im.getMessageId(),
-					im.getGroupId(), "", im.getTimestamp(), false, false);
-		return new ConversationNoticeInItem(im.getMessageId(), im.getGroupId(),
-				"", im.getTimestamp(), im.isRead());
-	}
-
-	/**
-	 * This method should not be used to get user-facing objects,
-	 * Its purpose is only to provide data for the contact list.
-	 */
-	public static ConversationItem from(InvitationMessage im) {
-		if (im.isLocal())
-			return new ConversationNoticeOutItem(im.getId(), im.getGroupId(),
-					"", im.getTimestamp(), false, false);
-		return new ConversationNoticeInItem(im.getId(), im.getGroupId(), "",
-				im.getTimestamp(), im.isRead());
-	}
+	interface PartialItem {
 
-	interface OutgoingItem {
+		@Nullable
+		String getText();
 
-		@NotNull
-		MessageId getId();
+		void setText(String text);
 
-		boolean isSent();
-
-		void setSent(boolean sent);
-
-		boolean isSeen();
-
-		void setSeen(boolean seen);
 	}
 
-	interface IncomingItem {
-
-		@NotNull
-		MessageId getId();
-
-		@NotNull
-		GroupId getGroupId();
-
-		boolean isRead();
-
-		void setRead(boolean read);
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItemViewHolder.java b/briar-android/src/org/briarproject/android/contact/ConversationItemViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..45713cdecdc2df9e627900941d2fa8c62ccad727
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationItemViewHolder.java
@@ -0,0 +1,42 @@
+package org.briarproject.android.contact;
+
+import android.support.annotation.CallSuper;
+import android.support.annotation.UiThread;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.util.AndroidUtils;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.util.StringUtils;
+
+@UiThread
+@NotNullByDefault
+class ConversationItemViewHolder extends ViewHolder {
+
+	protected final ViewGroup layout;
+	private final TextView text;
+	private final TextView time;
+
+	ConversationItemViewHolder(View v) {
+		super(v);
+		layout = (ViewGroup) v.findViewById(R.id.layout);
+		text = (TextView) v.findViewById(R.id.text);
+		time = (TextView) v.findViewById(R.id.time);
+	}
+
+	@CallSuper
+	void bind(ConversationItem item) {
+		if (item.getText() == null) {
+			text.setText("\u2026");
+		} else {
+			text.setText(StringUtils.trim(item.getText()));
+		}
+
+		long timestamp = item.getTime();
+		time.setText(AndroidUtils.formatDate(time.getContext(), timestamp));
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java
index eefef09b4ef4dc3c38d8d2896ce405af9948e61b..96388b7ae5ce42c939998ff9373c79111c8d3228 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java
@@ -1,31 +1,22 @@
 package org.briarproject.android.contact;
 
+import org.briarproject.android.contact.ConversationItem.PartialItem;
 import org.briarproject.api.messaging.PrivateMessageHeader;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
-// This class is not thread-safe
-class ConversationMessageInItem extends ConversationMessageItem
-		implements ConversationItem.IncomingItem {
+import javax.annotation.concurrent.NotThreadSafe;
 
-	private boolean read;
+@NotThreadSafe
+@NotNullByDefault
+class ConversationMessageInItem extends ConversationInItem
+		implements PartialItem {
 
-	ConversationMessageInItem(PrivateMessageHeader header) {
-		super(header);
-
-		read = header.isRead();
-	}
-
-	@Override
-	int getType() {
-		return MSG_IN;
+	ConversationMessageInItem(PrivateMessageHeader h) {
+		super(h.getId(), h.getGroupId(), null, h.getTimestamp(), h.isRead());
 	}
 
-	@Override
-	public boolean isRead() {
-		return read;
+	public void setText(String body) {
+		text = body;
 	}
 
-	@Override
-	public void setRead(boolean read) {
-		this.read = read;
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java
deleted file mode 100644
index b4dc64b6cc8c2aa86e729f545b19a7a78776b04d..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.briarproject.android.contact;
-
-import org.briarproject.api.messaging.PrivateMessageHeader;
-
-// This class is not thread-safe
-abstract class ConversationMessageItem extends ConversationItem {
-
-	private final PrivateMessageHeader header;
-	private byte[] body;
-
-	ConversationMessageItem(PrivateMessageHeader header) {
-		super(header.getId(), header.getGroupId(), header.getTimestamp());
-
-		this.header = header;
-		body = null;
-	}
-
-	PrivateMessageHeader getHeader() {
-		return header;
-	}
-
-	byte[] getBody() {
-		return body;
-	}
-
-	void setBody(byte[] body) {
-		this.body = body;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java
index 550219e9b3e5cd9aadc71405bb0488b9513fbc4b..9253fd7fa3caa9d1038eca89a19228c6e250e674 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java
@@ -1,42 +1,23 @@
 package org.briarproject.android.contact;
 
+import org.briarproject.android.contact.ConversationItem.PartialItem;
 import org.briarproject.api.messaging.PrivateMessageHeader;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
-// This class is not thread-safe
-class ConversationMessageOutItem extends ConversationMessageItem
-		implements ConversationItem.OutgoingItem {
+import javax.annotation.concurrent.NotThreadSafe;
 
-	private boolean sent, seen;
+@NotThreadSafe
+@NotNullByDefault
+class ConversationMessageOutItem extends ConversationOutItem
+		implements PartialItem {
 
-	ConversationMessageOutItem(PrivateMessageHeader header) {
-		super(header);
-
-		sent = header.isSent();
-		seen = header.isSeen();
-	}
-
-	@Override
-	int getType() {
-		return MSG_OUT;
-	}
-
-	@Override
-	public boolean isSent() {
-		return sent;
-	}
-
-	@Override
-	public void setSent(boolean sent) {
-		this.sent = sent;
+	ConversationMessageOutItem(PrivateMessageHeader h) {
+		super(h.getId(), h.getGroupId(), null, h.getTimestamp(), h.isSent(),
+				h.isSeen());
 	}
 
-	@Override
-	public boolean isSeen() {
-		return seen;
+	public void setText(String body) {
+		text = body;
 	}
 
-	@Override
-	public void setSeen(boolean seen) {
-		this.seen = seen;
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageOutViewHolder.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageOutViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..83d58dcee413315435edef53f126f4e86d8366ed
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageOutViewHolder.java
@@ -0,0 +1,16 @@
+package org.briarproject.android.contact;
+
+import android.view.View;
+
+class ConversationMessageOutViewHolder extends ConversationOutItemViewHolder {
+
+	ConversationMessageOutViewHolder(View v) {
+		super(v);
+	}
+
+	@Override
+	protected boolean hasDarkBackground() {
+		return true;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
index 6e8fbfc8bcb3331b817bea7793f3295eb30b1cfa..df377d2d77eab2f230f4db1411d788b59516acc8 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
@@ -1,33 +1,29 @@
 package org.briarproject.android.contact;
 
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
 
-// This class is not thread-safe
-class ConversationNoticeInItem extends ConversationNoticeItem
-		implements ConversationItem.IncomingItem {
+import javax.annotation.concurrent.NotThreadSafe;
 
-	private boolean read;
+@NotThreadSafe
+@NotNullByDefault
+class ConversationNoticeInItem extends ConversationInItem {
 
-	ConversationNoticeInItem(MessageId id, GroupId groupId, String text,
-			long time, boolean read) {
-		super(id, groupId, text, time);
+	@Nullable
+	private final String msgText;
 
-		this.read = read;
+	ConversationNoticeInItem(MessageId id, GroupId groupId,
+			String text, @Nullable String msgText, long time,
+			boolean read) {
+		super(id, groupId, text, time, read);
+		this.msgText = msgText;
 	}
 
-	@Override
-	int getType() {
-		return NOTICE_IN;
+	@Nullable
+	public String getMsgText() {
+		return msgText;
 	}
 
-	@Override
-	public boolean isRead() {
-		return read;
-	}
-
-	@Override
-	public void setRead(boolean read) {
-		this.read = read;
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInViewHolder.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e5a60824db789619a1ca6323dfd9ffc867a425c
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInViewHolder.java
@@ -0,0 +1,43 @@
+package org.briarproject.android.contact;
+
+import android.support.annotation.UiThread;
+import android.view.View;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.util.StringUtils;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+@UiThread
+@NotNullByDefault
+class ConversationNoticeInViewHolder extends ConversationItemViewHolder {
+
+	private final TextView msgText;
+
+	ConversationNoticeInViewHolder(View v) {
+		super(v);
+		msgText = (TextView) v.findViewById(R.id.msgText);
+	}
+
+	@Override
+	void bind(ConversationItem conversationItem) {
+		super.bind(conversationItem);
+
+		ConversationNoticeInItem item =
+				(ConversationNoticeInItem) conversationItem;
+
+		String message = item.getMsgText();
+		if (StringUtils.isNullOrEmpty(message)) {
+			msgText.setVisibility(GONE);
+			layout.setBackgroundResource(R.drawable.notice_in);
+		} else {
+			msgText.setVisibility(VISIBLE);
+			msgText.setText(StringUtils.trim(message));
+			layout.setBackgroundResource(R.drawable.notice_in_bottom);
+		}
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java
deleted file mode 100644
index 758c311d01f57e9611e1a5b7411c7dcc41d4d689..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.briarproject.android.contact;
-
-import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
-
-abstract class ConversationNoticeItem extends ConversationItem {
-
-	private final String text;
-
-	ConversationNoticeItem(MessageId id, GroupId groupId, String text,
-			long time) {
-		super(id, groupId, time);
-
-		this.text = text;
-	}
-
-	public String getText() {
-		return text;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
index 95ed78510fba0d585b51044422d660a0f1d3a7ad..19ab79c64a159947be001c71edfd5325a11c7cbb 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
@@ -1,44 +1,29 @@
 package org.briarproject.android.contact;
 
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
 
-// This class is not thread-safe
-class ConversationNoticeOutItem extends ConversationNoticeItem
-		implements ConversationItem.OutgoingItem {
+import javax.annotation.concurrent.NotThreadSafe;
 
-	private boolean sent, seen;
+@NotThreadSafe
+@NotNullByDefault
+class ConversationNoticeOutItem extends ConversationOutItem {
 
-	ConversationNoticeOutItem(MessageId id, GroupId groupId, String text,
-			long time, boolean sent, boolean seen) {
-		super(id, groupId, text, time);
+	@Nullable
+	private final String msgText;
 
-		this.sent = sent;
-		this.seen = seen;
+	ConversationNoticeOutItem(MessageId id, GroupId groupId,
+			String text, @Nullable String msgText, long time,
+			boolean sent, boolean seen) {
+		super(id, groupId, text, time, sent, seen);
+		this.msgText = msgText;
 	}
 
-	@Override
-	int getType() {
-		return NOTICE_OUT;
+	@Nullable
+	public String getMsgText() {
+		return msgText;
 	}
 
-	@Override
-	public  boolean isSent() {
-		return sent;
-	}
-
-	@Override
-	public void setSent(boolean sent) {
-		this.sent = sent;
-	}
-
-	@Override
-	public boolean isSeen() {
-		return seen;
-	}
-
-	@Override
-	public void setSeen(boolean seen) {
-		this.seen = seen;
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutViewHolder.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..c0bdf7fcf758c42956bacd0b76c4c0b4869af560
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutViewHolder.java
@@ -0,0 +1,48 @@
+package org.briarproject.android.contact;
+
+import android.support.annotation.UiThread;
+import android.view.View;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.util.StringUtils;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+@UiThread
+@NotNullByDefault
+class ConversationNoticeOutViewHolder extends ConversationOutItemViewHolder {
+
+	private final TextView msgText;
+
+	ConversationNoticeOutViewHolder(View v) {
+		super(v);
+		msgText = (TextView) v.findViewById(R.id.msgText);
+	}
+
+	@Override
+	void bind(ConversationItem conversationItem) {
+		super.bind(conversationItem);
+
+		ConversationNoticeOutItem item =
+				(ConversationNoticeOutItem) conversationItem;
+
+		String message = item.getMsgText();
+		if (StringUtils.isNullOrEmpty(message)) {
+			msgText.setVisibility(GONE);
+			layout.setBackgroundResource(R.drawable.notice_out);
+		} else {
+			msgText.setVisibility(VISIBLE);
+			msgText.setText(StringUtils.trim(message));
+			layout.setBackgroundResource(R.drawable.notice_out_bottom);
+		}
+	}
+
+	@Override
+	protected boolean hasDarkBackground() {
+		return false;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationOutItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..f8cbaec23aad74e83426a539c01ca4481917f253
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationOutItem.java
@@ -0,0 +1,40 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+@NotNullByDefault
+abstract class ConversationOutItem extends ConversationItem {
+
+	private boolean sent, seen;
+
+	ConversationOutItem(MessageId id, GroupId groupId, @Nullable String text,
+			long time, boolean sent, boolean seen) {
+		super(id, groupId, text, time);
+
+		this.sent = sent;
+		this.seen = seen;
+	}
+
+	public boolean isSent() {
+		return sent;
+	}
+
+	public void setSent(boolean sent) {
+		this.sent = sent;
+	}
+
+	public boolean isSeen() {
+		return seen;
+	}
+
+	public void setSeen(boolean seen) {
+		this.seen = seen;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationOutItemViewHolder.java b/briar-android/src/org/briarproject/android/contact/ConversationOutItemViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c10ba402499c155e48d70fa40202b96ef1b018e
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationOutItemViewHolder.java
@@ -0,0 +1,44 @@
+package org.briarproject.android.contact;
+
+import android.support.annotation.UiThread;
+import android.view.View;
+import android.widget.ImageView;
+
+import org.briarproject.R;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+
+@UiThread
+@NotNullByDefault
+abstract class ConversationOutItemViewHolder
+		extends ConversationItemViewHolder {
+
+	private final ImageView status;
+
+	ConversationOutItemViewHolder(View v) {
+		super(v);
+		status = (ImageView) v.findViewById(R.id.status);
+	}
+
+	@Override
+	void bind(ConversationItem conversationItem) {
+		super.bind(conversationItem);
+
+		ConversationOutItem item = (ConversationOutItem) conversationItem;
+
+		int res;
+		if (item.isSeen()) {
+			if (hasDarkBackground()) res = R.drawable.message_delivered_white;
+			else res = R.drawable.message_delivered;
+		} else if (item.isSent()) {
+			if (hasDarkBackground()) res = R.drawable.message_sent_white;
+			else res = R.drawable.message_sent;
+		} else {
+			if (hasDarkBackground()) res = R.drawable.message_stored_white;
+			else res = R.drawable.message_stored;
+		}
+		status.setImageResource(res);
+	}
+
+	protected abstract boolean hasDarkBackground();
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationRequestItem.java b/briar-android/src/org/briarproject/android/contact/ConversationRequestItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..9999350d42f59e5c7ad72f6360c0ff0ddfd1945f
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationRequestItem.java
@@ -0,0 +1,46 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+@NotNullByDefault
+class ConversationRequestItem extends ConversationNoticeInItem {
+
+	enum RequestType { INTRODUCTION, FORUM, BLOG };
+	private final RequestType requestType;
+	private final SessionId sessionId;
+	private boolean answered;
+
+	ConversationRequestItem(MessageId id, GroupId groupId,
+			RequestType requestType, SessionId sessionId, String text,
+			@Nullable String msgText, long time, boolean read,
+			boolean answered) {
+		super(id, groupId, text, msgText, time, read);
+		this.requestType = requestType;
+		this.sessionId = sessionId;
+		this.answered = answered;
+	}
+
+	public RequestType getRequestType() {
+		return requestType;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	boolean wasAnswered() {
+		return answered;
+	}
+
+	void setAnswered(boolean answered) {
+		this.answered = answered;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationRequestViewHolder.java b/briar-android/src/org/briarproject/android/contact/ConversationRequestViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..972884ada22976df853dce81c8b5651df13d62e1
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationRequestViewHolder.java
@@ -0,0 +1,58 @@
+package org.briarproject.android.contact;
+
+import android.support.annotation.UiThread;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+
+import org.briarproject.R;
+import org.briarproject.android.contact.ConversationAdapter.RequestListener;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+@UiThread
+@NotNullByDefault
+class ConversationRequestViewHolder extends ConversationNoticeInViewHolder {
+
+	private final Button acceptButton;
+	private final Button declineButton;
+
+	ConversationRequestViewHolder(View v) {
+		super(v);
+		acceptButton = (Button) v.findViewById(R.id.acceptButton);
+		declineButton = (Button) v.findViewById(R.id.declineButton);
+	}
+
+	void bind(ConversationItem conversationItem,
+			final RequestListener listener) {
+		super.bind(conversationItem);
+
+		final ConversationRequestItem item =
+				(ConversationRequestItem) conversationItem;
+
+		if (item.wasAnswered()) {
+			acceptButton.setVisibility(GONE);
+			declineButton.setVisibility(GONE);
+		} else {
+			acceptButton.setVisibility(VISIBLE);
+			acceptButton.setOnClickListener(new OnClickListener() {
+				@Override
+				public void onClick(View v) {
+					item.setAnswered(true);
+					listener.respondToRequest(item, true);
+				}
+			});
+			declineButton.setVisibility(VISIBLE);
+			declineButton.setOnClickListener(new OnClickListener() {
+				@Override
+				public void onClick(View v) {
+					item.setAnswered(true);
+					listener.respondToRequest(item, false);
+				}
+			});
+		}
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationInItem.java
deleted file mode 100644
index 6e46b7b4160174929712bf959921830e9c12f88a..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationInItem.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.briarproject.android.contact;
-
-import org.briarproject.api.blogs.BlogInvitationRequest;
-import org.briarproject.api.forum.ForumInvitationRequest;
-import org.briarproject.api.sharing.InvitationRequest;
-
-// This class is not thread-safe
-class ConversationShareableInvitationInItem
-		extends ConversationShareableInvitationItem
-		implements ConversationItem.IncomingItem {
-
-	private final int type;
-	private boolean read;
-
-	ConversationShareableInvitationInItem(InvitationRequest ir) {
-		super(ir);
-
-		if (ir instanceof ForumInvitationRequest) {
-			this.type = FORUM_INVITATION_IN;
-		} else if (ir instanceof BlogInvitationRequest) {
-			this.type = BLOG_INVITATION_IN;
-		} else {
-			throw new IllegalArgumentException("Unknown Invitation Type.");
-		}
-
-		this.read = ir.isRead();
-	}
-
-	@Override
-	int getType() {
-		return type;
-	}
-
-	@Override
-	public boolean isRead() {
-		return read;
-	}
-
-	@Override
-	public void setRead(boolean read) {
-		this.read = read;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationItem.java
deleted file mode 100644
index 60f8a70b1ca4bd1f331925cb771c35813e1f422e..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationItem.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.briarproject.android.contact;
-
-import org.briarproject.api.sharing.InvitationRequest;
-
-abstract class ConversationShareableInvitationItem extends ConversationItem {
-
-	private final InvitationRequest fim;
-
-	ConversationShareableInvitationItem(InvitationRequest fim) {
-		super(fim.getId(), fim.getGroupId(), fim.getTimestamp());
-
-		this.fim = fim;
-	}
-
-	InvitationRequest getInvitationRequest() {
-		return fim;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationOutItem.java
deleted file mode 100644
index 426403ab36c488cb94af91a76ef0f52b010be671..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationOutItem.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package org.briarproject.android.contact;
-
-import org.briarproject.api.blogs.BlogInvitationRequest;
-import org.briarproject.api.forum.ForumInvitationRequest;
-import org.briarproject.api.sharing.InvitationRequest;
-
-/**
- * This class is needed and can not be replaced by an ConversationNoticeOutItem,
- * because it carries the optional invitation message
- * to be displayed as a regular private message.
- * <p/>
- * This class is not thread-safe
- */
-class ConversationShareableInvitationOutItem
-		extends ConversationShareableInvitationItem
-		implements ConversationItem.OutgoingItem {
-
-	private final int type;
-	private boolean sent, seen;
-
-	ConversationShareableInvitationOutItem(InvitationRequest ir) {
-		super(ir);
-
-		if (ir instanceof ForumInvitationRequest) {
-			this.type = FORUM_INVITATION_OUT;
-		} else if (ir instanceof BlogInvitationRequest) {
-			this.type = BLOG_INVITATION_OUT;
-		} else {
-			throw new IllegalArgumentException("Unknown Invitation Type.");
-		}
-
-		this.sent = ir.isSent();
-		this.seen = ir.isSeen();
-	}
-
-	@Override
-	int getType() {
-		return type;
-	}
-
-	@Override
-	public boolean isSent() {
-		return sent;
-	}
-
-	@Override
-	public void setSent(boolean sent) {
-		this.sent = sent;
-	}
-
-	@Override
-	public boolean isSeen() {
-		return seen;
-	}
-
-	@Override
-	public void setSeen(boolean seen) {
-		this.seen = seen;
-	}
-}