diff --git a/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTest.java
index bca45b25f81d71816dc280fd83ca0ad86252b88f..2cc0340bcd48593f8888a4734dbefab8c1920fcb 100644
--- a/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTest.java
@@ -547,7 +547,7 @@ public class BlogSharingIntegrationTest extends BriarIntegrationTest {
 				BlogInvitationReceivedEvent event =
 						(BlogInvitationReceivedEvent) e;
 				eventWaiter.assertEquals(contactId1, event.getContactId());
-				Blog b = event.getBlog();
+				Blog b = event.getShareable();
 				try {
 					Contact c = contactManager0.getContact(contactId1);
 					blogSharingManager0.respondToInvitation(b, c, true);
@@ -589,7 +589,7 @@ public class BlogSharingIntegrationTest extends BriarIntegrationTest {
 						(BlogInvitationReceivedEvent) e;
 				requestReceived = true;
 				if (!answer) return;
-				Blog b = event.getBlog();
+				Blog b = event.getShareable();
 				try {
 					eventWaiter.assertEquals(1,
 							blogSharingManager1.getInvitations().size());
diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java
index ae3e770a4e944e48fc201490cee7e8965c0876e0..6d2184a6aec574ad6ffec86bd58ab02ab6105f2f 100644
--- a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java
@@ -34,7 +34,7 @@ import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.lifecycle.LifecycleManager;
-import org.briarproject.api.sharing.InvitationItem;
+import org.briarproject.api.sharing.SharingInvitationItem;
 import org.briarproject.api.sharing.InvitationMessage;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.SyncSession;
@@ -762,7 +762,7 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
 					"Sharer2 to Invitee");
 
 			// make sure we now have two invitations to the same forum available
-			Collection<InvitationItem> forums =
+			Collection<SharingInvitationItem> forums =
 					forumSharingManager1.getInvitations();
 			assertEquals(1, forums.size());
 			assertEquals(2, forums.iterator().next().getNewSharers().size());
@@ -939,7 +939,7 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
 						(ForumInvitationReceivedEvent) e;
 				eventWaiter.assertEquals(contactId1, event.getContactId());
 				requestReceived = true;
-				Forum f = event.getForum();
+				Forum f = event.getShareable();
 				try {
 					Contact c = contactManager0.getContact(contactId1);
 					forumSharingManager0.respondToInvitation(f, c, true);
@@ -982,11 +982,11 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
 						(ForumInvitationReceivedEvent) e;
 				requestReceived = true;
 				if (!answer) return;
-				Forum f = event.getForum();
+				Forum f = event.getShareable();
 				try {
 					eventWaiter.assertEquals(1,
 							forumSharingManager1.getInvitations().size());
-					InvitationItem invitation =
+					SharingInvitationItem invitation =
 							forumSharingManager1.getInvitations().iterator()
 									.next();
 					eventWaiter.assertEquals(f, invitation.getShareable());
diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index aad00c6d9e747d81de25845bbd0493c0f2060861..7e8006f8f049ac505841726b175497c48c479090 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -123,7 +123,17 @@
 		</activity>
 
 		<activity
-			android:name=".android.sharing.InvitationsForumActivity"
+			android:name=".android.privategroup.invitation.GroupInvitationActivity"
+			android:label="@string/groups_invitations_title"
+			android:parentActivityName=".android.NavDrawerActivity">
+			<meta-data
+				android:name="android.support.PARENT_ACTIVITY"
+				android:value=".android.NavDrawerActivity"
+				/>
+		</activity>
+
+		<activity
+			android:name=".android.sharing.ForumInvitationActivity"
 			android:label="@string/forum_invitations_title"
 			android:parentActivityName=".android.NavDrawerActivity">
 			<meta-data
@@ -133,7 +143,7 @@
 		</activity>
 
 		<activity
-			android:name=".android.sharing.InvitationsBlogActivity"
+			android:name=".android.sharing.BlogInvitationActivity"
 			android:label="@string/blogs_sharing_invitations_title"
 			android:parentActivityName=".android.contact.ConversationActivity">
 			<meta-data
@@ -187,7 +197,7 @@
 		</activity>
 
 		<activity
-			android:name=".android.sharing.SharingStatusForumActivity"
+			android:name=".android.sharing.ForumSharingStatusActivity"
 			android:label="@string/sharing_status"
 			android:parentActivityName=".android.forum.ForumActivity">
 			<meta-data
@@ -197,7 +207,7 @@
 		</activity>
 
 		<activity
-			android:name=".android.sharing.SharingStatusBlogActivity"
+			android:name=".android.sharing.BlogSharingStatusActivity"
 			android:label="@string/sharing_status"
 			android:parentActivityName=".android.blogs.BlogActivity">
 			<meta-data
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 872800c9f05297d0fa40ae400567dfe9d37069da..878b281e48a5de9ec154e3b7b0ca69a2bf8467fe 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>
@@ -168,6 +167,21 @@
 	<string name="groups_leave">Leave Group</string>
 	<string name="groups_dissolve">Dissolve Group</string>
 
+	<!-- Private Group Invitations -->
+	<string name="groups_invitations_title">Group Invitations</string>
+	<string name="groups_invitations_invitation_sent">You have invited %1$s to your group "%2$s".</string>
+	<string name="groups_invitations_invitation_received">%1$s has invited you to join the group "%2$s".</string>
+	<string name="groups_invitations_joined">Joined group</string>
+	<string name="groups_invitations_declined">Group invitation declined</string>
+	<plurals name="groups_invitations_open">
+		<item quantity="one">%d open group invitation</item>
+		<item quantity="other">%d open group invitations</item>
+	</plurals>
+	<string name="groups_invitations_response_accepted_sent">You accepted the group invitation from %s.</string>
+	<string name="groups_invitations_response_declined_sent">You declined the group invitation from %s.</string>
+	<string name="groups_invitations_response_accepted_received">%s accepted your group invitation.</string>
+	<string name="groups_invitations_response_declined_received">%s declined your group invitation.</string>
+
 	<!-- Forums -->
 	<string name="no_forums">You don\'t have any forums yet.\n\nWhy don\'t you create a new one yourself by tapping the + icon at the top?\n\nYou can also ask your contacts to share forums with you.</string>
 	<string name="create_forum_title">New Forum</string>
@@ -206,7 +220,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>
@@ -270,7 +283,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/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index 1179905b6932ced492c512debc964cc53d0ff76a..0a7776b09e0b06731f864f51c48d138b606d1987 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -33,15 +33,16 @@ import org.briarproject.android.privategroup.creation.CreateGroupFragment;
 import org.briarproject.android.privategroup.conversation.GroupActivity;
 import org.briarproject.android.privategroup.creation.CreateGroupMessageFragment;
 import org.briarproject.android.privategroup.list.GroupListFragment;
-import org.briarproject.android.sharing.ContactSelectorFragment;
-import org.briarproject.android.sharing.InvitationsBlogActivity;
-import org.briarproject.android.sharing.InvitationsForumActivity;
+import org.briarproject.android.privategroup.invitation.GroupInvitationActivity;
 import org.briarproject.android.sharing.ShareBlogActivity;
-import org.briarproject.android.sharing.ShareBlogMessageFragment;
+import org.briarproject.android.sharing.BlogSharingStatusActivity;
+import org.briarproject.android.sharing.ContactSelectorFragment;
+import org.briarproject.android.sharing.BlogInvitationActivity;
+import org.briarproject.android.sharing.ForumInvitationActivity;
 import org.briarproject.android.sharing.ShareForumActivity;
 import org.briarproject.android.sharing.ShareForumMessageFragment;
-import org.briarproject.android.sharing.SharingStatusBlogActivity;
-import org.briarproject.android.sharing.SharingStatusForumActivity;
+import org.briarproject.android.sharing.ForumSharingStatusActivity;
+import org.briarproject.android.sharing.ShareBlogMessageFragment;
 import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
 import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
 
@@ -72,13 +73,13 @@ public interface ActivityComponent {
 
 	void inject(ConversationActivity activity);
 
-	void inject(InvitationsForumActivity activity);
+	void inject(ForumInvitationActivity activity);
 
-	void inject(InvitationsBlogActivity activity);
+	void inject(BlogInvitationActivity activity);
 
 	void inject(CreateGroupActivity activity);
-
 	void inject(GroupActivity activity);
+	void inject(GroupInvitationActivity activity);
 
 	void inject(CreateForumActivity activity);
 
@@ -86,9 +87,9 @@ public interface ActivityComponent {
 
 	void inject(ShareBlogActivity activity);
 
-	void inject(SharingStatusForumActivity activity);
+	void inject(ForumSharingStatusActivity activity);
 
-	void inject(SharingStatusBlogActivity activity);
+	void inject(BlogSharingStatusActivity activity);
 
 	void inject(ForumActivity activity);
 
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 69505d32f3ea0d2f7bbef96cd5e21e97470dc57b..174a5c94668a193735716965d889c99644626b21 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -25,8 +25,14 @@ import org.briarproject.android.privategroup.conversation.GroupController;
 import org.briarproject.android.privategroup.conversation.GroupControllerImpl;
 import org.briarproject.android.privategroup.creation.CreateGroupController;
 import org.briarproject.android.privategroup.creation.CreateGroupControllerImpl;
+import org.briarproject.android.privategroup.invitation.GroupInvitationController;
+import org.briarproject.android.privategroup.invitation.GroupInvitationControllerImpl;
 import org.briarproject.android.privategroup.list.GroupListController;
 import org.briarproject.android.privategroup.list.GroupListControllerImpl;
+import org.briarproject.android.sharing.BlogInvitationController;
+import org.briarproject.android.sharing.BlogInvitationControllerImpl;
+import org.briarproject.android.sharing.ForumInvitationController;
+import org.briarproject.android.sharing.ForumInvitationControllerImpl;
 
 import dagger.Module;
 import dagger.Provides;
@@ -117,6 +123,13 @@ public class ActivityModule {
 		return groupController;
 	}
 
+	@ActivityScope
+	@Provides
+	protected GroupInvitationController provideInvitationGroupController(
+			GroupInvitationControllerImpl groupInvitationController) {
+		return groupInvitationController;
+	}
+
 	@ActivityScope
 	@Provides
 	protected ForumController provideForumController(
@@ -125,6 +138,22 @@ public class ActivityModule {
 		return forumController;
 	}
 
+	@ActivityScope
+	@Provides
+	protected ForumInvitationController provideInvitationForumController(
+			ForumInvitationControllerImpl forumInvitationController) {
+		activity.addLifecycleController(forumInvitationController);
+		return forumInvitationController;
+	}
+
+	@ActivityScope
+	@Provides
+	protected BlogInvitationController provideInvitationBlogController(
+			BlogInvitationControllerImpl blogInvitationController) {
+		activity.addLifecycleController(blogInvitationController);
+		return blogInvitationController;
+	}
+
 	@ActivityScope
 	@Provides
 	BlogController provideBlogController(BlogControllerImpl blogController) {
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index ceb3cd9e0b94be42eaa86acd8e2e24df8658b982..6081f1da9b5172bc1c1a6d2e7a74c47f37b6ad27 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -35,6 +35,7 @@ import org.briarproject.api.messaging.PrivateMessageFactory;
 import org.briarproject.api.plugins.ConnectionRegistry;
 import org.briarproject.api.plugins.PluginManager;
 import org.briarproject.api.privategroup.PrivateGroupManager;
+import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
 import org.briarproject.api.settings.SettingsManager;
 import org.briarproject.api.system.Clock;
 import org.briarproject.plugins.AndroidPluginsModule;
@@ -96,6 +97,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
 
 	PrivateGroupManager privateGroupManager();
 
+	GroupInvitationManager groupInvitationManager();
+
 	ForumManager forumManager();
 
 	ForumSharingManager forumSharingManager();
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
index 8809864d55e7103f684d1262bfcf017752b4a342..ac8e2816609e4719333ca6c0c4e6ffe2cb62ae6d 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
@@ -24,7 +24,7 @@ import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
 import org.briarproject.android.controller.handler.UiResultExceptionHandler;
 import org.briarproject.android.fragment.BaseFragment;
 import org.briarproject.android.sharing.ShareBlogActivity;
-import org.briarproject.android.sharing.SharingStatusBlogActivity;
+import org.briarproject.android.sharing.BlogSharingStatusActivity;
 import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.blogs.BlogPostHeader;
 import org.briarproject.api.db.DbException;
@@ -164,7 +164,7 @@ public class BlogFragment extends BaseFragment implements
 				return true;
 			case R.id.action_blog_sharing_status:
 				Intent i3 = new Intent(getActivity(),
-						SharingStatusBlogActivity.class);
+						BlogSharingStatusActivity.class);
 				i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
 				i3.putExtra(GROUP_ID, groupId.getBytes());
 				startActivity(i3, options.toBundle());
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
index c7114857c62297168ea4ddc3757996d58b9d44c7..9f5d23971e307f06a08e63055ab8c34aa957e530 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
@@ -22,6 +22,7 @@ import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.fragment.BaseFragment;
 import org.briarproject.android.keyagreement.KeyAgreementActivity;
 import org.briarproject.android.view.BriarRecyclerView;
+import org.briarproject.api.clients.BaseMessageHeader;
 import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -270,34 +271,35 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 			LOG.info("Private message received, updating item");
 			PrivateMessageReceivedEvent p = (PrivateMessageReceivedEvent) e;
 			PrivateMessageHeader h = p.getMessageHeader();
-			updateItem(p.getContactId(), ConversationItem.from(h));
+			updateItem(p.getContactId(), h);
 		} else if (e instanceof IntroductionRequestReceivedEvent) {
 			LOG.info("Introduction request received, updating item");
 			IntroductionRequestReceivedEvent m =
 					(IntroductionRequestReceivedEvent) e;
 			IntroductionRequest ir = m.getIntroductionRequest();
-			updateItem(m.getContactId(), ConversationItem.from(ir));
+			updateItem(m.getContactId(), 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(), ir);
 		} else if (e instanceof InvitationRequestReceivedEvent) {
-			LOG.info("Invitation request received, updating item");
-			InvitationRequestReceivedEvent m = (InvitationRequestReceivedEvent) e;
+			LOG.info("Invitation Request received, update item");
+			InvitationRequestReceivedEvent m =
+					(InvitationRequestReceivedEvent) e;
 			InvitationRequest ir = m.getRequest();
-			updateItem(m.getContactId(), ConversationItem.from(ir));
+			updateItem(m.getContactId(), 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(), ir);
 		}
 	}
 
-	private void updateItem(final ContactId c, final ConversationItem m) {
+	private void updateItem(final ContactId c, final BaseMessageHeader h) {
 		listener.runOnUiThreadUnlessDestroyed(new Runnable() {
 			@Override
 			public void run() {
@@ -305,7 +307,8 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 				int position = adapter.findItemPosition(c);
 				ContactListItem item = adapter.getItemAt(position);
 				if (item != null) {
-					item.addMessage(m);
+					ConversationItem i = ConversationItem.from(getContext(), h);
+					item.addMessage(i);
 					adapter.updateItemAt(position, item);
 				}
 			}
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListItem.java b/briar-android/src/org/briarproject/android/contact/ContactListItem.java
index e90cdf939b14f6a582594bcc36dd36d4b2aa7909..e9c9e568b2e9c8853d0a17020ac7e4bf754ae646 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListItem.java
@@ -6,9 +6,9 @@ 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;
+import javax.annotation.concurrent.NotThreadSafe;
 
-// This class is NOT thread-safe
+@NotThreadSafe
 public class ContactListItem {
 
 	private final Contact contact;
@@ -34,8 +34,7 @@ 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.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..7bd1dfc45fad34d51221a1c5186b6520b4348803 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,7 @@ 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.introduction.IntroductionActivity;
 import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.android.view.TextInputView;
@@ -39,6 +40,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;
@@ -64,6 +66,7 @@ import org.briarproject.api.messaging.PrivateMessage;
 import org.briarproject.api.messaging.PrivateMessageFactory;
 import org.briarproject.api.messaging.PrivateMessageHeader;
 import org.briarproject.api.plugins.ConnectionRegistry;
+import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
 import org.briarproject.api.settings.Settings;
 import org.briarproject.api.settings.SettingsManager;
 import org.briarproject.api.sharing.InvitationMessage;
@@ -72,6 +75,7 @@ import org.briarproject.api.sharing.InvitationResponse;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.StringUtils;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -92,16 +96,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 +120,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;
@@ -144,6 +147,8 @@ public class ConversationActivity extends BriarActivity
 	volatile ForumSharingManager forumSharingManager;
 	@Inject
 	volatile BlogSharingManager blogSharingManager;
+	@Inject
+	volatile GroupInvitationManager groupInvitationManager;
 
 	private volatile GroupId groupId = null;
 	private volatile ContactId contactId = null;
@@ -325,7 +330,6 @@ public class ConversationActivity extends BriarActivity
 					toolbarStatus
 							.setContentDescription(getString(R.string.offline));
 				}
-				adapter.setContactName(contactName);
 			}
 		});
 	}
@@ -350,10 +354,14 @@ public class ConversationActivity extends BriarActivity
 					Collection<InvitationMessage> blogInvitations =
 							blogSharingManager
 									.getInvitationMessages(contactId);
+					Collection<InvitationMessage> groupInvitations =
+							groupInvitationManager
+									.getInvitationMessages(contactId);
 					List<InvitationMessage> invitations = new ArrayList<>(
 							forumInvitations.size() + blogInvitations.size());
 					invitations.addAll(forumInvitations);
 					invitations.addAll(blogInvitations);
+					invitations.addAll(groupInvitations);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Loading messages took " + duration + " ms");
@@ -397,34 +405,41 @@ public class ConversationActivity extends BriarActivity
 			Collection<PrivateMessageHeader> headers,
 			Collection<IntroductionMessage> introductions,
 			Collection<InvitationMessage> invitations) {
-		int size = headers.size() + introductions.size() + invitations.size();
+		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());
+			ConversationItem item = ConversationItem.from(h);
+			String 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));
+		for (IntroductionMessage m : introductions) {
+			ConversationItem item;
+			if (m instanceof IntroductionRequest) {
+				IntroductionRequest i = (IntroductionRequest) m;
+				item = ConversationItem
+						.from(ConversationActivity.this, contactName, i);
 			} else {
-				IntroductionResponse ir = (IntroductionResponse) im;
-				items.add(ConversationItem.from(ConversationActivity.this,
-								contactName, ir));
+				IntroductionResponse i = (IntroductionResponse) m;
+				item = ConversationItem
+						.from(ConversationActivity.this, contactName, i);
 			}
+			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 (InvitationMessage i : invitations) {
+			ConversationItem item;
+			if (i instanceof InvitationRequest) {
+				InvitationRequest r = (InvitationRequest) i;
+				item = ConversationItem
+						.from(ConversationActivity.this, contactName, r);
+			} else {
+				InvitationResponse r = (InvitationResponse) i;
+				item = ConversationItem
+						.from(ConversationActivity.this, contactName, r);
 			}
+			items.add(item);
 		}
 		return items;
 	}
@@ -439,7 +454,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,15 +463,15 @@ 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);
 						adapter.notifyItemChanged(messages.keyAt(i));
@@ -482,9 +497,9 @@ public class ConversationActivity extends BriarActivity
 
 	private void markMessagesRead() {
 		Map<MessageId, GroupId> unread = new HashMap<>();
-		SparseArray<IncomingItem> list = adapter.getIncomingMessages();
+		SparseArray<ConversationItem> list = adapter.getIncomingMessages();
 		for (int i = 0; i < list.size(); i++) {
-			IncomingItem item = list.valueAt(i);
+			ConversationItem item = list.valueAt(i);
 			if (!item.isRead())
 				unread.put(item.getId(), item.getGroupId());
 		}
@@ -561,7 +576,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 =
+						ConversationItem.from(this, contactName, ir);
 				addConversationItem(item);
 			}
 		} else if (e instanceof IntroductionResponseReceivedEvent) {
@@ -580,7 +596,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 +620,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 +640,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 +650,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 +665,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,7 +679,7 @@ 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);
+					ConversationItem item = ConversationItem.from(h);
 					item.setBody(body);
 					bodyCache.put(id, body);
 					addConversationItem(item);
@@ -812,21 +830,37 @@ public class ConversationActivity extends BriarActivity
 		});
 	}
 
+	@UiThread
 	@Override
-	public void respondToIntroduction(final SessionId sessionId,
+	public void respondToRequest(@NotNull 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;
+						case GROUP:
+							respondToGroupRequest(item.getSessionId(), accept);
+							break;
+						default:
+							throw new IllegalArgumentException(
+									"Unknown Request Type");
 					}
 					loadMessages();
 				} catch (DbException | FormatException e) {
@@ -839,6 +873,34 @@ 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);
+		}
+	}
+
+	@DatabaseExecutor
+	private void respondToForumRequest(SessionId id, boolean accept)
+			throws DbException {
+		forumSharingManager.respondToInvitation(id, accept);
+	}
+
+	@DatabaseExecutor
+	private void respondToBlogRequest(SessionId id, boolean accept)
+			throws DbException {
+		blogSharingManager.respondToInvitation(id, accept);
+	}
+
+	@DatabaseExecutor
+	private void respondToGroupRequest(SessionId id, boolean accept)
+			throws DbException {
+		groupInvitationManager.respondToInvitation(id, accept);
+	}
+
 	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..6798ff7d0a450fa60a98db863eee3a8f2c4eae5b 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
@@ -1,365 +1,64 @@
 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.support.annotation.UiThread;
 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 org.briarproject.api.nullsafety.NotNullByDefault;
 
-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();
+		ConversationItem item = items.get(position);
+		return item.getLayout();
 	}
 
 	@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);
+	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");
 		}
 	}
 
 	@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);
-		} else if (item instanceof ConversationNoticeOutItem) {
-			bindNotice((NoticeHolder) ui, (ConversationNoticeOutItem) item);
-		} 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);
-			}
+	public void onBindViewHolder(ConversationItemViewHolder ui, int position) {
+		ConversationItem item = items.get(position);
+		if (item instanceof ConversationRequestItem) {
+			((ConversationRequestViewHolder) ui).bind(item, listener);
 		} 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())));
-		} else {
-			// TODO support other content types
+			ui.bind(item);
 		}
-
-		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;
-			}
-		}
-
-		// 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);
-			}
-		}
-	}
-
-	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;
-			}
-		} 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.date.setText(AndroidUtils.formatDate(ctx, item.getTime()));
-		ui.notice.setBackgroundResource(backgroundRes);
 	}
 
 	@Override
@@ -393,138 +92,48 @@ class ConversationAdapter extends BriarAdapter<ConversationItem, ViewHolder> {
 		}
 	}
 
-	SparseArray<IncomingItem> getIncomingMessages() {
-		SparseArray<IncomingItem> messages = new SparseArray<>();
+	SparseArray<ConversationItem> getIncomingMessages() {
+		SparseArray<ConversationItem> 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.isIncoming()) {
+				messages.put(i, 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);
-			}
-		}
-	}
-
-	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;
-			}
-		}
+	@UiThread
+	@NotNullByDefault
+	interface RequestListener {
+		void respondToRequest(ConversationRequestItem item, boolean accept);
 	}
 
-	interface IntroductionHandler {
-		void respondToIntroduction(SessionId sessionId, boolean accept);
-	}
 }
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..1a2c118e24c751448772a42f5ea005317d8a2103 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
@@ -1,65 +1,85 @@
 package org.briarproject.android.contact;
 
 import android.content.Context;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.StringRes;
 
 import org.briarproject.R;
+import org.briarproject.android.contact.ConversationRequestItem.RequestType;
+import org.briarproject.api.blogs.BlogInvitationRequest;
 import org.briarproject.api.blogs.BlogInvitationResponse;
+import org.briarproject.api.clients.BaseMessageHeader;
+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.privategroup.invitation.GroupInvitationRequest;
+import org.briarproject.api.privategroup.invitation.GroupInvitationResponse;
 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.GROUP;
+import static org.briarproject.android.contact.ConversationRequestItem.RequestType.INTRODUCTION;
+
+@NotThreadSafe
+@NotNullByDefault
+abstract class ConversationItem {
+
+	protected @Nullable String body;
 	final private MessageId id;
 	final private GroupId groupId;
 	final private long time;
+	private boolean read;
 
-	public ConversationItem(@NotNull MessageId id, @NotNull GroupId groupId,
-			long time) {
+	ConversationItem(MessageId id, GroupId groupId, @Nullable String body,
+			long time, boolean read) {
 		this.id = id;
 		this.groupId = groupId;
+		this.body = body;
 		this.time = time;
+		this.read = read;
 	}
 
-	abstract int getType();
-
-	@NotNull
-	public MessageId getId() {
+	MessageId getId() {
 		return id;
 	}
 
-	@NotNull
-	public GroupId getGroupId() {
+	GroupId getGroupId() {
 		return groupId;
 	}
 
+	void setBody(String body) {
+		this.body = body;
+	}
+
+	@Nullable
+	public String getBody() {
+		return body;
+	}
+
 	long getTime() {
 		return time;
 	}
 
-	public static ConversationMessageItem from(PrivateMessageHeader h) {
+	public boolean isRead() {
+		return read;
+	}
+
+	abstract public boolean isIncoming();
+
+	@LayoutRes
+	abstract public int getLayout();
+
+	static ConversationItem from(PrivateMessageHeader h) {
 		if (h.isLocal()) {
 			return new ConversationMessageOutItem(h);
 		} else {
@@ -67,17 +87,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 +133,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 +153,146 @@ 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,
+			} else if (ir instanceof BlogInvitationRequest) {
+				text = ctx.getString(R.string.blogs_sharing_invitation_sent,
+						((BlogInvitationRequest) ir).getBlogAuthorName(),
 						contactName);
+			} else if (ir instanceof GroupInvitationRequest) {
+				text = ctx.getString(
+						R.string.groups_invitations_invitation_sent,
+						contactName,
+						((GroupInvitationRequest) ir).getGroupName());
+			} else {
+				throw new IllegalArgumentException("Unknown InvitationRequest");
 			}
-			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()) {
+			RequestType type;
+			if (ir instanceof ForumInvitationRequest) {
+				text = ctx.getString(R.string.forum_invitation_received,
+						contactName,
+						((ForumInvitationRequest) ir).getForumName());
+				type = FORUM;
+			} else if (ir instanceof BlogInvitationRequest) {
+				text = ctx.getString(R.string.blogs_sharing_invitation_received,
+						contactName,
+						((BlogInvitationRequest) ir).getBlogAuthorName());
+				type = BLOG;
+			} else if (ir instanceof GroupInvitationRequest) {
 				text = ctx.getString(
-						R.string.forum_invitation_response_accepted_received,
-						contactName);
+						R.string.groups_invitations_invitation_received,
+						contactName,
+						((GroupInvitationRequest) ir).getGroupName());
+				type = GROUP;
 			} else {
-				text = ctx.getString(
-						R.string.forum_invitation_response_declined_received,
-						contactName);
+				throw new IllegalArgumentException("Unknown InvitationRequest");
+			}
+			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 if (ir instanceof BlogInvitationResponse) {
+					res = R.string.blogs_sharing_response_accepted_sent;
+				} else if (ir instanceof GroupInvitationResponse) {
+					res = R.string.groups_invitations_response_accepted_sent;
+				} else {
+					throw new IllegalArgumentException(
+							"Unknown InvitationResponse");
+				}
 			} 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 if (ir instanceof BlogInvitationResponse) {
+					res = R.string.blogs_sharing_response_declined_sent;
+				} else if (ir instanceof GroupInvitationResponse) {
+					res = R.string.groups_invitations_response_declined_sent;
+				} else {
+					throw new IllegalArgumentException(
+							"Unknown InvitationResponse");
+				}
 			}
-			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 if (ir instanceof BlogInvitationResponse) {
+					res = R.string.blogs_sharing_response_accepted_received;
+				} else if (ir instanceof GroupInvitationResponse) {
+					res = R.string.groups_invitations_response_accepted_received;
+				} else {
+					throw new IllegalArgumentException(
+							"Unknown InvitationResponse");
+				}
 			} 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 if (ir instanceof BlogInvitationResponse) {
+						res = R.string.blogs_sharing_response_declined_received;
+					} else if (ir instanceof GroupInvitationResponse) {
+						res = R.string.groups_invitations_response_declined_received;
+					} else {
+						throw new IllegalArgumentException(
+								"Unknown InvitationResponse");
+					}
 			}
-			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 OutgoingItem {
-
-		@NotNull
-		MessageId getId();
-
-		boolean isSent();
-
-		void setSent(boolean sent);
-
-		boolean isSeen();
-
-		void setSeen(boolean seen);
+	 * This method should not be used to display the resulting ConversationItem
+	 * in the UI, but only to update list information based on the
+	 * BaseMessageHeader.
+	 **/
+	static ConversationItem from(Context ctx, BaseMessageHeader h) {
+		if (h instanceof PrivateMessageHeader) {
+			return from((PrivateMessageHeader) h);
+		} else if(h instanceof IntroductionRequest) {
+			return from(ctx, "", (IntroductionRequest) h);
+		} else if(h instanceof IntroductionResponse) {
+			return from(ctx, "", (IntroductionResponse) h);
+		} else if(h instanceof InvitationRequest) {
+			return from(ctx, "", (InvitationRequest) h);
+		} else if(h instanceof InvitationResponse) {
+			return from(ctx, "", (InvitationResponse) h);
+		} else {
+			throw new IllegalArgumentException("Unknown message header");
+		}
 	}
 
-	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..58a42951b98c2a9f8a92df2fb4969cdc5d120549
--- /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.getBody() == null) {
+			text.setText("\u2026");
+		} else {
+			text.setText(StringUtils.trim(item.getBody()));
+		}
+
+		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..23d8571366e27d46c2838e05c04d2cb796c078cc 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java
@@ -1,31 +1,30 @@
 package org.briarproject.android.contact;
 
-import org.briarproject.api.messaging.PrivateMessageHeader;
+import android.support.annotation.LayoutRes;
 
-// This class is not thread-safe
-class ConversationMessageInItem extends ConversationMessageItem
-		implements ConversationItem.IncomingItem {
+import org.briarproject.R;
+import org.briarproject.api.messaging.PrivateMessageHeader;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
-	private boolean read;
+import javax.annotation.concurrent.NotThreadSafe;
 
-	ConversationMessageInItem(PrivateMessageHeader header) {
-		super(header);
+@NotThreadSafe
+@NotNullByDefault
+class ConversationMessageInItem extends ConversationItem {
 
-		read = header.isRead();
+	ConversationMessageInItem(PrivateMessageHeader h) {
+		super(h.getId(), h.getGroupId(), null, h.getTimestamp(), h.isRead());
 	}
 
 	@Override
-	int getType() {
-		return MSG_IN;
+	public boolean isIncoming() {
+		return true;
 	}
 
+	@LayoutRes
 	@Override
-	public boolean isRead() {
-		return read;
+	public int getLayout() {
+		return R.layout.list_item_conversation_msg_in;
 	}
 
-	@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..ee2d1a11b95884cb5b4aad0c117a46146b91dbec 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java
@@ -1,42 +1,26 @@
 package org.briarproject.android.contact;
 
-import org.briarproject.api.messaging.PrivateMessageHeader;
+import android.support.annotation.LayoutRes;
 
-// This class is not thread-safe
-class ConversationMessageOutItem extends ConversationMessageItem
-		implements ConversationItem.OutgoingItem {
+import org.briarproject.R;
+import org.briarproject.api.messaging.PrivateMessageHeader;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
-	private boolean sent, seen;
+import javax.annotation.concurrent.NotThreadSafe;
 
-	ConversationMessageOutItem(PrivateMessageHeader header) {
-		super(header);
+@NotThreadSafe
+@NotNullByDefault
+class ConversationMessageOutItem extends ConversationOutItem {
 
-		sent = header.isSent();
-		seen = header.isSeen();
+	ConversationMessageOutItem(PrivateMessageHeader h) {
+		super(h.getId(), h.getGroupId(), null, h.getTimestamp(), h.isSent(),
+				h.isSeen());
 	}
 
+	@LayoutRes
 	@Override
-	int getType() {
-		return MSG_OUT;
+	public int getLayout() {
+		return R.layout.list_item_conversation_msg_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/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..0e70e66312ee213190994e40c67a7f7cef2f9c56 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
@@ -1,33 +1,43 @@
 package org.briarproject.android.contact;
 
+import android.support.annotation.LayoutRes;
+
+import org.briarproject.R;
+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 ConversationItem {
 
-	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;
+	public boolean isIncoming() {
+		return true;
 	}
 
+	@LayoutRes
 	@Override
-	public void setRead(boolean read) {
-		this.read = read;
+	public int getLayout() {
+		return R.layout.list_item_conversation_notice_in;
 	}
+
 }
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..78cddf3a766ea7a3402fa0ca1262001c7e93e56b 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
@@ -1,44 +1,38 @@
 package org.briarproject.android.contact;
 
+import android.support.annotation.LayoutRes;
+
+import org.briarproject.R;
+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 {
-
-	private boolean sent, seen;
+import javax.annotation.concurrent.NotThreadSafe;
 
-	ConversationNoticeOutItem(MessageId id, GroupId groupId, String text,
-			long time, boolean sent, boolean seen) {
-		super(id, groupId, text, time);
+@NotThreadSafe
+@NotNullByDefault
+class ConversationNoticeOutItem extends ConversationOutItem {
 
-		this.sent = sent;
-		this.seen = seen;
-	}
-
-	@Override
-	int getType() {
-		return NOTICE_OUT;
-	}
+	@Nullable
+	private final String msgText;
 
-	@Override
-	public  boolean isSent() {
-		return sent;
+	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
-	public void setSent(boolean sent) {
-		this.sent = sent;
+	@Nullable
+	public String getMsgText() {
+		return msgText;
 	}
 
+	@LayoutRes
 	@Override
-	public boolean isSeen() {
-		return seen;
+	public int getLayout() {
+		return R.layout.list_item_conversation_notice_out;
 	}
 
-	@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..469f4b0f448fc5f6ad5610700cf308c00ee45b9a
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationOutItem.java
@@ -0,0 +1,45 @@
+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, true);
+
+		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;
+	}
+
+	@Override
+	public boolean isIncoming() {
+		return false;
+	}
+
+}
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..f0a1cb08b40fdd73c909bae2a0912fdde6ab3c6c
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationRequestItem.java
@@ -0,0 +1,55 @@
+package org.briarproject.android.contact;
+
+import android.support.annotation.LayoutRes;
+
+import org.briarproject.R;
+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, GROUP };
+	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;
+	}
+
+	@LayoutRes
+	@Override
+	public int getLayout() {
+		return R.layout.list_item_conversation_request;
+	}
+
+}
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;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index 12487efc946fa28c551df033daf29f9a76920687..307a7c5e158407d4a0cd173974607da181716181 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -19,7 +19,7 @@ import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.controller.handler.UiResultExceptionHandler;
 import org.briarproject.android.sharing.ShareForumActivity;
-import org.briarproject.android.sharing.SharingStatusForumActivity;
+import org.briarproject.android.sharing.ForumSharingStatusActivity;
 import org.briarproject.android.threaded.ThreadListActivity;
 import org.briarproject.android.threaded.ThreadListController;
 import org.briarproject.api.db.DbException;
@@ -114,7 +114,7 @@ public class ForumActivity extends
 						REQUEST_FORUM_SHARED, options.toBundle());
 				return true;
 			case R.id.action_forum_sharing_status:
-				Intent i3 = new Intent(this, SharingStatusForumActivity.class);
+				Intent i3 = new Intent(this, ForumSharingStatusActivity.class);
 				i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
 				i3.putExtra(GROUP_ID, groupId.getBytes());
 				ActivityCompat.startActivity(this, i3, options.toBundle());
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
index f1faecd46d260908e5c7b7e89428df6b2aed2b8b..bb5d1eee0a231c2983aaa4c093c0ae0ac923519f 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
@@ -18,7 +18,7 @@ import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.fragment.BaseEventFragment;
-import org.briarproject.android.sharing.InvitationsForumActivity;
+import org.briarproject.android.sharing.ForumInvitationActivity;
 import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.db.DbException;
@@ -220,10 +220,10 @@ public class ForumListFragment extends BaseEventFragment implements
 				if (availableCount == 0) {
 					snackbar.dismiss();
 				} else {
-					snackbar.show();
 					snackbar.setText(getResources().getQuantityString(
 							R.plurals.forums_shared, availableCount,
 							availableCount));
+					if (!snackbar.isShownOrQueued()) snackbar.show();
 				}
 			}
 		});
@@ -286,7 +286,7 @@ public class ForumListFragment extends BaseEventFragment implements
 	@Override
 	public void onClick(View view) {
 		// snackbar click
-		Intent i = new Intent(getContext(), InvitationsForumActivity.class);
+		Intent i = new Intent(getContext(), ForumInvitationActivity.class);
 		startActivity(i);
 	}
 }
diff --git a/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationActivity.java b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..658a611d4d31856f7f82485d929438791b13acbf
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationActivity.java
@@ -0,0 +1,47 @@
+package org.briarproject.android.privategroup.invitation;
+
+import android.content.Context;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.sharing.InvitationActivity;
+import org.briarproject.android.sharing.InvitationAdapter;
+import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
+
+import javax.inject.Inject;
+
+import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
+
+public class GroupInvitationActivity
+		extends InvitationActivity<GroupInvitationItem> {
+
+	@Inject
+	protected GroupInvitationController controller;
+
+	@Override
+	public void injectActivity(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	protected GroupInvitationController getController() {
+		return controller;
+	}
+
+	@Override
+	protected InvitationAdapter<GroupInvitationItem, ?> getAdapter(Context ctx,
+			InvitationClickListener<GroupInvitationItem> listener) {
+		return new GroupInvitationAdapter(ctx, listener);
+	}
+
+	@Override
+	protected int getAcceptRes() {
+		return R.string.groups_invitations_joined;
+	}
+
+	@Override
+	protected int getDeclineRes() {
+		return R.string.groups_invitations_declined;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationAdapter.java b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..9c818b68070b26af9db47e26e71e88b8e02dd030
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationAdapter.java
@@ -0,0 +1,28 @@
+package org.briarproject.android.privategroup.invitation;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import org.briarproject.android.sharing.InvitationAdapter;
+import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
+
+class GroupInvitationAdapter extends
+		InvitationAdapter<GroupInvitationItem, GroupInvitationViewHolder> {
+
+	GroupInvitationAdapter(Context ctx,
+			InvitationClickListener<GroupInvitationItem> listener) {
+		super(ctx, GroupInvitationItem.class, listener);
+	}
+
+	@Override
+	public GroupInvitationViewHolder onCreateViewHolder(ViewGroup parent,
+			int viewType) {
+		return new GroupInvitationViewHolder(getView(parent));
+	}
+
+	@Override
+	public boolean areContentsTheSame(GroupInvitationItem item1,
+			GroupInvitationItem item2) {
+		return item1.isSubscribed() == item2.isSubscribed();
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationController.java b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationController.java
new file mode 100644
index 0000000000000000000000000000000000000000..54318f31b2dc5b96e4852e1650d8cd27696e556b
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationController.java
@@ -0,0 +1,8 @@
+package org.briarproject.android.privategroup.invitation;
+
+import org.briarproject.android.sharing.InvitationController;
+import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
+
+public interface GroupInvitationController
+		extends InvitationController<GroupInvitationItem> {
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..b48a8743774d67ac6b5f0195a95bceb4c375723d
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationControllerImpl.java
@@ -0,0 +1,83 @@
+package org.briarproject.android.privategroup.invitation;
+
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.android.sharing.InvitationControllerImpl;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.GroupInvitationReceivedEvent;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.PrivateGroupManager;
+import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
+import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
+import org.briarproject.api.sync.ClientId;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+
+public class GroupInvitationControllerImpl
+		extends InvitationControllerImpl<GroupInvitationItem>
+		implements GroupInvitationController {
+
+	private final PrivateGroupManager privateGroupManager;
+	private final GroupInvitationManager groupInvitationManager;
+
+	@Inject
+	GroupInvitationControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			LifecycleManager lifecycleManager, EventBus eventBus,
+			PrivateGroupManager privateGroupManager,
+			GroupInvitationManager groupInvitationManager) {
+		super(dbExecutor, lifecycleManager, eventBus);
+		this.privateGroupManager = privateGroupManager;
+		this.groupInvitationManager = groupInvitationManager;
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		super.eventOccurred(e);
+
+		if (e instanceof GroupInvitationReceivedEvent) {
+			LOG.info("Group invitation received, reloading");
+			listener.loadInvitations(false);
+		}
+	}
+
+	@Override
+	protected ClientId getShareableClientId() {
+		return privateGroupManager.getClientId();
+	}
+
+	@Override
+	protected Collection<GroupInvitationItem> getInvitations()
+			throws DbException {
+		return groupInvitationManager.getInvitations();
+	}
+
+	@Override
+	public void respondToInvitation(final GroupInvitationItem item,
+			final boolean accept,
+			final ResultExceptionHandler<Void, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					PrivateGroup g = item.getShareable();
+					Contact c = item.getCreator();
+					groupInvitationManager.respondToInvitation(g, c, accept);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..25aef8ce8d2df68022ee0c7dd4e0286b85347c99
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationViewHolder.java
@@ -0,0 +1,28 @@
+package org.briarproject.android.privategroup.invitation;
+
+import android.support.annotation.Nullable;
+import android.view.View;
+
+import org.briarproject.R;
+import org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
+import org.briarproject.android.sharing.InvitationViewHolder;
+import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
+
+public class GroupInvitationViewHolder extends InvitationViewHolder<GroupInvitationItem> {
+
+	public GroupInvitationViewHolder(View v) {
+		super(v);
+	}
+
+	@Override
+	public void onBind(@Nullable final GroupInvitationItem item,
+			final InvitationClickListener<GroupInvitationItem> listener) {
+		super.onBind(item, listener);
+		if (item == null) return;
+
+		sharedBy.setText(
+				sharedBy.getContext().getString(R.string.groups_created_by,
+						item.getCreator().getAuthor().getName()));
+	}
+
+}
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListController.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListController.java
index fbe05315d01fcb316fb57ef3f74b95e27f5d107a..f102f82e49dd0641709f6b52a85f59e39a6aecb5 100644
--- a/briar-android/src/org/briarproject/android/privategroup/list/GroupListController.java
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListController.java
@@ -30,6 +30,9 @@ public interface GroupListController extends DbController {
 	void removeGroup(GroupId g,
 			ResultExceptionHandler<Void, DbException> result);
 
+	void loadAvailableGroups(
+			ResultExceptionHandler<Integer, DbException> result);
+
 	interface GroupListListener extends DestroyableContext {
 
 		@UiThread
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java
index efa36fe5f2079e2bc818e0d0baad8b53e428d4c7..76856f244d627722d468826586a55b92dbc56bb6 100644
--- a/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java
@@ -20,6 +20,7 @@ import org.briarproject.api.lifecycle.LifecycleManager;
 import org.briarproject.api.privategroup.GroupMessageHeader;
 import org.briarproject.api.privategroup.PrivateGroup;
 import org.briarproject.api.privategroup.PrivateGroupManager;
+import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
 
@@ -41,6 +42,7 @@ public class GroupListControllerImpl extends DbControllerImpl
 			Logger.getLogger(GroupListControllerImpl.class.getName());
 
 	private final PrivateGroupManager groupManager;
+	private final GroupInvitationManager groupInvitationManager;
 	private final EventBus eventBus;
 	private final AndroidNotificationManager notificationManager;
 	private final IdentityManager identityManager;
@@ -50,10 +52,12 @@ public class GroupListControllerImpl extends DbControllerImpl
 	@Inject
 	GroupListControllerImpl(@DatabaseExecutor Executor dbExecutor,
 			LifecycleManager lifecycleManager, PrivateGroupManager groupManager,
-			EventBus eventBus, AndroidNotificationManager notificationManager,
+			GroupInvitationManager groupInvitationManager, EventBus eventBus,
+			AndroidNotificationManager notificationManager,
 			IdentityManager identityManager) {
 		super(dbExecutor, lifecycleManager);
 		this.groupManager = groupManager;
+		this.groupInvitationManager = groupInvitationManager;
 		this.eventBus = eventBus;
 		this.notificationManager = notificationManager;
 		this.identityManager = identityManager;
@@ -187,4 +191,22 @@ public class GroupListControllerImpl extends DbControllerImpl
 		});
 	}
 
+	@Override
+	public void loadAvailableGroups(
+			final ResultExceptionHandler<Integer, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					handler.onResult(
+							groupInvitationManager.getInvitations().size());
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java
index 9e77003acedb1f79f17c76cac2df0309848ed2ad..64727401dc37727b4b04e227267afaa6277924d0 100644
--- a/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java
@@ -4,13 +4,16 @@ import android.content.Intent;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
+import android.support.design.widget.Snackbar;
 import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v4.content.ContextCompat;
 import android.support.v7.widget.LinearLayoutManager;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 
 import org.briarproject.R;
@@ -18,6 +21,7 @@ import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.controller.handler.UiResultExceptionHandler;
 import org.briarproject.android.fragment.BaseFragment;
 import org.briarproject.android.privategroup.creation.CreateGroupActivity;
+import org.briarproject.android.privategroup.invitation.GroupInvitationActivity;
 import org.briarproject.android.privategroup.list.GroupListController.GroupListListener;
 import org.briarproject.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
 import org.briarproject.android.view.BriarRecyclerView;
@@ -30,10 +34,11 @@ import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
+import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
 import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
 
 public class GroupListFragment extends BaseFragment implements
-		GroupListListener, OnGroupRemoveClickListener {
+		GroupListListener, OnGroupRemoveClickListener, OnClickListener {
 
 	public final static String TAG = GroupListFragment.class.getName();
 	private static final Logger LOG = Logger.getLogger(TAG);
@@ -47,6 +52,7 @@ public class GroupListFragment extends BaseFragment implements
 
 	private BriarRecyclerView list;
 	private GroupListAdapter adapter;
+	private Snackbar snackbar;
 
 	@Nullable
 	@Override
@@ -61,6 +67,12 @@ public class GroupListFragment extends BaseFragment implements
 		list.setLayoutManager(new LinearLayoutManager(getContext()));
 		list.setAdapter(adapter);
 
+		snackbar = Snackbar.make(list, "", LENGTH_INDEFINITE);
+		snackbar.getView().setBackgroundResource(R.color.briar_primary);
+		snackbar.setAction(R.string.show, this);
+		snackbar.setActionTextColor(ContextCompat
+				.getColor(getContext(), R.color.briar_button_positive));
+
 		return v;
 	}
 
@@ -76,6 +88,7 @@ public class GroupListFragment extends BaseFragment implements
 		controller.onStart();
 		list.startPeriodicUpdate();
 		loadGroups();
+		loadAvailableGroups();
 	}
 
 	@Override
@@ -180,4 +193,40 @@ public class GroupListFragment extends BaseFragment implements
 				});
 	}
 
+	private void loadAvailableGroups() {
+		controller.loadAvailableGroups(
+				new UiResultExceptionHandler<Integer, DbException>(this) {
+					@Override
+					public void onResultUi(Integer num) {
+						if (num == 0) {
+							snackbar.dismiss();
+						} else {
+							snackbar.setText(getResources().getQuantityString(
+									R.plurals.groups_invitations_open, num,
+									num));
+							if (!snackbar.isShownOrQueued()) snackbar.show();
+						}
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO handle this error
+						finish();
+					}
+				});
+	}
+
+	/**
+	 * This method is handling the available groups snackbar action
+	 */
+	@Override
+	public void onClick(View v) {
+		Intent i = new Intent(getContext(), GroupInvitationActivity.class);
+		ActivityOptionsCompat options =
+				makeCustomAnimation(getActivity(),
+						android.R.anim.slide_in_left,
+						android.R.anim.slide_out_right);
+		startActivity(i, options.toBundle());
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/sharing/BlogInvitationActivity.java b/briar-android/src/org/briarproject/android/sharing/BlogInvitationActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..54ffa984dd6fdc812c3d646f890607158a61bca8
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/BlogInvitationActivity.java
@@ -0,0 +1,46 @@
+package org.briarproject.android.sharing;
+
+import android.content.Context;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.api.sharing.SharingInvitationItem;
+
+import javax.inject.Inject;
+
+import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
+
+public class BlogInvitationActivity
+		extends InvitationActivity<SharingInvitationItem> {
+
+	@Inject
+	BlogInvitationController controller;
+
+	@Override
+	public void injectActivity(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	protected InvitationController<SharingInvitationItem> getController() {
+		return controller;
+	}
+
+	@Override
+	protected InvitationAdapter<SharingInvitationItem, ?> getAdapter(
+			Context ctx,
+			InvitationClickListener<SharingInvitationItem> listener) {
+		return new SharingInvitationAdapter(ctx, listener);
+	}
+
+	@Override
+	protected int getAcceptRes() {
+		return R.string.blogs_sharing_joined_toast;
+	}
+
+	@Override
+	protected int getDeclineRes() {
+		return R.string.blogs_sharing_declined_toast;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/BlogInvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/BlogInvitationAdapter.java
deleted file mode 100644
index 7b0e7dfa6e108067aed9470e96f88921216518de..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/sharing/BlogInvitationAdapter.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.briarproject.android.sharing;
-
-import android.content.Context;
-
-import org.briarproject.R;
-import org.briarproject.api.blogs.Blog;
-import org.briarproject.api.sharing.InvitationItem;
-
-class BlogInvitationAdapter extends InvitationAdapter {
-
-	BlogInvitationAdapter(Context ctx, AvailableForumClickListener listener) {
-		super(ctx, listener);
-	}
-
-	@Override
-	public void onBindViewHolder(InvitationsViewHolder ui, int position) {
-		super.onBindViewHolder(ui, position);
-		InvitationItem item = getItemAt(position);
-		if (item == null) return;
-
-		Blog blog = (Blog) item.getShareable();
-
-		ui.avatar.setAuthorAvatar(blog.getAuthor());
-
-		ui.name.setText(ctx.getString(R.string.blogs_personal_blog,
-				blog.getAuthor().getName()));
-
-		if (item.isSubscribed()) {
-			ui.subscribed.setText(ctx.getString(R.string.blogs_sharing_exists));
-		}
-	}
-
-	@Override
-	public int compare(InvitationItem o1, InvitationItem o2) {
-		return String.CASE_INSENSITIVE_ORDER
-				.compare(((Blog) o1.getShareable()).getAuthor().getName(),
-						((Blog) o2.getShareable()).getAuthor().getName());
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/sharing/BlogInvitationController.java b/briar-android/src/org/briarproject/android/sharing/BlogInvitationController.java
new file mode 100644
index 0000000000000000000000000000000000000000..ebd447369e9ea28e1a22be632729a546ddf29430
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/BlogInvitationController.java
@@ -0,0 +1,7 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.api.sharing.SharingInvitationItem;
+
+public interface BlogInvitationController
+		extends InvitationController<SharingInvitationItem> {
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/BlogInvitationControllerImpl.java b/briar-android/src/org/briarproject/android/sharing/BlogInvitationControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..49507b4f4036bf7d0bb7444266ed180482d0181b
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/BlogInvitationControllerImpl.java
@@ -0,0 +1,82 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.blogs.Blog;
+import org.briarproject.api.blogs.BlogManager;
+import org.briarproject.api.blogs.BlogSharingManager;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.BlogInvitationReceivedEvent;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sharing.SharingInvitationItem;
+import org.briarproject.api.sync.ClientId;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+
+public class BlogInvitationControllerImpl
+		extends InvitationControllerImpl<SharingInvitationItem>
+		implements BlogInvitationController {
+
+	private final BlogManager blogManager;
+	private final BlogSharingManager blogSharingManager;
+
+	@Inject
+	BlogInvitationControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			LifecycleManager lifecycleManager, EventBus eventBus,
+			BlogManager blogManager, BlogSharingManager blogSharingManager) {
+		super(dbExecutor, lifecycleManager, eventBus);
+		this.blogManager = blogManager;
+		this.blogSharingManager = blogSharingManager;
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		super.eventOccurred(e);
+
+		if (e instanceof BlogInvitationReceivedEvent) {
+			LOG.info("Blog invitation received, reloading");
+			listener.loadInvitations(false);
+		}
+	}
+
+	@Override
+	protected ClientId getShareableClientId() {
+		return blogManager.getClientId();
+	}
+
+	@Override
+	protected Collection<SharingInvitationItem> getInvitations() throws DbException {
+		return blogSharingManager.getInvitations();
+	}
+
+	@Override
+	public void respondToInvitation(final SharingInvitationItem item,
+			final boolean accept,
+			final ResultExceptionHandler<Void, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Blog f = (Blog) item.getShareable();
+					for (Contact c : item.getNewSharers()) {
+						// TODO: What happens if a contact has been removed?
+						blogSharingManager.respondToInvitation(f, c, accept);
+					}
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/SharingStatusBlogActivity.java b/briar-android/src/org/briarproject/android/sharing/BlogSharingStatusActivity.java
similarity index 93%
rename from briar-android/src/org/briarproject/android/sharing/SharingStatusBlogActivity.java
rename to briar-android/src/org/briarproject/android/sharing/BlogSharingStatusActivity.java
index 5c7be6deea8fe71ce783d8315b5e6fe406b200a4..fed2c2a5af49d217e6c9b1b8dfe9733a3b38b2d9 100644
--- a/briar-android/src/org/briarproject/android/sharing/SharingStatusBlogActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/BlogSharingStatusActivity.java
@@ -9,7 +9,7 @@ import java.util.Collection;
 
 import javax.inject.Inject;
 
-public class SharingStatusBlogActivity extends SharingStatusActivity {
+public class BlogSharingStatusActivity extends SharingStatusActivity {
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject
diff --git a/briar-android/src/org/briarproject/android/sharing/ForumInvitationActivity.java b/briar-android/src/org/briarproject/android/sharing/ForumInvitationActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..37dccd442617edd04b0d61e33bb26fdb216c7f3d
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/ForumInvitationActivity.java
@@ -0,0 +1,46 @@
+package org.briarproject.android.sharing;
+
+import android.content.Context;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.api.sharing.SharingInvitationItem;
+
+import javax.inject.Inject;
+
+import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
+
+public class ForumInvitationActivity
+		extends InvitationActivity<SharingInvitationItem> {
+
+	@Inject
+	ForumInvitationController controller;
+
+	@Override
+	public void injectActivity(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	protected InvitationController<SharingInvitationItem> getController() {
+		return controller;
+	}
+
+	@Override
+	protected InvitationAdapter<SharingInvitationItem, ?> getAdapter(
+			Context ctx,
+			InvitationClickListener<SharingInvitationItem> listener) {
+		return new SharingInvitationAdapter(ctx, listener);
+	}
+
+	@Override
+	protected int getAcceptRes() {
+		return R.string.forum_joined_toast;
+	}
+
+	@Override
+	protected int getDeclineRes() {
+		return R.string.forum_declined_toast;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/ForumInvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/ForumInvitationAdapter.java
deleted file mode 100644
index cfe914a9c2949f7b74b42dc508232f2aae2f865a..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/sharing/ForumInvitationAdapter.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.briarproject.android.sharing;
-
-import android.content.Context;
-
-import org.briarproject.api.forum.Forum;
-import org.briarproject.api.sharing.InvitationItem;
-
-class ForumInvitationAdapter extends InvitationAdapter {
-
-	ForumInvitationAdapter(Context ctx, AvailableForumClickListener listener) {
-		super(ctx, listener);
-	}
-
-	@Override
-	public void onBindViewHolder(InvitationsViewHolder ui, int position) {
-		super.onBindViewHolder(ui, position);
-		InvitationItem item = getItemAt(position);
-		if (item == null) return;
-
-		Forum forum = (Forum) item.getShareable();
-
-		ui.avatar.setText(forum.getName().substring(0, 1));
-		ui.avatar.setBackgroundBytes(item.getShareable().getId().getBytes());
-
-		ui.name.setText(forum.getName());
-	}
-
-	@Override
-	public int compare(InvitationItem o1, InvitationItem o2) {
-		return String.CASE_INSENSITIVE_ORDER
-				.compare(((Forum) o1.getShareable()).getName(),
-						((Forum) o2.getShareable()).getName());
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/sharing/ForumInvitationController.java b/briar-android/src/org/briarproject/android/sharing/ForumInvitationController.java
new file mode 100644
index 0000000000000000000000000000000000000000..74ee19ee27f25a5ab6c0c0320861d3028378aa01
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/ForumInvitationController.java
@@ -0,0 +1,7 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.api.sharing.SharingInvitationItem;
+
+public interface ForumInvitationController
+		extends InvitationController<SharingInvitationItem> {
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/ForumInvitationControllerImpl.java b/briar-android/src/org/briarproject/android/sharing/ForumInvitationControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..4418900b821007374e46e6aca24fe70364e19ba9
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/ForumInvitationControllerImpl.java
@@ -0,0 +1,83 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.ForumInvitationReceivedEvent;
+import org.briarproject.api.forum.Forum;
+import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sharing.SharingInvitationItem;
+import org.briarproject.api.sync.ClientId;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+
+public class ForumInvitationControllerImpl
+		extends InvitationControllerImpl<SharingInvitationItem>
+		implements ForumInvitationController {
+
+	private final ForumManager forumManager;
+	private final ForumSharingManager forumSharingManager;
+
+	@Inject
+	ForumInvitationControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			LifecycleManager lifecycleManager, EventBus eventBus,
+			ForumManager forumManager,
+			ForumSharingManager forumSharingManager) {
+		super(dbExecutor, lifecycleManager, eventBus);
+		this.forumManager = forumManager;
+		this.forumSharingManager = forumSharingManager;
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		super.eventOccurred(e);
+
+		if (e instanceof ForumInvitationReceivedEvent) {
+			LOG.info("Forum invitation received, reloading");
+			listener.loadInvitations(false);
+		}
+	}
+
+	@Override
+	protected ClientId getShareableClientId() {
+		return forumManager.getClientId();
+	}
+
+	@Override
+	protected Collection<SharingInvitationItem> getInvitations() throws DbException {
+		return forumSharingManager.getInvitations();
+	}
+
+	@Override
+	public void respondToInvitation(final SharingInvitationItem item,
+			final boolean accept,
+			final ResultExceptionHandler<Void, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Forum f = (Forum) item.getShareable();
+					for (Contact c : item.getNewSharers()) {
+						// TODO: What happens if a contact has been removed?
+						forumSharingManager.respondToInvitation(f, c, accept);
+					}
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/SharingStatusForumActivity.java b/briar-android/src/org/briarproject/android/sharing/ForumSharingStatusActivity.java
similarity index 93%
rename from briar-android/src/org/briarproject/android/sharing/SharingStatusForumActivity.java
rename to briar-android/src/org/briarproject/android/sharing/ForumSharingStatusActivity.java
index ef3b333a0a41b62a071069613106902ef2e647a4..be51ab2cf4648090fc4c0d23bdfc0fbc642578fb 100644
--- a/briar-android/src/org/briarproject/android/sharing/SharingStatusForumActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/ForumSharingStatusActivity.java
@@ -9,7 +9,7 @@ import java.util.Collection;
 
 import javax.inject.Inject;
 
-public class SharingStatusForumActivity extends SharingStatusActivity {
+public class ForumSharingStatusActivity extends SharingStatusActivity {
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java b/briar-android/src/org/briarproject/android/sharing/InvitationActivity.java
similarity index 54%
rename from briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
rename to briar-android/src/org/briarproject/android/sharing/InvitationActivity.java
index 772be9bf605e7dd472ffb152a2f395b360147988..5a17037134a6174dbdb114e118a8f01a47d38351 100644
--- a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationActivity.java
@@ -2,39 +2,34 @@ package org.briarproject.android.sharing;
 
 import android.content.Context;
 import android.os.Bundle;
-import android.support.annotation.CallSuper;
+import android.support.annotation.StringRes;
 import android.support.v7.widget.LinearLayoutManager;
 import android.widget.Toast;
 
 import org.briarproject.R;
 import org.briarproject.android.BriarActivity;
+import org.briarproject.android.controller.handler.UiResultExceptionHandler;
+import org.briarproject.android.sharing.InvitationController.InvitationListener;
 import org.briarproject.android.view.BriarRecyclerView;
-import org.briarproject.api.event.ContactRemovedEvent;
-import org.briarproject.api.event.Event;
-import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.EventListener;
+import org.briarproject.api.db.DbException;
 import org.briarproject.api.sharing.InvitationItem;
 
 import java.util.Collection;
 import java.util.logging.Logger;
 
-import javax.inject.Inject;
-
 import static android.widget.Toast.LENGTH_SHORT;
-import static org.briarproject.android.sharing.InvitationAdapter.AvailableForumClickListener;
+import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
 
-abstract class InvitationsActivity extends BriarActivity
-		implements EventListener, AvailableForumClickListener {
+public abstract class InvitationActivity<I extends InvitationItem>
+		extends BriarActivity
+		implements InvitationListener, InvitationClickListener<I> {
 
 	protected static final Logger LOG =
-			Logger.getLogger(InvitationsActivity.class.getName());
+			Logger.getLogger(InvitationActivity.class.getName());
 
-	protected InvitationAdapter adapter;
+	private InvitationAdapter<I, ?> adapter;
 	private BriarRecyclerView list;
 
-	@Inject
-	EventBus eventBus;
-
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(state);
@@ -42,7 +37,6 @@ abstract class InvitationsActivity extends BriarActivity
 		setContentView(R.layout.list);
 
 		adapter = getAdapter(this, this);
-
 		list = (BriarRecyclerView) findViewById(R.id.list);
 		if (list != null) {
 			list.setLayoutManager(new LinearLayoutManager(this));
@@ -50,32 +44,24 @@ abstract class InvitationsActivity extends BriarActivity
 		}
 	}
 
+	abstract protected InvitationAdapter<I, ?> getAdapter(Context ctx,
+			InvitationClickListener<I> listener);
+
 	@Override
 	public void onStart() {
 		super.onStart();
-		eventBus.addListener(this);
 		loadInvitations(false);
 	}
 
 	@Override
 	public void onStop() {
 		super.onStop();
-		eventBus.removeListener(this);
 		adapter.clear();
 		list.showProgressBar();
 	}
 
 	@Override
-	@CallSuper
-	public void eventOccurred(Event e) {
-		if (e instanceof ContactRemovedEvent) {
-			LOG.info("Contact removed, reloading...");
-			loadInvitations(true);
-		}
-	}
-
-	@Override
-	public void onItemClick(InvitationItem item, boolean accept) {
+	public void onItemClick(I item, boolean accept) {
 		respondToInvitation(item, accept);
 
 		// show toast
@@ -91,26 +77,58 @@ abstract class InvitationsActivity extends BriarActivity
 		}
 	}
 
-	abstract protected InvitationAdapter getAdapter(Context ctx,
-			AvailableForumClickListener listener);
+	@Override
+	public void loadInvitations(final boolean clear) {
+		final int revision = adapter.getRevision();
+		getController().loadInvitations(clear,
+				new UiResultExceptionHandler<Collection<I>, DbException>(
+						this) {
+					@Override
+					public void onResultUi(Collection<I> items) {
+						displayInvitations(revision, items, clear);
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO proper error handling
+						finish();
+					}
+				});
+	}
+
+	abstract protected InvitationController<I> getController();
+
+	protected void respondToInvitation(final I item,
+			final boolean accept) {
+		getController().respondToInvitation(item, accept,
+				new UiResultExceptionHandler<Void, DbException>(this) {
+					@Override
+					public void onResultUi(Void result) {
 
-	abstract protected void loadInvitations(boolean clear);
+					}
 
-	abstract protected void respondToInvitation(final InvitationItem item,
-			final boolean accept);
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO proper error handling
+						finish();
+					}
+				});
+	}
 
+	@StringRes
 	abstract protected int getAcceptRes();
 
+	@StringRes
 	abstract protected int getDeclineRes();
 
 	protected void displayInvitations(final int revision,
-			final Collection<InvitationItem> invitations, final boolean clear) {
+			final Collection<I> invitations, final boolean clear) {
 		runOnUiThreadUnlessDestroyed(new Runnable() {
 			@Override
 			public void run() {
 				if (invitations.isEmpty()) {
 					LOG.info("No more invitations available, finishing");
-					finish();
+					supportFinishAfterTransition();
 				} else if (revision == adapter.getRevision()) {
 					adapter.incrementRevision();
 					if (clear) adapter.setItems(invitations);
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java
index db35fd55bc4dbd0ee3a416d6112167eee42514f1..5afd24c07c66b2750810dc195a6505dcc44e234e 100644
--- a/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java
@@ -1,111 +1,51 @@
 package org.briarproject.android.sharing;
 
 import android.content.Context;
-import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.TextView;
 
 import org.briarproject.R;
 import org.briarproject.android.util.BriarAdapter;
-import org.briarproject.android.view.TextAvatarView;
-import org.briarproject.api.contact.Contact;
 import org.briarproject.api.sharing.InvitationItem;
-import org.briarproject.util.StringUtils;
 
-import java.util.ArrayList;
-import java.util.Collection;
+public abstract class InvitationAdapter<I extends InvitationItem, VH extends InvitationViewHolder<I>>
+		extends BriarAdapter<I, VH> {
 
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
+	private final InvitationClickListener<I> listener;
 
-abstract class InvitationAdapter extends
-		BriarAdapter<InvitationItem, InvitationAdapter.InvitationsViewHolder> {
-
-	private final AvailableForumClickListener listener;
-
-	InvitationAdapter(Context ctx, AvailableForumClickListener listener) {
-		super(ctx, InvitationItem.class);
+	public InvitationAdapter(Context ctx, Class<I> c,
+			InvitationClickListener<I> listener) {
+		super(ctx, c);
 		this.listener = listener;
 	}
 
-	@Override
-	public InvitationsViewHolder onCreateViewHolder(ViewGroup parent,
-			int viewType) {
-
-		View v = LayoutInflater.from(ctx)
-				.inflate(R.layout.list_item_invitations, parent,  false);
-		return new InvitationsViewHolder(v);
+	protected View getView(ViewGroup parent) {
+		return LayoutInflater.from(ctx)
+				.inflate(R.layout.list_item_invitations, parent, false);
 	}
 
 	@Override
-	public void onBindViewHolder(InvitationsViewHolder ui, int position) {
-		final InvitationItem item = getItemAt(position);
+	public void onBindViewHolder(VH ui, int position) {
+		final I item = getItemAt(position);
 		if (item == null) return;
-
-		Collection<String> names = new ArrayList<>();
-		for (Contact c : item.getNewSharers())
-			names.add(c.getAuthor().getName());
-		ui.sharedBy.setText(ctx.getString(R.string.shared_by_format,
-				StringUtils.join(names, ", ")));
-
-		if (item.isSubscribed()) {
-			ui.subscribed.setVisibility(VISIBLE);
-		} else {
-			ui.subscribed.setVisibility(GONE);
-		}
-
-		ui.accept.setOnClickListener(new View.OnClickListener() {
-			@Override
-			public void onClick(View v) {
-				listener.onItemClick(item, true);
-			}
-		});
-		ui.decline.setOnClickListener(new View.OnClickListener() {
-			@Override
-			public void onClick(View v) {
-				listener.onItemClick(item, false);
-			}
-		});
+		ui.onBind(item, listener);
 	}
 
 	@Override
-	public boolean areContentsTheSame(InvitationItem oldItem,
-			InvitationItem newItem) {
-		return oldItem.isSubscribed() == newItem.isSubscribed() &&
-				oldItem.getNewSharers().equals(newItem.getNewSharers());
+	public boolean areItemsTheSame(I oldItem, I newItem) {
+		return oldItem.getShareable().equals(newItem.getShareable());
 	}
 
 	@Override
-	public boolean areItemsTheSame(InvitationItem oldItem,
-			InvitationItem newItem) {
-		return oldItem.getShareable().equals(newItem.getShareable());
+	public int compare(I o1, I o2) {
+		return String.CASE_INSENSITIVE_ORDER
+				.compare((o1.getShareable()).getName(),
+						(o2.getShareable()).getName());
 	}
 
-	static class InvitationsViewHolder extends RecyclerView.ViewHolder {
-
-		final TextAvatarView avatar;
-		final TextView name;
-		private final TextView sharedBy;
-		final TextView subscribed;
-		private final Button accept;
-		private final Button decline;
-
-		private InvitationsViewHolder(View v) {
-			super(v);
-
-			avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
-			name = (TextView) v.findViewById(R.id.forumNameView);
-			sharedBy = (TextView) v.findViewById(R.id.sharedByView);
-			subscribed = (TextView) v.findViewById(R.id.forumSubscribedView);
-			accept = (Button) v.findViewById(R.id.acceptButton);
-			decline = (Button) v.findViewById(R.id.declineButton);
-		}
+	public interface InvitationClickListener<I> {
+		void onItemClick(I item, boolean accept);
 	}
 
-	interface AvailableForumClickListener {
-		void onItemClick(InvitationItem item, boolean accept);
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationController.java b/briar-android/src/org/briarproject/android/sharing/InvitationController.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ea5c88e362d7ca84d6338e7e01bab1cd60592d4
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationController.java
@@ -0,0 +1,25 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.sharing.InvitationItem;
+
+import java.util.Collection;
+
+public interface InvitationController<I extends InvitationItem>
+		extends ActivityLifecycleController {
+
+	void loadInvitations(boolean clear,
+			ResultExceptionHandler<Collection<I>, DbException> handler);
+
+	void respondToInvitation(I item, boolean accept,
+			ResultExceptionHandler<Void, DbException> handler);
+
+	interface InvitationListener {
+
+		void loadInvitations(boolean clear);
+
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationControllerImpl.java b/briar-android/src/org/briarproject/android/sharing/InvitationControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..b1b52aca58da293c7fe0a19fc9b488fe9a4242d2
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationControllerImpl.java
@@ -0,0 +1,116 @@
+package org.briarproject.android.sharing;
+
+import android.app.Activity;
+import android.support.annotation.CallSuper;
+
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.ContactRemovedEvent;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.GroupAddedEvent;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sharing.InvitationItem;
+import org.briarproject.api.sync.ClientId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+public abstract class InvitationControllerImpl<I extends InvitationItem>
+		extends DbControllerImpl
+		implements InvitationController<I>, EventListener {
+
+	protected static final Logger LOG =
+			Logger.getLogger(InvitationControllerImpl.class.getName());
+
+	private final EventBus eventBus;
+	protected InvitationListener listener;
+
+	public InvitationControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			LifecycleManager lifecycleManager, EventBus eventBus) {
+		super(dbExecutor, lifecycleManager);
+		this.eventBus = eventBus;
+	}
+
+	@Override
+	public void onActivityCreate(Activity activity) {
+		listener = (InvitationListener) activity;
+	}
+
+	@Override
+	public void onActivityStart() {
+		eventBus.addListener(this);
+	}
+
+	@Override
+	public void onActivityStop() {
+		eventBus.removeListener(this);
+	}
+
+	@Override
+	public void onActivityDestroy() {
+
+	}
+
+	@CallSuper
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof ContactRemovedEvent) {
+			LOG.info("Contact removed, reloading...");
+			listener.loadInvitations(true);
+		} else if (e instanceof GroupAddedEvent) {
+			GroupAddedEvent g = (GroupAddedEvent) e;
+			ClientId cId = g.getGroup().getClientId();
+			if (cId.equals(getShareableClientId())) {
+				LOG.info("Group added, reloading");
+				listener.loadInvitations(false);
+			}
+		} else if (e instanceof GroupRemovedEvent) {
+			GroupRemovedEvent g = (GroupRemovedEvent) e;
+			ClientId cId = g.getGroup().getClientId();
+			if (cId.equals(getShareableClientId())) {
+				LOG.info("Group removed, reloading");
+				listener.loadInvitations(false);
+			}
+		}
+	}
+
+	protected abstract ClientId getShareableClientId();
+
+	@Override
+	public void loadInvitations(final boolean clear,
+			final ResultExceptionHandler<Collection<I>, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				Collection<I> invitations = new ArrayList<>();
+				try {
+					long now = System.currentTimeMillis();
+					invitations.addAll(getInvitations());
+					long duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info(
+								"Loading invitations took " + duration + " ms");
+					handler.onResult(invitations);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+	@DatabaseExecutor
+	protected abstract Collection<I> getInvitations() throws DbException;
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationViewHolder.java b/briar-android/src/org/briarproject/android/sharing/InvitationViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c8337c13c3271ac3eb95d601f6bb45739c55e97
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationViewHolder.java
@@ -0,0 +1,69 @@
+package org.briarproject.android.sharing;
+
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
+import org.briarproject.android.view.TextAvatarView;
+import org.briarproject.api.sharing.InvitationItem;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+public class InvitationViewHolder<I extends InvitationItem>
+		extends RecyclerView.ViewHolder {
+
+	private final TextAvatarView avatar;
+	private final TextView name;
+	protected final TextView sharedBy;
+	private final TextView subscribed;
+	private final Button accept;
+	private final Button decline;
+
+	public InvitationViewHolder(View v) {
+		super(v);
+
+		avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
+		name = (TextView) v.findViewById(R.id.forumNameView);
+		sharedBy = (TextView) v.findViewById(R.id.sharedByView);
+		subscribed = (TextView) v.findViewById(R.id.forumSubscribedView);
+		accept = (Button) v.findViewById(R.id.acceptButton);
+		decline = (Button) v.findViewById(R.id.declineButton);
+	}
+
+	@CallSuper
+	public void onBind(@Nullable final I item,
+			final InvitationClickListener<I> listener) {
+		if (item == null) return;
+
+		avatar.setText(item.getShareable().getName().substring(0, 1));
+		avatar.setBackgroundBytes(item.getShareable().getId().getBytes());
+
+		name.setText(item.getShareable().getName());
+
+		if (item.isSubscribed()) {
+			subscribed.setVisibility(VISIBLE);
+		} else {
+			subscribed.setVisibility(GONE);
+		}
+
+		accept.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				listener.onItemClick(item, true);
+			}
+		});
+		decline.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				listener.onItemClick(item, false);
+			}
+		});
+	}
+
+}
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsBlogActivity.java b/briar-android/src/org/briarproject/android/sharing/InvitationsBlogActivity.java
deleted file mode 100644
index 1b0e5f3bd7be5ce10c72991abb40792584d66bd0..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/sharing/InvitationsBlogActivity.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package org.briarproject.android.sharing;
-
-import android.content.Context;
-
-import org.briarproject.R;
-import org.briarproject.android.ActivityComponent;
-import org.briarproject.api.blogs.Blog;
-import org.briarproject.api.blogs.BlogManager;
-import org.briarproject.api.blogs.BlogSharingManager;
-import org.briarproject.api.contact.Contact;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.event.BlogInvitationReceivedEvent;
-import org.briarproject.api.event.Event;
-import org.briarproject.api.event.GroupAddedEvent;
-import org.briarproject.api.event.GroupRemovedEvent;
-import org.briarproject.api.sharing.InvitationItem;
-import org.briarproject.api.sync.ClientId;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-import javax.inject.Inject;
-
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.android.sharing.InvitationAdapter.AvailableForumClickListener;
-
-public class InvitationsBlogActivity extends InvitationsActivity {
-
-	// Fields that are accessed from background threads must be volatile
-	@Inject
-	volatile BlogManager blogManager;
-	@Inject
-	volatile BlogSharingManager blogSharingManager;
-
-	@Override
-	public void injectActivity(ActivityComponent component) {
-		component.inject(this);
-	}
-
-	@Override
-	public void eventOccurred(Event e) {
-		super.eventOccurred(e);
-
-		if (e instanceof GroupAddedEvent) {
-			GroupAddedEvent g = (GroupAddedEvent) e;
-			ClientId cId = g.getGroup().getClientId();
-			if (cId.equals(blogManager.getClientId())) {
-				LOG.info("Blog added, reloading");
-				loadInvitations(false);
-			}
-		} else if (e instanceof GroupRemovedEvent) {
-			GroupRemovedEvent g = (GroupRemovedEvent) e;
-			ClientId cId = g.getGroup().getClientId();
-			if (cId.equals(blogManager.getClientId())) {
-				LOG.info("Blog removed, reloading");
-				loadInvitations(false);
-			}
-		} else if (e instanceof BlogInvitationReceivedEvent) {
-			LOG.info("Blog invitation received, reloading");
-			loadInvitations(false);
-		}
-	}
-
-	@Override
-	protected InvitationAdapter getAdapter(Context ctx,
-			AvailableForumClickListener listener) {
-		return new BlogInvitationAdapter(ctx, listener);
-	}
-
-	@Override
-	protected void loadInvitations(final boolean clear) {
-		final int revision = adapter.getRevision();
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					Collection<InvitationItem> invitations = new ArrayList<>();
-					long now = System.currentTimeMillis();
-					invitations.addAll(blogSharingManager.getInvitations());
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Load took " + duration + " ms");
-					displayInvitations(revision, invitations, clear);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	@Override
-	protected void respondToInvitation(final InvitationItem item,
-			final boolean accept) {
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					Blog b = (Blog) item.getShareable();
-					for (Contact c : item.getNewSharers()) {
-						// TODO: What happens if a contact has been removed?
-						blogSharingManager.respondToInvitation(b, c, accept);
-					}
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	@Override
-	protected int getAcceptRes() {
-		return R.string.blogs_sharing_joined_toast;
-	}
-
-	@Override
-	protected int getDeclineRes() {
-		return R.string.blogs_sharing_declined_toast;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsForumActivity.java b/briar-android/src/org/briarproject/android/sharing/InvitationsForumActivity.java
deleted file mode 100644
index d64dab69f04af496aa5469aa79464c8eaa82d5a9..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/sharing/InvitationsForumActivity.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package org.briarproject.android.sharing;
-
-import android.content.Context;
-
-import org.briarproject.R;
-import org.briarproject.android.ActivityComponent;
-import org.briarproject.api.contact.Contact;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.event.Event;
-import org.briarproject.api.event.ForumInvitationReceivedEvent;
-import org.briarproject.api.event.GroupAddedEvent;
-import org.briarproject.api.event.GroupRemovedEvent;
-import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.forum.ForumSharingManager;
-import org.briarproject.api.sharing.InvitationItem;
-import org.briarproject.api.sync.ClientId;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-import javax.inject.Inject;
-
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.android.sharing.InvitationAdapter.AvailableForumClickListener;
-
-public class InvitationsForumActivity extends InvitationsActivity {
-
-	// Fields that are accessed from background threads must be volatile
-	@Inject
-	volatile ForumManager forumManager;
-	@Inject
-	volatile ForumSharingManager forumSharingManager;
-
-	@Override
-	public void injectActivity(ActivityComponent component) {
-		component.inject(this);
-	}
-
-	@Override
-	public void eventOccurred(Event e) {
-		super.eventOccurred(e);
-
-		if (e instanceof GroupAddedEvent) {
-			GroupAddedEvent g = (GroupAddedEvent) e;
-			ClientId cId = g.getGroup().getClientId();
-			if (cId.equals(forumManager.getClientId())) {
-				LOG.info("Forum added, reloading");
-				loadInvitations(false);
-			}
-		} else if (e instanceof GroupRemovedEvent) {
-			GroupRemovedEvent g = (GroupRemovedEvent) e;
-			ClientId cId = g.getGroup().getClientId();
-			if (cId.equals(forumManager.getClientId())) {
-				LOG.info("Forum removed, reloading");
-				loadInvitations(false);
-			}
-		} else if (e instanceof ForumInvitationReceivedEvent) {
-			LOG.info("Forum invitation received, reloading");
-			loadInvitations(false);
-		}
-	}
-
-	@Override
-	protected InvitationAdapter getAdapter(Context ctx,
-			AvailableForumClickListener listener) {
-		return new ForumInvitationAdapter(ctx, listener);
-	}
-
-	@Override
-	protected void loadInvitations(final boolean clear) {
-		final int revision = adapter.getRevision();
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					Collection<InvitationItem> invitations = new ArrayList<>();
-					long now = System.currentTimeMillis();
-					invitations.addAll(forumSharingManager.getInvitations());
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Load took " + duration + " ms");
-					displayInvitations(revision, invitations, clear);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	@Override
-	protected void respondToInvitation(final InvitationItem item,
-			final boolean accept) {
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					Forum f = (Forum) item.getShareable();
-					for (Contact c : item.getNewSharers()) {
-						// TODO: What happens if a contact has been removed?
-						forumSharingManager.respondToInvitation(f, c, accept);
-					}
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	@Override
-	protected int getAcceptRes() {
-		return R.string.forum_joined_toast;
-	}
-
-	@Override
-	protected int getDeclineRes() {
-		return R.string.forum_declined_toast;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/sharing/SharingInvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/SharingInvitationAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..2964c9b930db4f376bd028d2d5d3b0db98d8796d
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/SharingInvitationAdapter.java
@@ -0,0 +1,29 @@
+package org.briarproject.android.sharing;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import org.briarproject.api.sharing.SharingInvitationItem;
+
+class SharingInvitationAdapter extends
+		InvitationAdapter<SharingInvitationItem, SharingInvitationViewHolder> {
+
+	SharingInvitationAdapter(Context ctx, InvitationClickListener listener) {
+		super(ctx, SharingInvitationItem.class, listener);
+	}
+
+	@Override
+	public SharingInvitationViewHolder onCreateViewHolder(
+			ViewGroup parent,
+			int viewType) {
+		return new SharingInvitationViewHolder(getView(parent));
+	}
+
+	@Override
+	public boolean areContentsTheSame(SharingInvitationItem oldItem,
+			SharingInvitationItem newItem) {
+		return oldItem.isSubscribed() == newItem.isSubscribed() &&
+				oldItem.getNewSharers().equals(newItem.getNewSharers());
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/SharingInvitationViewHolder.java b/briar-android/src/org/briarproject/android/sharing/SharingInvitationViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..b10672468f1ada6f65c482c3c84576f4020527af
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/SharingInvitationViewHolder.java
@@ -0,0 +1,35 @@
+package org.briarproject.android.sharing;
+
+import android.support.annotation.Nullable;
+import android.view.View;
+
+import org.briarproject.R;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.sharing.SharingInvitationItem;
+import org.briarproject.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class SharingInvitationViewHolder
+		extends InvitationViewHolder<SharingInvitationItem> {
+
+	public SharingInvitationViewHolder(View v) {
+		super(v);
+	}
+
+	@Override
+	public void onBind(@Nullable final SharingInvitationItem item,
+			final InvitationAdapter.InvitationClickListener<SharingInvitationItem> listener) {
+		super.onBind(item, listener);
+		if (item == null) return;
+
+		Collection<String> names = new ArrayList<>();
+		for (Contact c : item.getNewSharers())
+			names.add(c.getAuthor().getName());
+		sharedBy.setText(
+				sharedBy.getContext().getString(R.string.shared_by_format,
+						StringUtils.join(names, ", ")));
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/clients/BaseMessage.java b/briar-api/src/org/briarproject/api/clients/BaseMessage.java
index 776bfcf5c93c694e2436b25f7ba5b8db43b5b986..fbfb56337789d1e2cb3dbee512fc5e170b04b84e 100644
--- a/briar-api/src/org/briarproject/api/clients/BaseMessage.java
+++ b/briar-api/src/org/briarproject/api/clients/BaseMessage.java
@@ -1,21 +1,26 @@
 package org.briarproject.api.clients;
 
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
 public abstract class BaseMessage {
 
 	private final Message message;
+	@Nullable
 	private final MessageId parent;
 
-	public BaseMessage(@NotNull Message message, @Nullable MessageId parent) {
+	public BaseMessage(Message message, @Nullable MessageId parent) {
 		this.message = message;
 		this.parent = parent;
 	}
 
-	@NotNull
 	public Message getMessage() {
 		return message;
 	}
diff --git a/briar-api/src/org/briarproject/api/event/BlogInvitationReceivedEvent.java b/briar-api/src/org/briarproject/api/event/BlogInvitationReceivedEvent.java
index 26a2ae097d81b4a7068661990191ca1db4aceb0e..2089e6c5d03c388c5bf8960bd5861eceddfadd60 100644
--- a/briar-api/src/org/briarproject/api/event/BlogInvitationReceivedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/BlogInvitationReceivedEvent.java
@@ -5,17 +5,11 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.sharing.InvitationRequest;
 
 public class BlogInvitationReceivedEvent extends
-		InvitationRequestReceivedEvent {
-
-	private final Blog blog;
+		InvitationRequestReceivedEvent<Blog> {
 
 	public BlogInvitationReceivedEvent(Blog blog, ContactId contactId,
 			InvitationRequest request) {
-		super(contactId, request);
-		this.blog = blog;
+		super(blog, contactId, request);
 	}
 
-	public Blog getBlog() {
-		return blog;
-	}
 }
diff --git a/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java b/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java
index 3077f91605de30b7a6d8bf105f7f058e754f995a..416979d2e400017a22adc914aafee71d992fb704 100644
--- a/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java
@@ -5,18 +5,11 @@ import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumInvitationRequest;
 
 public class ForumInvitationReceivedEvent extends
-		InvitationRequestReceivedEvent {
-
-	private final Forum forum;
+		InvitationRequestReceivedEvent<Forum> {
 
 	public ForumInvitationReceivedEvent(Forum forum, ContactId contactId,
 			ForumInvitationRequest request) {
-		super(contactId, request);
-		this.forum = forum;
-	}
-
-	public Forum getForum() {
-		return forum;
+		super(forum, contactId, request);
 	}
 
 }
diff --git a/briar-api/src/org/briarproject/api/event/GroupInvitationReceivedEvent.java b/briar-api/src/org/briarproject/api/event/GroupInvitationReceivedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8244a39248f621d4a829002afb232eed4f7208c
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/GroupInvitationReceivedEvent.java
@@ -0,0 +1,16 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.forum.ForumInvitationRequest;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.invitation.GroupInvitationRequest;
+
+public class GroupInvitationReceivedEvent extends
+		InvitationRequestReceivedEvent<PrivateGroup> {
+
+	public GroupInvitationReceivedEvent(PrivateGroup group, ContactId contactId,
+			GroupInvitationRequest request) {
+		super(group, contactId, request);
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/event/InvitationRequestReceivedEvent.java b/briar-api/src/org/briarproject/api/event/InvitationRequestReceivedEvent.java
index 184a88f5387a5e8fae71d4771c7d58e1893f1e50..f9bcfe1a5c792b2e9b6ef2490c53730224aa4e0a 100644
--- a/briar-api/src/org/briarproject/api/event/InvitationRequestReceivedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/InvitationRequestReceivedEvent.java
@@ -2,14 +2,18 @@ package org.briarproject.api.event;
 
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.sharing.InvitationRequest;
+import org.briarproject.api.sharing.Shareable;
 
-public abstract class InvitationRequestReceivedEvent extends Event {
+public abstract class InvitationRequestReceivedEvent<S extends Shareable>
+		extends Event {
 
+	private final S shareable;
 	private final ContactId contactId;
 	private final InvitationRequest request;
 
-	InvitationRequestReceivedEvent(ContactId contactId,
+	InvitationRequestReceivedEvent(S shareable, ContactId contactId,
 			InvitationRequest request) {
+		this.shareable = shareable;
 		this.contactId = contactId;
 		this.request = request;
 	}
@@ -21,4 +25,8 @@ public abstract class InvitationRequestReceivedEvent extends Event {
 	public InvitationRequest getRequest() {
 		return request;
 	}
+
+	public S getShareable() {
+		return shareable;
+	}
 }
diff --git a/briar-api/src/org/briarproject/api/forum/ForumInvitationRequest.java b/briar-api/src/org/briarproject/api/forum/ForumInvitationRequest.java
index 3e90116c0b63554169b2dbc382b4fb8367c13461..5d4c8d5542d29e2f2c6397fe71063f8938cee7db 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumInvitationRequest.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumInvitationRequest.java
@@ -5,16 +5,15 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.sharing.InvitationRequest;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
-import org.jetbrains.annotations.Nullable;
 
 public class ForumInvitationRequest extends InvitationRequest {
 
 	private final String forumName;
 
 	public ForumInvitationRequest(MessageId id, SessionId sessionId,
-			GroupId groupId, ContactId contactId, String forumName, String message,
-			boolean available, long time, boolean local, boolean sent,
-			boolean seen, boolean read) {
+			GroupId groupId, ContactId contactId, String forumName,
+			String message, boolean available, long time, boolean local,
+			boolean sent, boolean seen, boolean read) {
 
 		super(id, sessionId, groupId, contactId, message, available, time,
 				local, sent, seen, read);
diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java
index 22457de2eba5053e7fc189637bf1f22360061bf5..06f277460e7828d4231bbefc58b4a127e9b7be2c 100644
--- a/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java
+++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java
@@ -2,17 +2,22 @@ package org.briarproject.api.privategroup;
 
 import org.briarproject.api.clients.BaseMessage;
 import org.briarproject.api.identity.Author;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
 public class GroupMessage extends BaseMessage {
 
 	private final Author author;
 
-	public GroupMessage(@NotNull Message message, @Nullable MessageId parent,
-			@NotNull Author author) {
+	public GroupMessage(Message message, @Nullable MessageId parent,
+			Author author) {
 		super(message, parent);
 		this.author = author;
 	}
diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java
index 310611361b960acc44ef6b5e9f6d30207f488360..9b57d999108f822d2d777cef6ef391db9c7a6810 100644
--- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java
+++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java
@@ -3,6 +3,7 @@ package org.briarproject.api.privategroup;
 import org.briarproject.api.clients.NamedGroup;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sharing.Shareable;
 import org.briarproject.api.sync.Group;
 import org.jetbrains.annotations.NotNull;
 
@@ -10,7 +11,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-public class PrivateGroup extends NamedGroup {
+public class PrivateGroup extends NamedGroup implements Shareable {
 
 	private final Author author;
 
diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationConstants.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..a22272b22b2f3bb4549e7abebc4e65dc2677694a
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationConstants.java
@@ -0,0 +1,8 @@
+package org.briarproject.api.privategroup.invitation;
+
+public interface GroupInvitationConstants {
+
+	// Group Metadata Keys
+	String CONTACT_ID = "contactId";
+
+}
diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationItem.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..5cb23afee77575127930ea215d866d8179995fd9
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationItem.java
@@ -0,0 +1,27 @@
+package org.briarproject.api.privategroup.invitation;
+
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.sharing.InvitationItem;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class GroupInvitationItem extends InvitationItem<PrivateGroup> {
+
+	private final Contact creator;
+
+	public GroupInvitationItem(PrivateGroup shareable, boolean subscribed,
+			Contact creator) {
+		super(shareable, subscribed);
+
+		this.creator = creator;
+	}
+
+	public Contact getCreator() {
+		return creator;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..a5ca33dae143336d889b38e657f3c10ca6090a3b
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java
@@ -0,0 +1,48 @@
+package org.briarproject.api.privategroup.invitation;
+
+import org.briarproject.api.clients.MessageTracker;
+import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.sharing.InvitationMessage;
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+public interface GroupInvitationManager extends MessageTracker {
+
+	/** Returns the unique ID of the private group invitation client. */
+	ClientId getClientId();
+
+	/**
+	 * Sends an invitation to share the given forum with the given contact
+	 * and sends an optional message along with it.
+	 */
+	void sendInvitation(GroupId groupId, ContactId contactId,
+			String message)	throws DbException;
+
+	/**
+	 * Responds to a pending private group invitation
+	 */
+	void respondToInvitation(PrivateGroup g, Contact c, boolean accept)
+			throws DbException;
+
+	/**
+	 * Responds to a pending private group invitation
+	 */
+	void respondToInvitation(SessionId id, boolean accept) throws DbException;
+
+	/**
+	 * Returns all private group invitation messages related to the contact
+	 * identified by contactId.
+	 */
+	Collection<InvitationMessage> getInvitationMessages(
+			ContactId contactId) throws DbException;
+
+	/** Returns all private groups to which the user has been invited. */
+	Collection<GroupInvitationItem> getInvitations() throws DbException;
+
+}
diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationRequest.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationRequest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3115a6bc27d49a8c32e1ef28c03afc4b1cca4028
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationRequest.java
@@ -0,0 +1,39 @@
+package org.briarproject.api.privategroup.invitation;
+
+import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sharing.InvitationRequest;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+@Immutable
+@NotNullByDefault
+public class GroupInvitationRequest extends InvitationRequest {
+
+	private final String groupName;
+	private final Author creator;
+
+	public GroupInvitationRequest(MessageId id, SessionId sessionId,
+			GroupId groupId, Author creator, ContactId contactId,
+			String groupName, String message, boolean available, long time,
+			boolean local, boolean sent, boolean seen, boolean read) {
+		super(id, sessionId, groupId, contactId, message, available, time,
+				local, sent, seen, read);
+		this.groupName = groupName;
+		this.creator = creator;
+	}
+
+	public String getGroupName() {
+		return groupName;
+	}
+
+	public Author getCreator() {
+		return creator;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationResponse.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..dda5d554ac6ce1b8b78cc8a68f271038a695b457
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationResponse.java
@@ -0,0 +1,40 @@
+package org.briarproject.api.privategroup.invitation;
+
+import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sharing.InvitationResponse;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+@Immutable
+@NotNullByDefault
+public class GroupInvitationResponse extends InvitationResponse {
+
+	private final String groupName;
+	private final Author creator;
+
+	public GroupInvitationResponse(MessageId id, SessionId sessionId,
+			GroupId groupId, String groupName, Author creator,
+			ContactId contactId, boolean accept, long time, boolean local,
+			boolean sent, boolean seen, boolean read) {
+		super(id, sessionId, groupId, contactId, accept, time, local, sent,
+				seen, read);
+		this.groupName = groupName;
+		this.creator = creator;
+	}
+
+	public String getGroupName() {
+		return groupName;
+	}
+
+	public Author getCreator() {
+		return creator;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/sharing/InvitationItem.java b/briar-api/src/org/briarproject/api/sharing/InvitationItem.java
index bc180ea13f011cd895048d46c4b54ee79030462b..c81a4730d87bd00faf9acab6e481f0fcfbc325ec 100644
--- a/briar-api/src/org/briarproject/api/sharing/InvitationItem.java
+++ b/briar-api/src/org/briarproject/api/sharing/InvitationItem.java
@@ -1,32 +1,36 @@
 package org.briarproject.api.sharing;
 
-import org.briarproject.api.contact.Contact;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.GroupId;
 
-import java.util.Collection;
+import javax.annotation.concurrent.Immutable;
 
-public class InvitationItem {
+@Immutable
+@NotNullByDefault
+public abstract class InvitationItem<S extends Shareable> {
 
-	private final Shareable shareable;
+	private final S shareable;
 	private final boolean subscribed;
-	private final Collection<Contact> newSharers;
-
-	public InvitationItem(Shareable shareable, boolean subscribed,
-			Collection<Contact> newSharers) {
 
+	public InvitationItem(S shareable, boolean subscribed) {
 		this.shareable = shareable;
 		this.subscribed = subscribed;
-		this.newSharers = newSharers;
 	}
 
-	public Shareable getShareable() {
+	public S getShareable() {
 		return shareable;
 	}
 
+	public GroupId getId() {
+		return shareable.getId();
+	}
+
+	public String getName() {
+		return shareable.getName();
+	}
+
 	public boolean isSubscribed() {
 		return subscribed;
 	}
 
-	public Collection<Contact> getNewSharers() {
-		return newSharers;
-	}
 }
diff --git a/briar-api/src/org/briarproject/api/sharing/Shareable.java b/briar-api/src/org/briarproject/api/sharing/Shareable.java
index 13c11fdeeff5b39d3f775b293281bcfabc08d097..144b06dde1db37991a3258b4970d91dfbbfa7443 100644
--- a/briar-api/src/org/briarproject/api/sharing/Shareable.java
+++ b/briar-api/src/org/briarproject/api/sharing/Shareable.java
@@ -8,4 +8,7 @@ public interface Shareable {
 	GroupId getId();
 
 	Group getGroup();
+
+	String getName();
+
 }
diff --git a/briar-api/src/org/briarproject/api/sharing/SharingInvitationItem.java b/briar-api/src/org/briarproject/api/sharing/SharingInvitationItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..04b69f49e9cd4a65be8afd3805edcd42069a03f0
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/sharing/SharingInvitationItem.java
@@ -0,0 +1,27 @@
+package org.briarproject.api.sharing;
+
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+
+import java.util.Collection;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class SharingInvitationItem extends InvitationItem<Shareable> {
+
+	private final Collection<Contact> newSharers;
+
+	public SharingInvitationItem(Shareable shareable, boolean subscribed,
+			Collection<Contact> newSharers) {
+		super(shareable, subscribed);
+
+		this.newSharers = newSharers;
+	}
+
+	public Collection<Contact> getNewSharers() {
+		return newSharers;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/sharing/SharingManager.java b/briar-api/src/org/briarproject/api/sharing/SharingManager.java
index b4613cd94035ef209d7d82b24c83a6126a841453..c7b1c8004954c5af181b8a1cb092beb8f16adb84 100644
--- a/briar-api/src/org/briarproject/api/sharing/SharingManager.java
+++ b/briar-api/src/org/briarproject/api/sharing/SharingManager.java
@@ -1,6 +1,7 @@
 package org.briarproject.api.sharing;
 
 import org.briarproject.api.clients.MessageTracker;
+import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
@@ -30,7 +31,14 @@ public interface SharingManager<S extends Shareable> extends MessageTracker {
 			throws DbException;
 
 	/**
-	 * Returns all group sharing messages sent by the given contact.
+	 * Responds to a pending group invitation
+	 */
+	void respondToInvitation(SessionId id, boolean accept)
+			throws DbException;
+
+	/**
+	 * Returns all group sharing messages sent by the Contact
+	 * identified by contactId.
 	 */
 	Collection<InvitationMessage> getInvitationMessages(
 			ContactId contactId) throws DbException;
@@ -38,7 +46,7 @@ public interface SharingManager<S extends Shareable> extends MessageTracker {
 	/**
 	 * Returns all invitations to groups.
 	 */
-	Collection<InvitationItem> getInvitations() throws DbException;
+	Collection<SharingInvitationItem> getInvitations() throws DbException;
 
 	/**
 	 * Returns all contacts who are sharing the given group with us.
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java
index 562b95c308cf3d25ca63a982631a56bc72f14c9d..570f697a629d952be8790dd5b27e04e1a21a2a90 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java
@@ -1,17 +1,19 @@
 package org.briarproject.privategroup;
 
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.messaging.ConversationManager;
 import org.briarproject.api.privategroup.GroupMessageFactory;
 import org.briarproject.api.privategroup.PrivateGroupFactory;
 import org.briarproject.api.privategroup.PrivateGroupManager;
-import org.briarproject.api.sync.GroupFactory;
+import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
 import org.briarproject.api.sync.ValidationManager;
 import org.briarproject.api.system.Clock;
-
-import java.security.SecureRandom;
+import org.briarproject.privategroup.invitation.GroupInvitationManagerImpl;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -25,6 +27,8 @@ public class PrivateGroupModule {
 	public static class EagerSingletons {
 		@Inject
 		GroupMessageValidator groupMessageValidator;
+		@Inject
+		GroupInvitationManager groupInvitationManager;
 	}
 
 	@Provides
@@ -65,4 +69,22 @@ public class PrivateGroupModule {
 		return validator;
 	}
 
+	@Provides
+	@Singleton
+	GroupInvitationManager provideGroupInvitationManager(
+			LifecycleManager lifecycleManager, ContactManager contactManager,
+			GroupInvitationManagerImpl groupInvitationManager,
+			ConversationManager conversationManager,
+			ValidationManager validationManager) {
+
+		validationManager.registerIncomingMessageHook(
+				groupInvitationManager.getClientId(), groupInvitationManager);
+		lifecycleManager.registerClient(groupInvitationManager);
+		contactManager.registerAddContactHook(groupInvitationManager);
+		contactManager.registerRemoveContactHook(groupInvitationManager);
+		conversationManager.registerConversationClient(groupInvitationManager);
+
+		return groupInvitationManager;
+	}
+
 }
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..12477ab20b994595192b2e53b2bef17944e0f9c2
--- /dev/null
+++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
@@ -0,0 +1,141 @@
+package org.briarproject.privategroup.invitation;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.Client;
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.ContactGroupFactory;
+import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.data.MetadataParser;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.messaging.ConversationManager;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
+import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
+import org.briarproject.api.sharing.InvitationMessage;
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.Message;
+import org.briarproject.clients.ConversationClientImpl;
+import org.briarproject.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+import static org.briarproject.api.privategroup.invitation.GroupInvitationConstants.CONTACT_ID;
+
+public class GroupInvitationManagerImpl extends ConversationClientImpl
+		implements GroupInvitationManager, Client,
+		ContactManager.AddContactHook, ContactManager.RemoveContactHook,
+		ConversationManager.ConversationClient {
+
+	private static final ClientId CLIENT_ID =
+			new ClientId(StringUtils.fromHexString(
+					"B55231ABFC4A10666CD93D649B1D7F4F"
+							+ "016E65B87BB4C04F4E35613713DBCD13"));
+
+	private final ContactGroupFactory contactGroupFactory;
+	private final Group localGroup;
+
+	@Inject
+	protected GroupInvitationManagerImpl(DatabaseComponent db,
+			ClientHelper clientHelper, MetadataParser metadataParser,
+			ContactGroupFactory contactGroupFactory) {
+		super(db, clientHelper, metadataParser);
+		this.contactGroupFactory = contactGroupFactory;
+		localGroup = contactGroupFactory.createLocalGroup(getClientId());
+	}
+
+	@Override
+	public ClientId getClientId() {
+		return CLIENT_ID;
+	}
+
+	@Override
+	public void createLocalState(Transaction txn) throws DbException {
+		db.addGroup(txn, localGroup);
+		// Ensure we've set things up for any pre-existing contacts
+		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
+	}
+
+	@Override
+	public void addingContact(Transaction txn, Contact c) throws DbException {
+		try {
+			// Create a group to share with the contact
+			Group g = getContactGroup(c);
+			// Return if we've already set things up for this contact
+			if (db.containsGroup(txn, g.getId())) return;
+			// Store the group and share it with the contact
+			db.addGroup(txn, g);
+			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
+			// Attach the contact ID to the group
+			BdfDictionary meta = new BdfDictionary();
+			meta.put(CONTACT_ID, c.getId().getInt());
+			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	@Override
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		// remove the contact group (all messages will be removed with it)
+		db.removeGroup(txn, getContactGroup(c));
+	}
+
+	@Override
+	protected Group getContactGroup(Contact c) {
+		return contactGroupFactory.createContactGroup(getClientId(), c);
+	}
+
+	@Override
+	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
+			BdfDictionary meta) throws DbException, FormatException {
+		return false;
+	}
+
+	@Override
+	public void sendInvitation(GroupId groupId, ContactId contactId,
+			String message) throws DbException {
+
+	}
+
+	@Override
+	public void respondToInvitation(PrivateGroup g, Contact c, boolean accept)
+			throws DbException {
+
+	}
+
+	@Override
+	public void respondToInvitation(SessionId id, boolean accept)
+			throws DbException {
+
+	}
+
+	@Override
+	public Collection<InvitationMessage> getInvitationMessages(
+			ContactId contactId) throws DbException {
+		Collection<InvitationMessage> invitations =
+				new ArrayList<InvitationMessage>();
+
+		return invitations;
+	}
+
+	@Override
+	public Collection<GroupInvitationItem> getInvitations() throws DbException {
+		Collection<GroupInvitationItem> invitations =
+				new ArrayList<GroupInvitationItem>();
+
+		return invitations;
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java b/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
index a7c90d0a78f45edcf201803856d1cce078572517..a355018773d32db16252c9705b01f0faff0b4431 100644
--- a/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
@@ -25,7 +25,7 @@ import org.briarproject.api.event.Event;
 import org.briarproject.api.event.InvitationRequestReceivedEvent;
 import org.briarproject.api.event.InvitationResponseReceivedEvent;
 import org.briarproject.api.identity.LocalAuthor;
-import org.briarproject.api.sharing.InvitationItem;
+import org.briarproject.api.sharing.SharingInvitationItem;
 import org.briarproject.api.sharing.InvitationMessage;
 import org.briarproject.api.sharing.Shareable;
 import org.briarproject.api.sharing.SharingManager;
@@ -316,27 +316,23 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		try {
 			// find session state based on shareable
 			IS localState = getSessionStateForResponse(txn, f, c);
+			respondToInvitation(txn, localState, accept);
+			txn.setComplete();
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
 
-			// define action
-			InviteeSessionState.Action localAction;
-			if (accept) {
-				localAction = InviteeSessionState.Action.LOCAL_ACCEPT;
-			} else {
-				localAction = InviteeSessionState.Action.LOCAL_DECLINE;
-			}
-
-			// start engine and process its state update
-			InviteeEngine<IS, IR> engine =
-					new InviteeEngine<IS, IR>(getIRFactory(), clock);
-			StateUpdate<IS, BaseMessage> update =
-					engine.onLocalAction(localState, localAction);
-			processInviteeStateUpdate(txn, null, update);
-
-			// track message
-			// TODO handle this properly without engine hacks (#376)
-			long time = update.toSend.get(0).getTime();
-			trackMessage(txn, localState.getGroupId(), time, true);
+	@Override
+	public void respondToInvitation(SessionId id, boolean accept)
+			throws DbException {
 
+		Transaction txn = db.startTransaction(false);
+		try {
+			IS localState = (IS) getSessionState(txn, id, true);
+			respondToInvitation(txn, localState, accept);
 			txn.setComplete();
 		} catch (FormatException e) {
 			throw new DbException(e);
@@ -345,6 +341,29 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		}
 	}
 
+	private void respondToInvitation(Transaction txn, IS localState,
+			boolean accept) throws DbException, FormatException {
+		// define action
+		InviteeSessionState.Action localAction;
+		if (accept) {
+			localAction = InviteeSessionState.Action.LOCAL_ACCEPT;
+		} else {
+			localAction = InviteeSessionState.Action.LOCAL_DECLINE;
+		}
+
+		// start engine and process its state update
+		InviteeEngine<IS, IR> engine =
+				new InviteeEngine<IS, IR>(getIRFactory(), clock);
+		StateUpdate<IS, BaseMessage> update =
+				engine.onLocalAction(localState, localAction);
+		processInviteeStateUpdate(txn, null, update);
+
+		// track message
+		// TODO handle this properly without engine hacks (#376)
+		long time = update.toSend.get(0).getTime();
+		trackMessage(txn, localState.getGroupId(), time, true);
+	}
+
 	@Override
 	public Collection<InvitationMessage> getInvitationMessages(ContactId contactId)
 			throws DbException {
@@ -418,8 +437,8 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 	}
 
 	@Override
-	public Collection<InvitationItem> getInvitations() throws DbException {
-		List<InvitationItem> invitations = new ArrayList<InvitationItem>();
+	public Collection<SharingInvitationItem> getInvitations() throws DbException {
+		List<SharingInvitationItem> invitations = new ArrayList<SharingInvitationItem>();
 		Transaction txn = db.startTransaction(true);
 		try {
 			Set<S> shareables = new HashSet<S>();
@@ -445,8 +464,8 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 			for (S s : shareables) {
 				Collection<Contact> newS = newSharers.get(s.getId());
 				boolean subscribed = db.containsGroup(txn, s.getId());
-				InvitationItem invitation =
-						new InvitationItem(s, subscribed, newS);
+				SharingInvitationItem invitation =
+						new SharingInvitationItem(s, subscribed, newS);
 				invitations.add(invitation);
 			}
 			txn.setComplete();