diff --git a/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTestComponent.java
index e0a7ada60e1cfede6ca822e31135517d30531b04..5515094fb97cb88a6519fbe1757d864149c347b0 100644
--- a/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTestComponent.java
+++ b/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTestComponent.java
@@ -17,6 +17,7 @@ import org.briarproject.event.EventModule;
 import org.briarproject.forum.ForumModule;
 import org.briarproject.identity.IdentityModule;
 import org.briarproject.lifecycle.LifecycleModule;
+import org.briarproject.messaging.MessagingModule;
 import org.briarproject.properties.PropertiesModule;
 import org.briarproject.sharing.SharingModule;
 import org.briarproject.sync.SyncModule;
@@ -46,7 +47,8 @@ import dagger.Component;
 		SharingModule.class,
 		SyncModule.class,
 		SystemModule.class,
-		TransportModule.class
+		TransportModule.class,
+		MessagingModule.class
 })
 interface BlogSharingIntegrationTestComponent {
 
diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java
index 74c771488f832a608ebf66ee610a0e5fd6e1ddc2..fee35a0088438d7d5d04547f210002e58c3de28f 100644
--- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java
+++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java
@@ -17,6 +17,7 @@ import org.briarproject.event.EventModule;
 import org.briarproject.forum.ForumModule;
 import org.briarproject.identity.IdentityModule;
 import org.briarproject.lifecycle.LifecycleModule;
+import org.briarproject.messaging.MessagingModule;
 import org.briarproject.properties.PropertiesModule;
 import org.briarproject.sharing.SharingModule;
 import org.briarproject.sync.SyncModule;
@@ -46,7 +47,8 @@ import dagger.Component;
 		SharingModule.class,
 		SyncModule.class,
 		SystemModule.class,
-		TransportModule.class
+		TransportModule.class,
+		MessagingModule.class
 })
 interface ForumManagerTestComponent {
 
diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTestComponent.java
index 10dacd7c8041ea2a6ccd03987ccaaf4ad59a2fad..88b058807ce19031ffa8f311ffb8c42c2cd55708 100644
--- a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTestComponent.java
+++ b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTestComponent.java
@@ -21,6 +21,7 @@ import org.briarproject.event.EventModule;
 import org.briarproject.forum.ForumModule;
 import org.briarproject.identity.IdentityModule;
 import org.briarproject.lifecycle.LifecycleModule;
+import org.briarproject.messaging.MessagingModule;
 import org.briarproject.properties.PropertiesModule;
 import org.briarproject.sharing.SharingModule;
 import org.briarproject.sync.SyncModule;
@@ -50,7 +51,8 @@ import dagger.Component;
 		SharingModule.class,
 		SyncModule.class,
 		SystemModule.class,
-		TransportModule.class
+		TransportModule.class,
+		MessagingModule.class
 })
 interface ForumSharingIntegrationTestComponent {
 
diff --git a/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java
index c1f7077a497fa2d7c51269cda0ec504e15a75ebe..617fa1cee530f83f0125018fdda60e0ac80c71fe 100644
--- a/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java
+++ b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java
@@ -20,6 +20,7 @@ import org.briarproject.db.DatabaseModule;
 import org.briarproject.event.EventModule;
 import org.briarproject.identity.IdentityModule;
 import org.briarproject.lifecycle.LifecycleModule;
+import org.briarproject.messaging.MessagingModule;
 import org.briarproject.properties.PropertiesModule;
 import org.briarproject.sync.SyncModule;
 import org.briarproject.system.SystemModule;
@@ -46,7 +47,8 @@ import dagger.Component;
 		SyncModule.class,
 		SystemModule.class,
 		DataModule.class,
-		PropertiesModule.class
+		PropertiesModule.class,
+		MessagingModule.class
 })
 public interface IntroductionIntegrationTestComponent {
 
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index a5880d33686c970c759ea1f33008fd34e2c9b39d..ce051f5349f136428e6921607bca39516f24abe4 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -30,6 +30,7 @@ import org.briarproject.api.keyagreement.PayloadEncoder;
 import org.briarproject.api.keyagreement.PayloadParser;
 import org.briarproject.api.lifecycle.IoExecutor;
 import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.messaging.ConversationManager;
 import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessageFactory;
 import org.briarproject.api.plugins.ConnectionRegistry;
@@ -86,6 +87,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
 
 	ContactManager contactManager();
 
+	ConversationManager conversationManager();
+
 	MessagingManager messagingManager();
 
 	PrivateMessageFactory privateMessageFactory();
diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
index 966de01db949176bda374f03248e51174c1c0b88..7d47079e137bde40a5db8c649291f78938910286 100644
--- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
+++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
@@ -28,7 +28,7 @@ import org.briarproject.api.event.ForumPostReceivedEvent;
 import org.briarproject.api.event.IntroductionRequestReceivedEvent;
 import org.briarproject.api.event.IntroductionResponseReceivedEvent;
 import org.briarproject.api.event.IntroductionSucceededEvent;
-import org.briarproject.api.event.InvitationReceivedEvent;
+import org.briarproject.api.event.InvitationRequestReceivedEvent;
 import org.briarproject.api.event.InvitationResponseReceivedEvent;
 import org.briarproject.api.event.PrivateMessageReceivedEvent;
 import org.briarproject.api.event.SettingsUpdatedEvent;
@@ -235,8 +235,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 		} else if (e instanceof IntroductionResponseReceivedEvent) {
 			ContactId c = ((IntroductionResponseReceivedEvent) e).getContactId();
 			showNotificationForPrivateConversation(c);
-		} else if (e instanceof InvitationReceivedEvent) {
-			ContactId c = ((InvitationReceivedEvent) e).getContactId();
+		} else if (e instanceof InvitationRequestReceivedEvent) {
+			ContactId c = ((InvitationRequestReceivedEvent) e).getContactId();
 			showNotificationForPrivateConversation(c);
 		} else if (e instanceof InvitationResponseReceivedEvent) {
 			ContactId c = ((InvitationResponseReceivedEvent) e).getContactId();
diff --git a/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java b/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java
index b1ab777c442989c4735d3b2d210729d4482ba065..aa48714f77f086e031054e7d5929e5b70c11ed24 100644
--- a/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java
+++ b/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java
@@ -50,10 +50,8 @@ public abstract class BaseContactListAdapter<VH extends BaseContactListAdapter.B
 			}
 		});
 
-		if (item.getGroupId() != null) {
-			ViewCompat.setTransitionName(ui.avatar, "avatar" +
-					StringUtils.toHexString(item.getGroupId().getBytes()));
-		}
+		ViewCompat.setTransitionName(ui.avatar, "avatar" +
+				StringUtils.toHexString(item.getGroupId().getBytes()));
 	}
 
 	@Override
@@ -96,8 +94,9 @@ public abstract class BaseContactListAdapter<VH extends BaseContactListAdapter.B
 		int count = getItemCount();
 		for (int i = 0; i < count; i++) {
 			ContactListItem item = getItemAt(i);
-			if (item != null && item.getGroupId().equals(g))
+			if (item != null && item.getGroupId().equals(g)) {
 				return i;
+			}
 		}
 		return INVALID_POSITION; // Not found
 	}
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
index 5feb4d3a15a3014145aec79ac0d0e033e77770b1..22f77a841dc18a44aadf1c5772718188d4159b95 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
@@ -21,13 +21,12 @@ 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.blogs.BlogSharingManager;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.NoSuchContactException;
-import org.briarproject.api.event.ContactAddedEvent;
 import org.briarproject.api.event.ContactConnectedEvent;
 import org.briarproject.api.event.ContactDisconnectedEvent;
 import org.briarproject.api.event.ContactRemovedEvent;
@@ -37,24 +36,21 @@ import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.IntroductionRequestReceivedEvent;
 import org.briarproject.api.event.IntroductionResponseReceivedEvent;
-import org.briarproject.api.event.InvitationReceivedEvent;
+import org.briarproject.api.event.InvitationRequestReceivedEvent;
 import org.briarproject.api.event.InvitationResponseReceivedEvent;
 import org.briarproject.api.event.PrivateMessageReceivedEvent;
-import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
-import org.briarproject.api.introduction.IntroductionManager;
-import org.briarproject.api.introduction.IntroductionMessage;
 import org.briarproject.api.introduction.IntroductionRequest;
 import org.briarproject.api.introduction.IntroductionResponse;
-import org.briarproject.api.messaging.MessagingManager;
+import org.briarproject.api.messaging.ConversationManager;
 import org.briarproject.api.messaging.PrivateMessageHeader;
 import org.briarproject.api.plugins.ConnectionRegistry;
-import org.briarproject.api.sharing.InvitationMessage;
+import org.briarproject.api.sharing.InvitationRequest;
+import org.briarproject.api.sharing.InvitationResponse;
 import org.briarproject.api.sync.GroupId;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.logging.Logger;
 
@@ -88,13 +84,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 	@Inject
 	protected volatile IdentityManager identityManager;
 	@Inject
-	protected volatile MessagingManager messagingManager;
-	@Inject
-	protected volatile IntroductionManager introductionManager;
-	@Inject
-	protected volatile ForumSharingManager forumSharingManager;
-	@Inject
-	protected volatile BlogSharingManager blogSharingManager;
+	protected volatile ConversationManager conversationManager;
 
 	public static ContactListFragment newInstance() {
 
@@ -130,7 +120,6 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 				new ContactListAdapter.OnItemClickListener() {
 					@Override
 					public void onItemClick(View view, ContactListItem item) {
-
 						GroupId groupId = item.getGroupId();
 						Intent i = new Intent(getActivity(),
 								ConversationActivity.class);
@@ -215,15 +204,15 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 						try {
 							ContactId id = c.getId();
 							GroupId groupId =
-									messagingManager.getConversationId(id);
-							Collection<ConversationItem> messages =
-									getMessages(id);
+									conversationManager.getConversationId(id);
+							GroupCount count =
+									conversationManager.getGroupCount(id);
 							boolean connected =
 									connectionRegistry.isConnected(c.getId());
 							LocalAuthor localAuthor = identityManager
 									.getLocalAuthor(c.getLocalAuthorId());
 							contacts.add(new ContactListItem(c, localAuthor,
-									connected, groupId, messages));
+									connected, groupId, count));
 						} catch (NoSuchContactException e) {
 							// Continue
 						}
@@ -252,14 +241,9 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 
 	@Override
 	public void eventOccurred(Event e) {
-		if (e instanceof ContactAddedEvent) {
-			if (((ContactAddedEvent) e).isActive()) {
-				LOG.info("Contact added as active, reloading");
-				loadContacts();
-			}
-		} else if (e instanceof ContactStatusChangedEvent) {
+		if (e instanceof ContactStatusChangedEvent) {
 			LOG.info("Contact Status changed, reloading");
-			// TODO We can update the contact state without needing to reload
+			// is also broadcast when contact was added
 			loadContacts();
 		} else if (e instanceof ContactConnectedEvent) {
 			setConnected(((ContactConnectedEvent) e).getContactId(), true);
@@ -285,50 +269,20 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 					(IntroductionResponseReceivedEvent) e;
 			IntroductionResponse ir = m.getIntroductionResponse();
 			updateItem(m.getContactId(), ConversationItem.from(ir));
-		} else if (e instanceof InvitationReceivedEvent) {
-			LOG.info("Invitation received, reloading conversation...");
-			InvitationReceivedEvent m = (InvitationReceivedEvent) e;
-			reloadConversation(m.getContactId());
+		} else if (e instanceof InvitationRequestReceivedEvent) {
+			LOG.info("Invitation Request received, update contact");
+			InvitationRequestReceivedEvent m = (InvitationRequestReceivedEvent) e;
+			InvitationRequest ir = m.getRequest();
+			updateItem(m.getContactId(), ConversationItem.from(ir));
 		} else if (e instanceof InvitationResponseReceivedEvent) {
-			LOG.info("Invitation Response received, reloading ...");
+			LOG.info("Invitation Response received, update contact");
 			InvitationResponseReceivedEvent m =
 					(InvitationResponseReceivedEvent) e;
-			reloadConversation(m.getContactId());
+			InvitationResponse ir = m.getResponse();
+			updateItem(m.getContactId(), ConversationItem.from(ir));
 		}
 	}
 
-	private void reloadConversation(final ContactId c) {
-		listener.runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					Collection<ConversationItem> messages = getMessages(c);
-					updateItem(c, messages);
-				} catch (NoSuchContactException e) {
-					LOG.info("Contact removed");
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	private void updateItem(final ContactId c,
-			final Collection<ConversationItem> messages) {
-		listener.runOnUiThread(new Runnable() {
-			@Override
-			public void run() {
-				int position = adapter.findItemPosition(c);
-				ContactListItem item = adapter.getItemAt(position);
-				if (item != null) {
-					item.setMessages(messages);
-					adapter.updateItemAt(position, item);
-				}
-			}
-		});
-	}
-
 	private void updateItem(final ContactId c, final ConversationItem m) {
 		listener.runOnUiThread(new Runnable() {
 			@Override
@@ -382,50 +336,4 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 		});
 	}
 
-	// This needs to be called from the DB thread
-	// Do not call getActivty() here as it might return null
-	private Collection<ConversationItem> getMessages(ContactId id)
-			throws DbException {
-
-		long now = System.currentTimeMillis();
-
-		Collection<ConversationItem> messages = new ArrayList<>();
-
-		Collection<PrivateMessageHeader> headers =
-				messagingManager.getMessageHeaders(id);
-		for (PrivateMessageHeader h : headers) {
-			messages.add(ConversationItem.from(h));
-		}
-		long duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Loading message headers took " + duration + " ms");
-
-		now = System.currentTimeMillis();
-		Collection<IntroductionMessage> introductions =
-				introductionManager
-						.getIntroductionMessages(id);
-		for (IntroductionMessage m : introductions) {
-			messages.add(ConversationItem.from(m));
-		}
-		duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Loading introduction messages took " + duration + " ms");
-
-		now = System.currentTimeMillis();
-		Collection<InvitationMessage> forumInvitations =
-				forumSharingManager.getInvitationMessages(id);
-		for (InvitationMessage i : forumInvitations) {
-			messages.add(ConversationItem.from(i));
-		}
-		Collection<InvitationMessage> blogInvitations =
-				blogSharingManager.getInvitationMessages(id);
-		for (InvitationMessage i : blogInvitations) {
-			messages.add(ConversationItem.from(i));
-		}
-		duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Loading invitations took " + duration + " ms");
-
-		return messages;
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListItem.java b/briar-android/src/org/briarproject/android/contact/ContactListItem.java
index e436d4a9b230067ff294cffa28c823855dc97bb1..e90cdf939b14f6a582594bcc36dd36d4b2aa7909 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListItem.java
@@ -1,14 +1,14 @@
 package org.briarproject.android.contact;
 
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.GroupId;
-
-import java.util.Collection;
+import org.jetbrains.annotations.NotNull;
 
 import static org.briarproject.android.contact.ConversationItem.IncomingItem;
 
-// This class is not thread-safe
+// This class is NOT thread-safe
 public class ContactListItem {
 
 	private final Contact contact;
@@ -18,26 +18,16 @@ public class ContactListItem {
 	private long timestamp;
 	private int unread;
 
-	public ContactListItem(Contact contact, LocalAuthor localAuthor,
-			boolean connected,
-			GroupId groupId,
-			Collection<ConversationItem> messages) {
+	public ContactListItem(@NotNull Contact contact,
+			@NotNull LocalAuthor localAuthor, boolean connected,
+			@NotNull GroupId groupId, @NotNull GroupCount count) {
 		this.contact = contact;
 		this.localAuthor = localAuthor;
 		this.groupId = groupId;
 		this.connected = connected;
-		setMessages(messages);
-	}
-
-	void setMessages(Collection<ConversationItem> messages) {
-		empty = messages == null || messages.isEmpty();
-		timestamp = 0;
-		unread = 0;
-		if (!empty) {
-			for (ConversationItem i : messages) {
-				addMessage(i);
-			}
-		}
+		this.empty = count.getMsgCount() == 0;
+		this.unread = count.getUnreadCount();
+		this.timestamp = count.getLatestMsgTime();
 	}
 
 	void addMessage(ConversationItem message) {
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
index 6f3f1f4e894eb470fb2cd70c80dad556932d8bc2..75c0b88c8fc38fda8a0b4bda51de840049608bf8 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
@@ -34,6 +34,7 @@ import org.briarproject.android.view.TextInputView;
 import org.briarproject.android.view.TextInputView.TextInputListener;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.blogs.BlogSharingManager;
+import org.briarproject.api.clients.BaseMessageHeader;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -50,7 +51,7 @@ import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.IntroductionRequestReceivedEvent;
 import org.briarproject.api.event.IntroductionResponseReceivedEvent;
-import org.briarproject.api.event.InvitationReceivedEvent;
+import org.briarproject.api.event.InvitationRequestReceivedEvent;
 import org.briarproject.api.event.InvitationResponseReceivedEvent;
 import org.briarproject.api.event.MessagesAckedEvent;
 import org.briarproject.api.event.MessagesSentEvent;
@@ -76,7 +77,7 @@ import org.briarproject.util.StringUtils;
 
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -476,28 +477,28 @@ public class ConversationActivity extends BriarActivity
 	}
 
 	private void markMessagesRead() {
-		List<MessageId> unread = new ArrayList<>();
+		Map<MessageId, GroupId> unread = new HashMap<>();
 		SparseArray<IncomingItem> list = adapter.getIncomingMessages();
 		for (int i = 0; i < list.size(); i++) {
 			IncomingItem item = list.valueAt(i);
-			if (!item.isRead()) unread.add(item.getId());
+			if (!item.isRead())
+				unread.put(item.getId(), item.getGroupId());
 		}
 		if (unread.isEmpty()) return;
 		if (LOG.isLoggable(INFO))
 			LOG.info("Marking " + unread.size() + " messages read");
-		markMessagesRead(Collections.unmodifiableList(unread));
+		markMessagesRead(unread);
 	}
 
-	private void markMessagesRead(final Collection<MessageId> unread) {
+	private void markMessagesRead(final Map<MessageId, GroupId> unread) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					for (MessageId m : unread)
-						// not really clean, but the messaging manager can
-						// handle introduction messages as well
-						messagingManager.setReadFlag(groupId, m, true);
+					for (Map.Entry<MessageId, GroupId> e : unread.entrySet())
+						messagingManager
+								.setReadFlag(e.getValue(), e.getKey(), true);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Marking read took " + duration + " ms");
@@ -560,6 +561,7 @@ public class ConversationActivity extends BriarActivity
 				IntroductionRequest ir = event.getIntroductionRequest();
 				ConversationItem item = new ConversationIntroductionInItem(ir);
 				addConversationItem(item);
+				markMessageReadIfNew(ir);
 			}
 		} else if (e instanceof IntroductionResponseReceivedEvent) {
 			IntroductionResponseReceivedEvent event =
@@ -570,25 +572,33 @@ public class ConversationActivity extends BriarActivity
 				ConversationItem item =
 						ConversationItem.from(this, contactName, ir);
 				addConversationItem(item);
+				markMessageReadIfNew(ir);
 			}
-		} else if (e instanceof InvitationReceivedEvent) {
-			InvitationReceivedEvent event =
-					(InvitationReceivedEvent) e;
+		} else if (e instanceof InvitationRequestReceivedEvent) {
+			InvitationRequestReceivedEvent event =
+					(InvitationRequestReceivedEvent) e;
 			if (event.getContactId().equals(contactId)) {
-				LOG.info("Invitation received, reloading...");
-				loadMessages();
+				LOG.info("Invitation received, adding...");
+				InvitationRequest ir = event.getRequest();
+				ConversationItem item = ConversationItem.from(ir);
+				addConversationItem(item);
+				markMessageReadIfNew(ir);
 			}
 		} else if (e instanceof InvitationResponseReceivedEvent) {
 			InvitationResponseReceivedEvent event =
 					(InvitationResponseReceivedEvent) e;
 			if (event.getContactId().equals(contactId)) {
-				LOG.info("Invitation response received, reloading...");
-				loadMessages();
+				LOG.info("Invitation response received, adding...");
+				InvitationResponse ir = event.getResponse();
+				ConversationItem item =
+						ConversationItem.from(this, contactName, ir);
+				addConversationItem(item);
+				markMessageReadIfNew(ir);
 			}
 		}
 	}
 
-	private void markMessageReadIfNew(final PrivateMessageHeader h) {
+	private void markMessageReadIfNew(final BaseMessageHeader h) {
 		runOnUiThread(new Runnable() {
 			@Override
 			public void run() {
@@ -597,22 +607,23 @@ public class ConversationActivity extends BriarActivity
 					// Mark the message read if it's the newest message
 					long lastMsgTime = item.getTime();
 					long newMsgTime = h.getTimestamp();
-					if (newMsgTime > lastMsgTime) markNewMessageRead(h.getId());
+					if (newMsgTime > lastMsgTime)
+						markNewMessageRead(h.getGroupId(), h.getId());
 					else loadMessages();
 				} else {
 					// mark the message as read as well if it is the first one
-					markNewMessageRead(h.getId());
+					markNewMessageRead(h.getGroupId(), h.getId());
 				}
 			}
 		});
 	}
 
-	private void markNewMessageRead(final MessageId m) {
+	private void markNewMessageRead(final GroupId g, final MessageId m) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
 				try {
-					messagingManager.setReadFlag(groupId, m, true);
+					messagingManager.setReadFlag(g, m, true);
 					loadMessages();
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -681,11 +692,10 @@ public class ConversationActivity extends BriarActivity
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Storing message took " + duration + " ms");
-
 					MessageId id = m.getMessage().getId();
 					PrivateMessageHeader h = new PrivateMessageHeader(id,
-							m.getMessage().getTimestamp(), m.getContentType(),
-							true, false, false, false);
+							groupId, m.getMessage().getTimestamp(),
+							m.getContentType(), true, false, false, false);
 					ConversationMessageItem item = ConversationItem.from(h);
 					item.setBody(body);
 					bodyCache.put(id, body);
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java
index 331a062358c62d4c6d5fef55771e6259cda7ac0f..6959d214b10abb5599c5d1e04d439ecc7ae1f296 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java
@@ -1,14 +1,16 @@
 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 ConversationItem.IncomingItem {
+		implements IncomingItem {
 
 	private boolean read;
 
-	ConversationIntroductionInItem(IntroductionRequest ir) {
+	ConversationIntroductionInItem(@NotNull IntroductionRequest ir) {
 		super(ir);
 
 		this.read = ir.isRead();
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
index 5fb30a23d96611efffbaa7ba13541082109e8483..d2cdef596ffdadd4c4b38a280f5536ce0d4ead6a 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
@@ -1,6 +1,7 @@
 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 {
@@ -8,13 +9,14 @@ abstract class ConversationIntroductionItem extends ConversationItem {
 	private final IntroductionRequest ir;
 	private boolean answered;
 
-	ConversationIntroductionItem(IntroductionRequest ir) {
-		super(ir.getMessageId(), ir.getTimestamp());
+	ConversationIntroductionItem(@NotNull IntroductionRequest ir) {
+		super(ir.getMessageId(), ir.getGroupId(), ir.getTimestamp());
 
 		this.ir = ir;
 		this.answered = ir.wasAnswered();
 	}
 
+	@NotNull
 	IntroductionRequest getIntroductionRequest() {
 		return ir;
 	}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
index 083dc6bf7840ce47c8611d729e2ec5de1dc583dd..d8c5e5aa746900f212744ae1423f811e5e09f818 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
@@ -12,7 +12,9 @@ import org.briarproject.api.messaging.PrivateMessageHeader;
 import org.briarproject.api.sharing.InvitationMessage;
 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 {
@@ -30,20 +32,29 @@ public abstract class ConversationItem {
 	final static int BLOG_INVITATION_IN = 9;
 	final static int BLOG_INVITATION_OUT = 10;
 
-	private MessageId id;
-	private long time;
+	final private MessageId id;
+	final private GroupId groupId;
+	final private long time;
 
-	public ConversationItem(MessageId id, long time) {
+	public ConversationItem(@NotNull MessageId id, @NotNull GroupId groupId,
+			long time) {
 		this.id = id;
+		this.groupId = groupId;
 		this.time = time;
 	}
 
 	abstract int getType();
 
+	@NotNull
 	public MessageId getId() {
 		return id;
 	}
 
+	@NotNull
+	public GroupId getGroupId() {
+		return groupId;
+	}
+
 	long getTime() {
 		return time;
 	}
@@ -78,8 +89,9 @@ public abstract class ConversationItem {
 						R.string.introduction_response_declined_sent,
 						ir.getName());
 			}
-			return new ConversationNoticeOutItem(ir.getMessageId(), text,
-					ir.getTimestamp(), ir.isSent(), ir.isSeen());
+			return new ConversationNoticeOutItem(ir.getMessageId(),
+					ir.getGroupId(), text, ir.getTimestamp(), ir.isSent(),
+					ir.isSeen());
 		} else {
 			String text;
 			if (ir.wasAccepted()) {
@@ -97,8 +109,8 @@ public abstract class ConversationItem {
 							contactName, ir.getName());
 				}
 			}
-			return new ConversationNoticeInItem(ir.getMessageId(), text,
-					ir.getTimestamp(), ir.isRead());
+			return new ConversationNoticeInItem(ir.getMessageId(),
+					ir.getGroupId(), text, ir.getTimestamp(), ir.isRead());
 		}
 	}
 
@@ -137,8 +149,8 @@ public abstract class ConversationItem {
 						R.string.forum_invitation_response_declined_sent,
 						contactName);
 			}
-			return new ConversationNoticeOutItem(fir.getId(), text,
-					fir.getTimestamp(), fir.isSent(), fir.isSeen());
+			return new ConversationNoticeOutItem(fir.getId(), fir.getGroupId(),
+					text, fir.getTimestamp(), fir.isSent(), fir.isSeen());
 		} else {
 			String text;
 			if (fir.wasAccepted()) {
@@ -150,8 +162,8 @@ public abstract class ConversationItem {
 						R.string.forum_invitation_response_declined_received,
 						contactName);
 			}
-			return new ConversationNoticeInItem(fir.getId(), text,
-					fir.getTimestamp(), fir.isRead());
+			return new ConversationNoticeInItem(fir.getId(), fir.getGroupId(),
+					text, fir.getTimestamp(), fir.isRead());
 		}
 	}
 
@@ -169,8 +181,8 @@ public abstract class ConversationItem {
 						R.string.blogs_sharing_response_declined_sent,
 						contactName);
 			}
-			return new ConversationNoticeOutItem(fir.getId(), text,
-					fir.getTimestamp(), fir.isSent(), fir.isSeen());
+			return new ConversationNoticeOutItem(fir.getId(), fir.getGroupId(),
+					text, fir.getTimestamp(), fir.isSent(), fir.isSeen());
 		} else {
 			String text;
 			if (fir.wasAccepted()) {
@@ -182,8 +194,8 @@ public abstract class ConversationItem {
 						R.string.blogs_sharing_response_declined_received,
 						contactName);
 			}
-			return new ConversationNoticeInItem(fir.getId(), text,
-					fir.getTimestamp(), fir.isRead());
+			return new ConversationNoticeInItem(fir.getId(), fir.getGroupId(),
+					text, fir.getTimestamp(), fir.isRead());
 		}
 	}
 
@@ -193,10 +205,10 @@ public abstract class ConversationItem {
 	 */
 	public static ConversationItem from(IntroductionMessage im) {
 		if (im.isLocal())
-			return new ConversationNoticeOutItem(im.getMessageId(), "",
-					im.getTimestamp(), false, false);
-		return new ConversationNoticeInItem(im.getMessageId(), "",
-				im.getTimestamp(), im.isRead());
+			return new ConversationNoticeOutItem(im.getMessageId(),
+					im.getGroupId(), "", im.getTimestamp(), false, false);
+		return new ConversationNoticeInItem(im.getMessageId(), im.getGroupId(),
+				"", im.getTimestamp(), im.isRead());
 	}
 
 	/**
@@ -205,14 +217,15 @@ public abstract class ConversationItem {
 	 */
 	public static ConversationItem from(InvitationMessage im) {
 		if (im.isLocal())
-			return new ConversationNoticeOutItem(im.getId(), "",
-					im.getTimestamp(), false, false);
-		return new ConversationNoticeInItem(im.getId(), "",
+			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();
@@ -226,8 +239,12 @@ public abstract class ConversationItem {
 
 	interface IncomingItem {
 
+		@NotNull
 		MessageId getId();
 
+		@NotNull
+		GroupId getGroupId();
+
 		boolean isRead();
 
 		void setRead(boolean read);
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java
index b7a6aac44a9b35698ae9a5a59c744be1b46d0034..b4dc64b6cc8c2aa86e729f545b19a7a78776b04d 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java
@@ -9,7 +9,7 @@ abstract class ConversationMessageItem extends ConversationItem {
 	private byte[] body;
 
 	ConversationMessageItem(PrivateMessageHeader header) {
-		super(header.getId(), header.getTimestamp());
+		super(header.getId(), header.getGroupId(), header.getTimestamp());
 
 		this.header = header;
 		body = null;
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
index bfa83160b167cac8761a3cde87a59fc195cfdde5..6e8fbfc8bcb3331b817bea7793f3295eb30b1cfa 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
@@ -1,5 +1,6 @@
 package org.briarproject.android.contact;
 
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 // This class is not thread-safe
@@ -8,9 +9,9 @@ class ConversationNoticeInItem extends ConversationNoticeItem
 
 	private boolean read;
 
-	ConversationNoticeInItem(MessageId id, String text, long time,
-			boolean read) {
-		super(id, text, time);
+	ConversationNoticeInItem(MessageId id, GroupId groupId, String text,
+			long time, boolean read) {
+		super(id, groupId, text, time);
 
 		this.read = read;
 	}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java
index b722ab95d2c32c945e544dcda0d10c5e95e8472f..758c311d01f57e9611e1a5b7411c7dcc41d4d689 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java
@@ -1,13 +1,15 @@
 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, String text, long time) {
-		super(id, time);
+	ConversationNoticeItem(MessageId id, GroupId groupId, String text,
+			long time) {
+		super(id, groupId, time);
 
 		this.text = text;
 	}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
index a5bdb5e20bf1fc2255c3c27214d3ecabd80fe329..95ed78510fba0d585b51044422d660a0f1d3a7ad 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
@@ -1,5 +1,6 @@
 package org.briarproject.android.contact;
 
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 // This class is not thread-safe
@@ -8,9 +9,9 @@ class ConversationNoticeOutItem extends ConversationNoticeItem
 
 	private boolean sent, seen;
 
-	ConversationNoticeOutItem(MessageId id, String text, long time,
-			boolean sent, boolean seen) {
-		super(id, text, time);
+	ConversationNoticeOutItem(MessageId id, GroupId groupId, String text,
+			long time, boolean sent, boolean seen) {
+		super(id, groupId, text, time);
 
 		this.sent = sent;
 		this.seen = seen;
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationItem.java
index a2e9b91cf06166a1c626e8b3de00cdb5e4e345f3..60f8a70b1ca4bd1f331925cb771c35813e1f422e 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationShareableInvitationItem.java
@@ -7,7 +7,7 @@ abstract class ConversationShareableInvitationItem extends ConversationItem {
 	private final InvitationRequest fim;
 
 	ConversationShareableInvitationItem(InvitationRequest fim) {
-		super(fim.getId(), fim.getTimestamp());
+		super(fim.getId(), fim.getGroupId(), fim.getTimestamp());
 
 		this.fim = fim;
 	}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumItem.java b/briar-android/src/org/briarproject/android/forum/ForumItem.java
deleted file mode 100644
index 92ae9320bf7ec48eeeb6b77c6a00ce51bc17ab76..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/forum/ForumItem.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.briarproject.android.forum;
-
-import org.briarproject.api.forum.ForumPostHeader;
-
-// This class is not thread-safe
-class ForumItem {
-
-	private final ForumPostHeader header;
-	private byte[] body;
-
-	ForumItem(ForumPostHeader header) {
-		this.header = header;
-		body = null;
-	}
-
-	ForumPostHeader getHeader() {
-		return header;
-	}
-
-	byte[] getBody() {
-		return body;
-	}
-
-	void setBody(byte[] body) {
-		this.body = body;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumItemComparator.java b/briar-android/src/org/briarproject/android/forum/ForumItemComparator.java
deleted file mode 100644
index d482b979b7d8e95caec1c15c61ce9c89d5f77693..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/forum/ForumItemComparator.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.briarproject.android.forum;
-
-import java.util.Comparator;
-
-class ForumItemComparator implements Comparator<ForumItem> {
-
-	static final ForumItemComparator INSTANCE = new ForumItemComparator();
-
-	public int compare(ForumItem a, ForumItem b) {
-		// The oldest message comes first
-		long aTime = a.getHeader().getTimestamp();
-		long bTime = b.getHeader().getTimestamp();
-		if (aTime < bTime) return -1;
-		if (aTime > bTime) return 1;
-		return 0;
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
index 9c3ba15fe3d578a3c5269ae79999678bfdf95fda..ca237eb35e8847002c3aec56dd3894a0cd285911 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
@@ -17,6 +17,7 @@ import org.briarproject.android.view.TextAvatarView;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.sync.GroupId;
 
+import static android.support.v7.util.SortedList.INVALID_POSITION;
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 import static org.briarproject.android.BriarActivity.GROUP_ID;
@@ -104,7 +105,7 @@ class ForumListAdapter
 
 	@Override
 	public boolean areContentsTheSame(ForumListItem a, ForumListItem b) {
-		return a.getForum().equals(b.getForum()) &&
+		return a.isEmpty() == b.isEmpty() &&
 				a.getTimestamp() == b.getTimestamp() &&
 				a.getUnreadCount() == b.getUnreadCount();
 	}
@@ -125,10 +126,14 @@ class ForumListAdapter
 		return null;
 	}
 
-	void updateItem(ForumListItem item) {
-		ForumListItem oldItem = findItem(item.getForum().getGroup().getId());
-		int position = items.indexOf(oldItem);
-		items.updateItemAt(position, item);
+	int findItemPosition(GroupId g) {
+		int count = getItemCount();
+		for (int i = 0; i < count; i++) {
+			ForumListItem item = getItemAt(i);
+			if (item != null && item.getForum().getGroup().getId().equals(g))
+				return i;
+		}
+		return INVALID_POSITION; // Not found
 	}
 
 	static class ForumViewHolder extends RecyclerView.ViewHolder {
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
index 94f2e2e4e322b956f9b9a228e7e76059c3a1c401..70d82d05d974105f3ac5ba301f7fcf620727f19e 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
@@ -20,6 +20,7 @@ import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.fragment.BaseEventFragment;
 import org.briarproject.android.sharing.InvitationsForumActivity;
 import org.briarproject.android.view.BriarRecyclerView;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.NoSuchGroupException;
 import org.briarproject.api.event.ContactRemovedEvent;
@@ -47,11 +48,8 @@ import static java.util.logging.Level.WARNING;
 public class ForumListFragment extends BaseEventFragment implements
 		OnClickListener {
 
-	public final static String TAG = "ForumListFragment";
-
-	private static final Logger LOG =
-			Logger.getLogger(ForumListFragment.class.getName());
-
+	public final static String TAG = ForumListFragment.class.getName();
+	private final static Logger LOG = Logger.getLogger(TAG);
 
 	private BriarRecyclerView list;
 	private ForumListAdapter adapter;
@@ -118,7 +116,7 @@ public class ForumListFragment extends BaseEventFragment implements
 
 		notificationManager.blockAllForumPostNotifications();
 		notificationManager.clearAllForumPostNotifications();
-		loadForumHeaders();
+		loadForums();
 		loadAvailableForums();
 		list.startPeriodicUpdate();
 	}
@@ -153,7 +151,7 @@ public class ForumListFragment extends BaseEventFragment implements
 		}
 	}
 
-	private void loadForumHeaders() {
+	private void loadForums() {
 		listener.runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
@@ -163,14 +161,14 @@ public class ForumListFragment extends BaseEventFragment implements
 					Collection<ForumListItem> forums = new ArrayList<>();
 					for (Forum f : forumManager.getForums()) {
 						try {
-							Collection<ForumPostHeader> headers =
-									forumManager.getPostHeaders(f.getId());
-							forums.add(new ForumListItem(f, headers));
+							GroupCount count =
+									forumManager.getGroupCount(f.getId());
+							forums.add(new ForumListItem(f, count));
 						} catch (NoSuchGroupException e) {
 							// Continue
 						}
 					}
-					displayForumHeaders(forums);
+					displayForums(forums);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Full load took " + duration + " ms");
@@ -182,7 +180,7 @@ public class ForumListFragment extends BaseEventFragment implements
 		});
 	}
 
-	private void displayForumHeaders(final Collection<ForumListItem> forums) {
+	private void displayForums(final Collection<ForumListItem> forums) {
 		listener.runOnUiThread(new Runnable() {
 			@Override
 			public void run() {
@@ -238,7 +236,7 @@ public class ForumListFragment extends BaseEventFragment implements
 			GroupAddedEvent g = (GroupAddedEvent) e;
 			if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
 				LOG.info("Forum added, reloading forums");
-				loadForumHeaders();
+				loadForums();
 			}
 		} else if (e instanceof GroupRemovedEvent) {
 			GroupRemovedEvent g = (GroupRemovedEvent) e;
@@ -248,39 +246,23 @@ public class ForumListFragment extends BaseEventFragment implements
 			}
 		} else if (e instanceof ForumPostReceivedEvent) {
 			ForumPostReceivedEvent m = (ForumPostReceivedEvent) e;
-			LOG.info("Forum post added, reloading");
-			loadForumHeaders(m.getGroupId());
+			LOG.info("Forum post added, updating...");
+			updateItem(m.getGroupId(), m.getForumPostHeader());
 		} else if (e instanceof ForumInvitationReceivedEvent) {
 			loadAvailableForums();
 		}
 	}
 
-	private void loadForumHeaders(final GroupId g) {
-		listener.runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					Forum f = forumManager.getForum(g);
-					Collection<ForumPostHeader> headers =
-							forumManager.getPostHeaders(g);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Partial load took " + duration + " ms");
-					updateForum(new ForumListItem(f, headers));
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	private void updateForum(final ForumListItem item) {
+	private void updateItem(final GroupId g, final ForumPostHeader m) {
 		listener.runOnUiThread(new Runnable() {
 			@Override
 			public void run() {
-				adapter.updateItem(item);
+				int position = adapter.findItemPosition(g);
+				ForumListItem item = adapter.getItemAt(position);
+				if (item != null) {
+					item.addHeader(m);
+					adapter.updateItemAt(position, item);
+				}
 			}
 		});
 	}
@@ -289,7 +271,8 @@ public class ForumListFragment extends BaseEventFragment implements
 		listener.runOnUiThread(new Runnable() {
 			@Override
 			public void run() {
-				ForumListItem item = adapter.findItem(g);
+				int position = adapter.findItemPosition(g);
+				ForumListItem item = adapter.getItemAt(position);
 				if (item != null) adapter.remove(item);
 			}
 		});
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListItem.java b/briar-android/src/org/briarproject/android/forum/ForumListItem.java
index 88a710d4e927719cd2d7ab8b6813f01f0a6a396d..281eb64b41fbffee6b382abfb97322134d10a2bf 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListItem.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListItem.java
@@ -1,40 +1,27 @@
 package org.briarproject.android.forum;
 
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumPostHeader;
 
-import java.util.Collection;
-
+// This class is NOT thread-safe
 class ForumListItem {
 
 	private final Forum forum;
-	private final boolean empty;
-	private final int postCount;
-	private final long timestamp;
-	private final int unread;
+	private int postCount, unread;
+	private long timestamp;
 
-	ForumListItem(Forum forum, Collection<ForumPostHeader> headers) {
+	ForumListItem(Forum forum, GroupCount count) {
 		this.forum = forum;
-		empty = headers.isEmpty();
-		if (empty) {
-			postCount = 0;
-			timestamp = 0;
-			unread = 0;
-		} else {
-			ForumPostHeader newest = null;
-			long timestamp = -1;
-			int unread = 0;
-			for (ForumPostHeader h : headers) {
-				if (h.getTimestamp() > timestamp) {
-					timestamp = h.getTimestamp();
-					newest = h;
-				}
-				if (!h.isRead()) unread++;
-			}
-			this.postCount = headers.size();
-			this.timestamp = newest.getTimestamp();
-			this.unread = unread;
-		}
+		this.postCount = count.getMsgCount();
+		this.unread = count.getUnreadCount();
+		this.timestamp = count.getLatestMsgTime();
+	}
+
+	void addHeader(ForumPostHeader h) {
+		postCount++;
+		if (!h.isRead()) unread++;
+		if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp();
 	}
 
 	Forum getForum() {
@@ -42,7 +29,7 @@ class ForumListItem {
 	}
 
 	boolean isEmpty() {
-		return empty;
+		return postCount == 0;
 	}
 
 	int getPostCount() {
diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
index ce25c8836ac5618799b7080453b1d1b8cdeaa674..0a7ffe7bd2c0b16316e0dc3ceae7cd2bb1be48f2 100644
--- a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
+++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
@@ -15,9 +15,9 @@ import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.contact.ContactListAdapter;
 import org.briarproject.android.contact.ContactListItem;
-import org.briarproject.android.contact.ConversationItem;
 import org.briarproject.android.fragment.BaseFragment;
 import org.briarproject.android.view.BriarRecyclerView;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
@@ -25,21 +25,16 @@ import org.briarproject.api.db.DbException;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
-import org.briarproject.api.introduction.IntroductionManager;
-import org.briarproject.api.introduction.IntroductionMessage;
-import org.briarproject.api.messaging.MessagingManager;
-import org.briarproject.api.messaging.PrivateMessageHeader;
+import org.briarproject.api.messaging.ConversationManager;
 import org.briarproject.api.plugins.ConnectionRegistry;
 import org.briarproject.api.sync.GroupId;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
-import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
 public class ContactChooserFragment extends BaseFragment {
@@ -61,9 +56,7 @@ public class ContactChooserFragment extends BaseFragment {
 	@Inject
 	protected volatile IdentityManager identityManager;
 	@Inject
-	protected volatile MessagingManager messagingManager;
-	@Inject
-	protected volatile IntroductionManager introductionManager;
+	protected volatile ConversationManager conversationManager;
 	@Inject
 	protected volatile ConnectionRegistry connectionRegistry;
 
@@ -159,23 +152,23 @@ public class ContactChooserFragment extends BaseFragment {
 			public void run() {
 				try {
 					List<ContactListItem> contacts = new ArrayList<>();
-					AuthorId localAuthorId = null;
+					AuthorId localAuthorId =
+							identityManager.getLocalAuthor().getId();
 					for (Contact c : contactManager.getActiveContacts()) {
 						if (c.getId().getInt() == contactId) {
 							c1 = c;
-							localAuthorId = c1.getLocalAuthorId();
 						} else {
 							ContactId id = c.getId();
 							GroupId groupId =
-									messagingManager.getConversationId(id);
-							Collection<ConversationItem> messages =
-									getMessages(id);
+									conversationManager.getConversationId(id);
+							GroupCount count =
+									conversationManager.getGroupCount(id);
 							boolean connected =
 									connectionRegistry.isConnected(c.getId());
 							LocalAuthor localAuthor = identityManager
 									.getLocalAuthor(c.getLocalAuthorId());
 							contacts.add(new ContactListItem(c, localAuthor,
-									connected, groupId, messages));
+									connected, groupId, count));
 						}
 					}
 					displayContacts(localAuthorId, contacts);
@@ -220,36 +213,4 @@ public class ContactChooserFragment extends BaseFragment {
 		builder.show();
 	}
 
-	/**
-	 * This needs to be called from the DbThread
-	 */
-	private Collection<ConversationItem> getMessages(ContactId id)
-			throws DbException {
-
-		long now = System.currentTimeMillis();
-
-		Collection<ConversationItem> messages = new ArrayList<>();
-
-		Collection<PrivateMessageHeader> headers =
-				messagingManager.getMessageHeaders(id);
-		for (PrivateMessageHeader h : headers) {
-			messages.add(ConversationItem.from(h));
-		}
-		long duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Loading message headers took " + duration + " ms");
-
-		now = System.currentTimeMillis();
-		Collection<IntroductionMessage> introductions =
-				introductionManager
-						.getIntroductionMessages(id);
-		for (IntroductionMessage m : introductions) {
-			messages.add(ConversationItem.from(m));
-		}
-		duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Loading introduction messages took " + duration + " ms");
-
-		return messages;
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java b/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java
index 951b49c09809d6eced379b3ac61ea5cfe5f1d69b..893ea91262addaa7c1e18b962968a1999172d7f2 100644
--- a/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java
+++ b/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java
@@ -3,6 +3,7 @@ package org.briarproject.android.sharing;
 import android.content.Context;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.annotation.Nullable;
 import android.support.v7.widget.LinearLayoutManager;
 import android.transition.Fade;
 import android.view.LayoutInflater;
@@ -39,6 +40,7 @@ import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.android.sharing.ShareActivity.CONTACTS;
 import static org.briarproject.android.sharing.ShareActivity.getContactsFromIds;
+import static org.briarproject.android.sharing.ShareActivity.getContactsFromIntegers;
 import static org.briarproject.api.sharing.SharingConstants.GROUP_ID;
 
 public class ContactSelectorFragment extends BaseFragment implements
@@ -122,8 +124,9 @@ public class ContactSelectorFragment extends BaseFragment implements
 		if (savedInstanceState != null) {
 			ArrayList<Integer> intContacts =
 					savedInstanceState.getIntegerArrayList(CONTACTS);
-			selectedContacts = ShareActivity.getContactsFromIntegers(
-					intContacts);
+			if (intContacts != null) {
+				selectedContacts = getContactsFromIntegers(intContacts);
+			}
 		}
 
 		return contentView;
@@ -185,7 +188,7 @@ public class ContactSelectorFragment extends BaseFragment implements
 		updateMenuItem();
 	}
 
-	private void loadContacts(final Collection<ContactId> selection) {
+	private void loadContacts(@Nullable final Collection<ContactId> selection) {
 		shareActivity.runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
diff --git a/briar-android/src/org/briarproject/android/sharing/SelectableContactListItem.java b/briar-android/src/org/briarproject/android/sharing/SelectableContactListItem.java
index 0d713fda2f4594b2f6c0be0b5dd4ea943a9655cc..8e76d39e705a562788528b93a0f900ed57940f89 100644
--- a/briar-android/src/org/briarproject/android/sharing/SelectableContactListItem.java
+++ b/briar-android/src/org/briarproject/android/sharing/SelectableContactListItem.java
@@ -1,14 +1,14 @@
 package org.briarproject.android.sharing;
 
+import android.support.annotation.UiThread;
+
 import org.briarproject.android.contact.ContactListItem;
-import org.briarproject.android.contact.ConversationItem;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.GroupId;
 
-import java.util.Collections;
-
-// This class is not thread-safe
+// This class is NOT thread-safe
 public class SelectableContactListItem extends ContactListItem {
 
 	private boolean selected, disabled;
@@ -16,8 +16,7 @@ public class SelectableContactListItem extends ContactListItem {
 	public SelectableContactListItem(Contact contact, LocalAuthor localAuthor,
 			GroupId groupId, boolean selected, boolean disabled) {
 
-		super(contact, localAuthor, false, groupId,
-				Collections.<ConversationItem>emptyList());
+		super(contact, localAuthor, false, groupId, new GroupCount(0, 0, 0));
 
 		this.selected = selected;
 		this.disabled = disabled;
diff --git a/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java b/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java
index 0d69fe6d293dfce17e233b07def8bf030a649ae2..422a7d16d85af5d05181c16d88a0c3a6737bad2b 100644
--- a/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java
@@ -9,6 +9,7 @@ import org.briarproject.R;
 import org.briarproject.android.BriarActivity;
 import org.briarproject.android.contact.ContactListItem;
 import org.briarproject.android.view.BriarRecyclerView;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.identity.IdentityManager;
@@ -105,8 +106,8 @@ abstract class SharingStatusActivity extends BriarActivity {
 						LocalAuthor localAuthor = identityManager
 								.getLocalAuthor(c.getLocalAuthorId());
 						ContactListItem item =
-								new ContactListItem(c, localAuthor, false, null,
-										null);
+								new ContactListItem(c, localAuthor, false,
+										groupId, new GroupCount(0, 0, 0));
 						contactItems.add(item);
 					}
 				} catch (DbException e) {
@@ -141,8 +142,8 @@ abstract class SharingStatusActivity extends BriarActivity {
 						LocalAuthor localAuthor = identityManager
 								.getLocalAuthor(c.getLocalAuthorId());
 						ContactListItem item =
-								new ContactListItem(c, localAuthor, false, null,
-										null);
+								new ContactListItem(c, localAuthor, false,
+										groupId, new GroupCount(0, 0, 0));
 						contactItems.add(item);
 					}
 				} catch (DbException e) {
diff --git a/briar-android/src/org/briarproject/android/view/TextAvatarView.java b/briar-android/src/org/briarproject/android/view/TextAvatarView.java
index 70ab3f3981bc18138cce89dfee9a5d6c791c0199..8b06ecb4c1cd49ffc4acd83b72f20ff08123ebb8 100644
--- a/briar-android/src/org/briarproject/android/view/TextAvatarView.java
+++ b/briar-android/src/org/briarproject/android/view/TextAvatarView.java
@@ -48,11 +48,12 @@ public class TextAvatarView extends FrameLayout {
 	}
 
 	public void setUnreadCount(int count) {
+		unreadCount = count;
 		if (count > 0) {
-			this.unreadCount = count;
 			badge.setBackgroundResource(R.drawable.bubble);
 			badge.setText(String.valueOf(count));
-			badge.setTextColor(ContextCompat.getColor(getContext(), R.color.briar_text_primary_inverse));
+			badge.setTextColor(ContextCompat.getColor(getContext(),
+					R.color.briar_text_primary_inverse));
 			badge.setVisibility(VISIBLE);
 		} else {
 			badge.setVisibility(INVISIBLE);
@@ -63,12 +64,11 @@ public class TextAvatarView extends FrameLayout {
 		if (problem) {
 			badge.setBackgroundResource(R.drawable.bubble_problem);
 			badge.setText("!");
-			badge.setTextColor(ContextCompat.getColor(getContext(), R.color.briar_primary));
+			badge.setTextColor(ContextCompat
+					.getColor(getContext(), R.color.briar_primary));
 			badge.setVisibility(VISIBLE);
-		} else if (unreadCount > 0) {
-			setUnreadCount(unreadCount);
 		} else {
-			badge.setVisibility(INVISIBLE);
+			setUnreadCount(unreadCount);
 		}
 	}
 
diff --git a/briar-android/src/org/briarproject/android/view/TextInputView.java b/briar-android/src/org/briarproject/android/view/TextInputView.java
index e65c519ab9ddf4eb979d15f34b18799759a378b6..e005624d2a62b58832bfa745f0a40cd71cc622a6 100644
--- a/briar-android/src/org/briarproject/android/view/TextInputView.java
+++ b/briar-android/src/org/briarproject/android/view/TextInputView.java
@@ -9,18 +9,13 @@ import android.support.annotation.CallSuper;
 import android.support.annotation.Nullable;
 import android.support.annotation.StringRes;
 import android.support.annotation.UiThread;
-import android.support.v7.widget.AppCompatImageButton;
 import android.text.Editable;
 import android.text.TextWatcher;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
-import android.widget.Button;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
 
 import org.briarproject.R;
 import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
@@ -32,7 +27,6 @@ import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
 import java.util.logging.Logger;
 
 import static android.content.Context.INPUT_METHOD_SERVICE;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 
 @UiThread
 public class TextInputView extends KeyboardAwareLinearLayout
diff --git a/briar-api/src/org/briarproject/api/blogs/BlogInvitationRequest.java b/briar-api/src/org/briarproject/api/blogs/BlogInvitationRequest.java
index 0f2970b25825ab134f338fa414a97023eae7336a..3fc41b9ddc1c67af73565aa7be09bea9cf3bec7d 100644
--- a/briar-api/src/org/briarproject/api/blogs/BlogInvitationRequest.java
+++ b/briar-api/src/org/briarproject/api/blogs/BlogInvitationRequest.java
@@ -2,8 +2,8 @@ package org.briarproject.api.blogs;
 
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.ContactId;
-import org.briarproject.api.sharing.InvitationMessage;
 import org.briarproject.api.sharing.InvitationRequest;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 public class BlogInvitationRequest extends InvitationRequest {
@@ -11,12 +11,12 @@ public class BlogInvitationRequest extends InvitationRequest {
 	private final String blogAuthorName;
 
 	public BlogInvitationRequest(MessageId id, SessionId sessionId,
-			ContactId contactId, String blogAuthorName, String message,
-			boolean available, long time, boolean local, boolean sent,
-			boolean seen, boolean read) {
+			GroupId groupId, ContactId contactId, String blogAuthorName,
+			String message, boolean available, long time, boolean local,
+			boolean sent, boolean seen, boolean read) {
 
-		super(id, sessionId, contactId, message, available, time, local, sent,
-				seen, read);
+		super(id, sessionId, groupId, contactId, message, available, time,
+				local, sent, seen, read);
 		this.blogAuthorName = blogAuthorName;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/blogs/BlogInvitationResponse.java b/briar-api/src/org/briarproject/api/blogs/BlogInvitationResponse.java
index e3e7cdf692c78197d81c214fcc3ef600e59522c5..3798ea75e29b2282cfe236ea40db1c9d9b5229ad 100644
--- a/briar-api/src/org/briarproject/api/blogs/BlogInvitationResponse.java
+++ b/briar-api/src/org/briarproject/api/blogs/BlogInvitationResponse.java
@@ -3,15 +3,18 @@ package org.briarproject.api.blogs;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.sharing.InvitationResponse;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
 
 public class BlogInvitationResponse extends InvitationResponse {
 
-	public BlogInvitationResponse(MessageId id, SessionId sessionId,
-			ContactId contactId, boolean accept, long time, boolean local,
-			boolean sent, boolean seen, boolean read) {
+	public BlogInvitationResponse(@NotNull MessageId id, SessionId sessionId,
+			GroupId groupId, ContactId contactId, boolean accept, long time,
+			boolean local, boolean sent, boolean seen, boolean read) {
 
-		super(id, sessionId, contactId, accept, time, local, sent, seen, read);
+		super(id, sessionId, groupId, contactId, accept, time, local, sent,
+				seen, read);
 	}
 
 }
diff --git a/briar-api/src/org/briarproject/api/blogs/BlogSharingMessage.java b/briar-api/src/org/briarproject/api/blogs/BlogSharingMessage.java
index 105b0d2a3f981c27db33f36fbc072b296f93689c..1465225e66136d5929a92674896e0f501c99b7b8 100644
--- a/briar-api/src/org/briarproject/api/blogs/BlogSharingMessage.java
+++ b/briar-api/src/org/briarproject/api/blogs/BlogSharingMessage.java
@@ -13,6 +13,7 @@ import static org.briarproject.api.blogs.BlogConstants.BLOG_PUBLIC_KEY;
 import static org.briarproject.api.blogs.BlogConstants.BLOG_TITLE;
 import static org.briarproject.api.sharing.SharingConstants.INVITATION_MSG;
 import static org.briarproject.api.sharing.SharingConstants.SESSION_ID;
+import static org.briarproject.api.sharing.SharingConstants.TIME;
 
 public interface BlogSharingMessage {
 
@@ -25,9 +26,9 @@ public interface BlogSharingMessage {
 
 		public BlogInvitation(GroupId groupId, SessionId sessionId,
 				String blogTitle, String blogDesc, String blogAuthorName,
-				byte[] blogPublicKey, String message) {
+				byte[] blogPublicKey, long time, String message) {
 
-			super(groupId, sessionId, message);
+			super(groupId, sessionId, time, message);
 
 			this.blogTitle = blogTitle;
 			this.blogDesc = blogDesc;
@@ -65,9 +66,10 @@ public interface BlogSharingMessage {
 			String blogAuthorName = d.getString(BLOG_AUTHOR_NAME);
 			byte[] blogPublicKey = d.getRaw(BLOG_PUBLIC_KEY);
 			String message = d.getOptionalString(INVITATION_MSG);
+			long time = d.getLong(TIME);
 
 			return new BlogInvitation(groupId, sessionId, blogTitle,
-					blogDesc, blogAuthorName, blogPublicKey, message);
+					blogDesc, blogAuthorName, blogPublicKey, time, message);
 		}
 
 		public String getBlogTitle() {
diff --git a/briar-api/src/org/briarproject/api/clients/BaseMessageHeader.java b/briar-api/src/org/briarproject/api/clients/BaseMessageHeader.java
index 852cf0b5dc39205a718685f8e975ccd51398dad0..b77ac908d4fa7a98a850f17ef5a67bcae7cbbda7 100644
--- a/briar-api/src/org/briarproject/api/clients/BaseMessageHeader.java
+++ b/briar-api/src/org/briarproject/api/clients/BaseMessageHeader.java
@@ -1,17 +1,22 @@
 package org.briarproject.api.clients;
 
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
 
 public abstract class BaseMessageHeader {
 
 	private final MessageId id;
+	private final GroupId groupId;
 	private final long timestamp;
 	private final boolean local, read, sent, seen;
 
-	public BaseMessageHeader(MessageId id, long timestamp, boolean local,
-			boolean read, boolean sent, boolean seen) {
+	public BaseMessageHeader(@NotNull MessageId id, @NotNull GroupId groupId,
+			long timestamp, boolean local, boolean read, boolean sent,
+			boolean seen) {
 
 		this.id = id;
+		this.groupId = groupId;
 		this.timestamp = timestamp;
 		this.local = local;
 		this.read = read;
@@ -19,10 +24,16 @@ public abstract class BaseMessageHeader {
 		this.seen = seen;
 	}
 
+	@NotNull
 	public MessageId getId() {
 		return id;
 	}
 
+	@NotNull
+	public GroupId getGroupId() {
+		return groupId;
+	}
+
 	public long getTimestamp() {
 		return timestamp;
 	}
@@ -42,4 +53,5 @@ public abstract class BaseMessageHeader {
 	public boolean isSeen() {
 		return seen;
 	}
+
 }
diff --git a/briar-api/src/org/briarproject/api/clients/MessageTracker.java b/briar-api/src/org/briarproject/api/clients/MessageTracker.java
index 8226a3058cfb697cd54dbf902f6a50c4f0f7c336..a73c87183fa949d61cd7858c2a6a443e99236f46 100644
--- a/briar-api/src/org/briarproject/api/clients/MessageTracker.java
+++ b/briar-api/src/org/briarproject/api/clients/MessageTracker.java
@@ -1,9 +1,7 @@
 package org.briarproject.api.clients;
 
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.Transaction;
 import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 
 public interface MessageTracker {
@@ -20,19 +18,20 @@ public interface MessageTracker {
 	void setReadFlag(GroupId g, MessageId m, boolean read) throws DbException;
 
 	class GroupCount {
-		private final long msgCount, unreadCount, latestMsgTime;
+		private final int msgCount, unreadCount;
+		private final long latestMsgTime;
 
-		public GroupCount(long msgCount, long unreadCount, long latestMsgTime) {
+		public GroupCount(int msgCount, int unreadCount, long latestMsgTime) {
 			this.msgCount = msgCount;
 			this.unreadCount = unreadCount;
 			this.latestMsgTime = latestMsgTime;
 		}
 
-		public long getMsgCount() {
+		public int getMsgCount() {
 			return msgCount;
 		}
 
-		public long getUnreadCount() {
+		public int getUnreadCount() {
 			return unreadCount;
 		}
 
diff --git a/briar-api/src/org/briarproject/api/event/BlogInvitationReceivedEvent.java b/briar-api/src/org/briarproject/api/event/BlogInvitationReceivedEvent.java
index 12d8d4cc4b8dec2dd91f2cf4bf1f80322f56f421..26a2ae097d81b4a7068661990191ca1db4aceb0e 100644
--- a/briar-api/src/org/briarproject/api/event/BlogInvitationReceivedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/BlogInvitationReceivedEvent.java
@@ -2,13 +2,16 @@ package org.briarproject.api.event;
 
 import org.briarproject.api.blogs.Blog;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.sharing.InvitationRequest;
 
-public class BlogInvitationReceivedEvent extends InvitationReceivedEvent {
+public class BlogInvitationReceivedEvent extends
+		InvitationRequestReceivedEvent {
 
 	private final Blog blog;
 
-	public BlogInvitationReceivedEvent(Blog blog, ContactId contactId) {
-		super(contactId);
+	public BlogInvitationReceivedEvent(Blog blog, ContactId contactId,
+			InvitationRequest request) {
+		super(contactId, request);
 		this.blog = blog;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/event/BlogInvitationResponseReceivedEvent.java b/briar-api/src/org/briarproject/api/event/BlogInvitationResponseReceivedEvent.java
index b6344ad928dd3c5e14155ad7c8dd7c70c69e3f38..fc4c04be2573bcdfce5eef8508ba6d20db2be3d7 100644
--- a/briar-api/src/org/briarproject/api/event/BlogInvitationResponseReceivedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/BlogInvitationResponseReceivedEvent.java
@@ -1,5 +1,6 @@
 package org.briarproject.api.event;
 
+import org.briarproject.api.blogs.BlogInvitationResponse;
 import org.briarproject.api.contact.ContactId;
 
 public class BlogInvitationResponseReceivedEvent extends InvitationResponseReceivedEvent {
@@ -7,8 +8,8 @@ public class BlogInvitationResponseReceivedEvent extends InvitationResponseRecei
 	private final String blogTitle;
 
 	public BlogInvitationResponseReceivedEvent(String blogTitle,
-			ContactId contactId) {
-		super(contactId);
+			ContactId contactId, BlogInvitationResponse response) {
+		super(contactId, response);
 		this.blogTitle = blogTitle;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java b/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java
index d823a2a6ba42f23fe765faee6b4fa0e5a7d0feca..3077f91605de30b7a6d8bf105f7f058e754f995a 100644
--- a/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java
@@ -2,17 +2,21 @@ package org.briarproject.api.event;
 
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.forum.Forum;
+import org.briarproject.api.forum.ForumInvitationRequest;
 
-public class ForumInvitationReceivedEvent extends InvitationReceivedEvent {
+public class ForumInvitationReceivedEvent extends
+		InvitationRequestReceivedEvent {
 
 	private final Forum forum;
 
-	public ForumInvitationReceivedEvent(Forum forum, ContactId contactId) {
-		super(contactId);
+	public ForumInvitationReceivedEvent(Forum forum, ContactId contactId,
+			ForumInvitationRequest request) {
+		super(contactId, request);
 		this.forum = forum;
 	}
 
 	public Forum getForum() {
 		return forum;
 	}
+
 }
diff --git a/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java b/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java
index cb51daf0a2c05b8f97b3097a4ee7ee2eec48caf3..f4669f94620328c6ad4fcd15647c5c6ca5148624 100644
--- a/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java
@@ -1,14 +1,15 @@
 package org.briarproject.api.event;
 
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.forum.ForumInvitationResponse;
 
 public class ForumInvitationResponseReceivedEvent extends InvitationResponseReceivedEvent {
 
 	private final String forumName;
 
 	public ForumInvitationResponseReceivedEvent(String forumName,
-			ContactId contactId) {
-		super(contactId);
+			ContactId contactId, ForumInvitationResponse response) {
+		super(contactId, response);
 		this.forumName = forumName;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/event/InvitationReceivedEvent.java b/briar-api/src/org/briarproject/api/event/InvitationReceivedEvent.java
deleted file mode 100644
index 834b71eddfc8df351bf34bdcc82d39036ad615b3..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/event/InvitationReceivedEvent.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.briarproject.api.event;
-
-import org.briarproject.api.contact.ContactId;
-
-public abstract class InvitationReceivedEvent extends Event {
-
-	private final ContactId contactId;
-
-	InvitationReceivedEvent(ContactId contactId) {
-		this.contactId = contactId;
-	}
-
-	public ContactId getContactId() {
-		return contactId;
-	}
-}
diff --git a/briar-api/src/org/briarproject/api/event/InvitationRequestReceivedEvent.java b/briar-api/src/org/briarproject/api/event/InvitationRequestReceivedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..184a88f5387a5e8fae71d4771c7d58e1893f1e50
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/InvitationRequestReceivedEvent.java
@@ -0,0 +1,24 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.sharing.InvitationRequest;
+
+public abstract class InvitationRequestReceivedEvent extends Event {
+
+	private final ContactId contactId;
+	private final InvitationRequest request;
+
+	InvitationRequestReceivedEvent(ContactId contactId,
+			InvitationRequest request) {
+		this.contactId = contactId;
+		this.request = request;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+
+	public InvitationRequest getRequest() {
+		return request;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/event/InvitationResponseReceivedEvent.java b/briar-api/src/org/briarproject/api/event/InvitationResponseReceivedEvent.java
index df84a69656d5cbb38ff78843e37397eec676454d..63f41be02d538503e37521386609fcfd94ce6eb9 100644
--- a/briar-api/src/org/briarproject/api/event/InvitationResponseReceivedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/InvitationResponseReceivedEvent.java
@@ -1,16 +1,24 @@
 package org.briarproject.api.event;
 
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.sharing.InvitationResponse;
 
 public abstract class InvitationResponseReceivedEvent extends Event {
 
 	private final ContactId contactId;
+	private final InvitationResponse response;
 
-	public InvitationResponseReceivedEvent(ContactId contactId) {
+	public InvitationResponseReceivedEvent(ContactId contactId,
+			InvitationResponse response) {
 		this.contactId = contactId;
+		this.response = response;
 	}
 
 	public ContactId getContactId() {
 		return contactId;
 	}
+
+	public InvitationResponse getResponse() {
+		return response;
+	}
 }
diff --git a/briar-api/src/org/briarproject/api/forum/ForumInvitationRequest.java b/briar-api/src/org/briarproject/api/forum/ForumInvitationRequest.java
index f043ca2011c6d11374bbbefd6cfbbd88a35be730..3e90116c0b63554169b2dbc382b4fb8367c13461 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumInvitationRequest.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumInvitationRequest.java
@@ -3,19 +3,21 @@ package org.briarproject.api.forum;
 import org.briarproject.api.clients.SessionId;
 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,
-			ContactId contactId, String forumName, String message,
+			GroupId groupId, ContactId contactId, String forumName, String message,
 			boolean available, long time, boolean local, boolean sent,
 			boolean seen, boolean read) {
 
-		super(id, sessionId, contactId, message, available, time, local, sent,
-				seen, read);
+		super(id, sessionId, groupId, contactId, message, available, time,
+				local, sent, seen, read);
 		this.forumName = forumName;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/forum/ForumInvitationResponse.java b/briar-api/src/org/briarproject/api/forum/ForumInvitationResponse.java
index 00a15eac2efb8709121e86759737cc13166e4d0d..d9c49d154546febfe1041f61535a08f66e8385c7 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumInvitationResponse.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumInvitationResponse.java
@@ -3,15 +3,19 @@ package org.briarproject.api.forum;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.sharing.InvitationResponse;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 public class ForumInvitationResponse extends InvitationResponse {
 
-	public ForumInvitationResponse(MessageId id, SessionId sessionId,
-			ContactId contactId, boolean accept, long time, boolean local,
+	public ForumInvitationResponse(@NotNull MessageId id, SessionId sessionId,
+			GroupId groupId, ContactId contactId, boolean accept, long time, boolean local,
 			boolean sent, boolean seen, boolean read) {
 
-		super(id, sessionId, contactId, accept, time, local, sent, seen, read);
+		super(id, sessionId, groupId, contactId, accept, time, local, sent,
+				seen, read);
 	}
 
 }
diff --git a/briar-api/src/org/briarproject/api/forum/ForumSharingMessage.java b/briar-api/src/org/briarproject/api/forum/ForumSharingMessage.java
index 45badea54948e5b5100263a194ed141cdd2abd5e..e93a39c6b84b782c5806f773cf83991fdc7ec974 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumSharingMessage.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumSharingMessage.java
@@ -11,6 +11,7 @@ import static org.briarproject.api.forum.ForumConstants.FORUM_NAME;
 import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
 import static org.briarproject.api.sharing.SharingConstants.INVITATION_MSG;
 import static org.briarproject.api.sharing.SharingConstants.SESSION_ID;
+import static org.briarproject.api.sharing.SharingConstants.TIME;
 
 public interface ForumSharingMessage {
 
@@ -20,9 +21,9 @@ public interface ForumSharingMessage {
 		private final byte[] forumSalt;
 
 		public ForumInvitation(GroupId groupId, SessionId sessionId,
-				String forumName, byte[] forumSalt, String message) {
+				String forumName, byte[] forumSalt, long time, String message) {
 
-			super(groupId, sessionId, message);
+			super(groupId, sessionId, time, message);
 
 			this.forumName = forumName;
 			this.forumSalt = forumSalt;
@@ -53,9 +54,10 @@ public interface ForumSharingMessage {
 			String forumName = d.getString(FORUM_NAME);
 			byte[] forumSalt = d.getRaw(FORUM_SALT);
 			String message = d.getOptionalString(INVITATION_MSG);
+			long time = d.getLong(TIME);
 
 			return new ForumInvitation(groupId, sessionId, forumName, forumSalt,
-					message);
+					time, message);
 		}
 
 		public String getForumName() {
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java
index 7ddf97aeadae65c87ed6455f01bd87bd407e3bb4..e2d968bfa119feb813900876df126b0a84b8f85c 100644
--- a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java
@@ -1,32 +1,36 @@
 package org.briarproject.api.introduction;
 
-import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.clients.BaseMessageHeader;
+import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
 
-import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
 import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
 
- public class IntroductionMessage extends BaseMessageHeader {
+public class IntroductionMessage extends BaseMessageHeader {
 
 	private final SessionId sessionId;
 	private final MessageId messageId;
 	private final int role;
 
-	public IntroductionMessage(SessionId sessionId, MessageId messageId,
-			int role, long time, boolean local, boolean sent, boolean seen,
+	public IntroductionMessage(@NotNull SessionId sessionId,
+			@NotNull MessageId messageId, @NotNull GroupId groupId, int role,
+			long time, boolean local, boolean sent, boolean seen,
 			boolean read) {
 
-		super(messageId, time, local, read, sent, seen);
+		super(messageId, groupId, time, local, read, sent, seen);
 		this.sessionId = sessionId;
 		this.messageId = messageId;
 		this.role = role;
 	}
 
+	@NotNull
 	public SessionId getSessionId() {
 		return sessionId;
 	}
 
+	@NotNull
 	public MessageId getMessageId() {
 		return messageId;
 	}
@@ -35,9 +39,4 @@ import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRO
 		return role == ROLE_INTRODUCER;
 	}
 
-	public boolean isIntroducee() {
-		return role == ROLE_INTRODUCEE;
-	}
-
 }
-
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java
index 912f8a3fba8ff0507e9618a55fbfe64cc07ab519..4a2910dc238092a2240e513acd16944af07734e1 100644
--- a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java
@@ -2,21 +2,25 @@ package org.briarproject.api.introduction;
 
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 public class IntroductionRequest extends IntroductionResponse {
 
 	private final String message;
 	private final boolean answered, exists, introducesOtherIdentity;
 
-	public IntroductionRequest(SessionId sessionId, MessageId messageId,
-			int role, long time, boolean local, boolean sent, boolean seen,
-			boolean read, AuthorId authorId, String name, boolean accepted,
-			String message, boolean answered, boolean exists,
+	public IntroductionRequest(@NotNull SessionId sessionId,
+			@NotNull MessageId messageId, @NotNull GroupId groupId, int role,
+			long time, boolean local, boolean sent, boolean seen, boolean read,
+			AuthorId authorId, String name, boolean accepted,
+			@Nullable String message, boolean answered, boolean exists,
 			boolean introducesOtherIdentity) {
 
-		super(sessionId, messageId, role, time, local, sent, seen, read,
-				authorId, name, accepted);
+		super(sessionId, messageId, groupId, role, time, local, sent, seen,
+				read, authorId, name, accepted);
 
 		this.message = message;
 		this.answered = answered;
@@ -24,6 +28,7 @@ public class IntroductionRequest extends IntroductionResponse {
 		this.introducesOtherIdentity = introducesOtherIdentity;
 	}
 
+	@Nullable
 	public String getMessage() {
 		return message;
 	}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java b/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java
index be4cb57072cabc32dcb2cfb7037c3de164a57a7a..bcb2ce3349e1fd7b02b506d78651156e20d8d753 100644
--- a/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java
@@ -2,7 +2,9 @@ package org.briarproject.api.introduction;
 
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
 
 public class IntroductionResponse extends IntroductionMessage {
 
@@ -10,12 +12,13 @@ public class IntroductionResponse extends IntroductionMessage {
 	private final String name;
 	private final boolean accepted;
 
-	public IntroductionResponse(SessionId sessionId, MessageId messageId,
-			int role, long time, boolean local, boolean sent, boolean seen,
-			boolean read, AuthorId remoteAuthorId, String name,
-			boolean accepted) {
+	public IntroductionResponse(@NotNull SessionId sessionId,
+			@NotNull MessageId messageId, @NotNull GroupId groupId, int role,
+			long time, boolean local, boolean sent, boolean seen, boolean read,
+			AuthorId remoteAuthorId, String name, boolean accepted) {
 
-		super(sessionId, messageId, role, time, local, sent, seen, read);
+		super(sessionId, messageId, groupId, role, time, local, sent, seen,
+				read);
 
 		this.remoteAuthorId = remoteAuthorId;
 		this.name = name;
diff --git a/briar-api/src/org/briarproject/api/messaging/ConversationManager.java b/briar-api/src/org/briarproject/api/messaging/ConversationManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..5695af95b22ce30dd96bb5ea9c71eb2013d3f865
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/messaging/ConversationManager.java
@@ -0,0 +1,28 @@
+package org.briarproject.api.messaging;
+
+import org.briarproject.api.clients.MessageTracker.GroupCount;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.sync.GroupId;
+
+public interface ConversationManager {
+
+	/**
+	 * Clients that present messages in a private conversation need to
+	 * register themselves here.
+	 */
+	void registerConversationClient(ConversationClient client);
+
+	/** Get the main group ID that represents this conversation */
+	GroupId getConversationId(ContactId contactId) throws DbException;
+
+	/** Get the unified group count for all private conversation messages. */
+	GroupCount getGroupCount(ContactId contactId) throws DbException;
+
+	interface ConversationClient {
+		GroupCount getGroupCount(Transaction txn, ContactId contactId)
+				throws DbException;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java b/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java
index e30a0ba636c91d0b4043201c61edd52202718df0..9fd76de6c8435c8030732219355a8c70d664aab7 100644
--- a/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java
+++ b/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java
@@ -1,17 +1,18 @@
 package org.briarproject.api.messaging;
 
 import org.briarproject.api.clients.BaseMessageHeader;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 public class PrivateMessageHeader extends BaseMessageHeader {
 
 	private final String contentType;
 
-	public PrivateMessageHeader(MessageId id, long timestamp,
+	public PrivateMessageHeader(MessageId id, GroupId groupId, long timestamp,
 			String contentType, boolean local, boolean read, boolean sent,
 			boolean seen) {
 
-		super(id, timestamp, local, read, sent, seen);
+		super(id, groupId, timestamp, local, read, sent, seen);
 		this.contentType = contentType;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/sharing/InvitationMessage.java b/briar-api/src/org/briarproject/api/sharing/InvitationMessage.java
index adc8d8932b38b84e19a355a2d4d384d9b8d308b3..4e6e4858f0a370f530b50f896e17ec3cafbaf8cb 100644
--- a/briar-api/src/org/briarproject/api/sharing/InvitationMessage.java
+++ b/briar-api/src/org/briarproject/api/sharing/InvitationMessage.java
@@ -1,28 +1,33 @@
 package org.briarproject.api.sharing;
 
+import org.briarproject.api.clients.BaseMessageHeader;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.ContactId;
-import org.briarproject.api.clients.BaseMessageHeader;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
 
 public abstract class InvitationMessage extends BaseMessageHeader {
 
 	private final SessionId sessionId;
 	private final ContactId contactId;
 
-	public InvitationMessage(MessageId id, SessionId sessionId,
-			ContactId contactId, long time, boolean local, boolean sent,
-			boolean seen, boolean read) {
+	public InvitationMessage(@NotNull MessageId id,
+			@NotNull SessionId sessionId, @NotNull GroupId groupId,
+			@NotNull ContactId contactId, long time, boolean local,
+			boolean sent, boolean seen, boolean read) {
 
-		super(id, time, local, read, sent, seen);
+		super(id, groupId, time, local, read, sent, seen);
 		this.sessionId = sessionId;
 		this.contactId = contactId;
 	}
 
+	@NotNull
 	public SessionId getSessionId() {
 		return sessionId;
 	}
 
+	@NotNull
 	public ContactId getContactId() {
 		return contactId;
 	}
diff --git a/briar-api/src/org/briarproject/api/sharing/InvitationRequest.java b/briar-api/src/org/briarproject/api/sharing/InvitationRequest.java
index 6d5f07d35e4ef2611c5565993fe2ec8dde96c6aa..23ed952b96d43829fa72a5149cec284bc6526e8d 100644
--- a/briar-api/src/org/briarproject/api/sharing/InvitationRequest.java
+++ b/briar-api/src/org/briarproject/api/sharing/InvitationRequest.java
@@ -2,23 +2,28 @@ package org.briarproject.api.sharing;
 
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 public abstract class InvitationRequest extends InvitationMessage {
 
 	private final String message;
 	private final boolean available;
 
-	public InvitationRequest(MessageId id, SessionId sessionId,
-			ContactId contactId, String message,
+	public InvitationRequest(@NotNull MessageId id,
+			@NotNull SessionId sessionId, @NotNull GroupId groupId,
+			@NotNull ContactId contactId, @Nullable String message,
 			boolean available, long time, boolean local, boolean sent,
 			boolean seen, boolean read) {
 
-		super(id, sessionId, contactId, time, local, read, sent, seen);
+		super(id, sessionId, groupId, contactId, time, local, sent, seen, read);
 		this.message = message;
 		this.available = available;
 	}
 
+	@Nullable
 	public String getMessage() {
 		return message;
 	}
diff --git a/briar-api/src/org/briarproject/api/sharing/InvitationResponse.java b/briar-api/src/org/briarproject/api/sharing/InvitationResponse.java
index cf4d53774e4efcadb1c4bbdedd1a2a299cf0097c..0e2d24ee65f00e1ca5f76a1108138d5c0ed9d69c 100644
--- a/briar-api/src/org/briarproject/api/sharing/InvitationResponse.java
+++ b/briar-api/src/org/briarproject/api/sharing/InvitationResponse.java
@@ -2,17 +2,19 @@ package org.briarproject.api.sharing;
 
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
 
 public abstract class InvitationResponse extends InvitationMessage {
 
 	private final boolean accept;
 
-	public InvitationResponse(MessageId id, SessionId sessionId,
-			ContactId contactId, boolean accept, long time, boolean local,
-			boolean sent, boolean seen, boolean read) {
+	public InvitationResponse(@NotNull MessageId id, SessionId sessionId,
+			GroupId groupId, ContactId contactId, boolean accept, long time,
+			boolean local, boolean sent, boolean seen, boolean read) {
 
-		super(id, sessionId, contactId, time, local, read, sent, seen);
+		super(id, sessionId, groupId, contactId, time, local, sent, seen, read);
 		this.accept = accept;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/sharing/SharingConstants.java b/briar-api/src/org/briarproject/api/sharing/SharingConstants.java
index e09b9015c53db9bf576ddf05db9b06b10bfd1c25..47a990669d9ebd0098341e2b89207e6455bd3035 100644
--- a/briar-api/src/org/briarproject/api/sharing/SharingConstants.java
+++ b/briar-api/src/org/briarproject/api/sharing/SharingConstants.java
@@ -19,6 +19,8 @@ public interface SharingConstants {
 	String IS_SHARER = "isSharer";
 	String SHAREABLE_ID = "shareableId";
 	String INVITATION_MSG = "invitationMsg";
+	String INVITATION_ID = "invitationId";
+	String RESPONSE_ID = "responseId";
 	int SHARE_MSG_TYPE_INVITATION = 1;
 	int SHARE_MSG_TYPE_ACCEPT = 2;
 	int SHARE_MSG_TYPE_DECLINE = 3;
diff --git a/briar-api/src/org/briarproject/api/sharing/SharingMessage.java b/briar-api/src/org/briarproject/api/sharing/SharingMessage.java
index be0cac328d2a76d243dff004e071069b5f42167b..3078d86b4ef208e0d233d0e9c19c6717d25d6090 100644
--- a/briar-api/src/org/briarproject/api/sharing/SharingMessage.java
+++ b/briar-api/src/org/briarproject/api/sharing/SharingMessage.java
@@ -14,6 +14,7 @@ import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEP
 import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE;
 import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
 import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE;
+import static org.briarproject.api.sharing.SharingConstants.TIME;
 import static org.briarproject.api.sharing.SharingConstants.TYPE;
 
 public interface SharingMessage {
@@ -21,10 +22,12 @@ public interface SharingMessage {
 	abstract class BaseMessage {
 		private final GroupId groupId;
 		private final SessionId sessionId;
+		private final long time;
 
-		BaseMessage(GroupId groupId, SessionId sessionId) {
+		BaseMessage(GroupId groupId, SessionId sessionId, long time) {
 			this.groupId = groupId;
 			this.sessionId = sessionId;
+			this.time = time;
 		}
 
 		public BdfList toBdfList() {
@@ -62,16 +65,20 @@ public interface SharingMessage {
 		public SessionId getSessionId() {
 			return sessionId;
 		}
+
+		public long getTime() {
+			return time;
+		}
 	}
 
 	abstract class Invitation extends BaseMessage {
 
 		protected final String message;
 
-		public Invitation(GroupId groupId, SessionId sessionId,
+		public Invitation(GroupId groupId, SessionId sessionId, long time,
 				String message) {
 
-			super(groupId, sessionId);
+			super(groupId, sessionId, time);
 
 			this.message = message;
 		}
@@ -90,8 +97,9 @@ public interface SharingMessage {
 
 		private final long type;
 
-		public SimpleMessage(long type, GroupId groupId, SessionId sessionId) {
-			super(groupId, sessionId);
+		public SimpleMessage(long type, GroupId groupId, SessionId sessionId,
+				long time) {
+			super(groupId, sessionId, time);
 			this.type = type;
 		}
 
@@ -114,7 +122,8 @@ public interface SharingMessage {
 					type != SHARE_MSG_TYPE_ABORT) throw new FormatException();
 
 			SessionId sessionId = new SessionId(d.getRaw(SESSION_ID));
-			return new SimpleMessage(type, groupId, sessionId);
+			long time = d.getLong(TIME);
+			return new SimpleMessage(type, groupId, sessionId, time);
 		}
 	}
 
diff --git a/briar-core/src/org/briarproject/clients/BdfIncomingMessageHook.java b/briar-core/src/org/briarproject/clients/BdfIncomingMessageHook.java
index c81293ab04e0324de5f5443aa2d636f4ce616a70..575a2433d4b36569c04ea39a6259b4193313bf1c 100644
--- a/briar-core/src/org/briarproject/clients/BdfIncomingMessageHook.java
+++ b/briar-core/src/org/briarproject/clients/BdfIncomingMessageHook.java
@@ -81,8 +81,8 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
 	protected void trackMessage(Transaction txn, GroupId g, long time,
 			boolean read) throws DbException {
 		GroupCount c = getGroupCount(txn, g);
-		long msgCount = c.getMsgCount() + 1;
-		long unreadCount = c.getUnreadCount() + (read ? 0 : 1);
+		int msgCount = c.getMsgCount() + 1;
+		int unreadCount = c.getUnreadCount() + (read ? 0 : 1);
 		long latestTime =
 				time > c.getLatestMsgTime() ? time : c.getLatestMsgTime();
 		storeGroupCount(txn, g,
@@ -103,14 +103,14 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
 		return count;
 	}
 
-	private GroupCount getGroupCount(Transaction txn, GroupId g)
+	protected GroupCount getGroupCount(Transaction txn, GroupId g)
 			throws DbException {
 		GroupCount count;
 		try {
 			BdfDictionary d = clientHelper.getGroupMetadataAsDictionary(txn, g);
 			count = new GroupCount(
-					d.getLong(GROUP_KEY_MSG_COUNT, 0L),
-					d.getLong(GROUP_KEY_UNREAD_COUNT, 0L),
+					d.getLong(GROUP_KEY_MSG_COUNT, 0L).intValue(),
+					d.getLong(GROUP_KEY_UNREAD_COUNT, 0L).intValue(),
 					d.getLong(GROUP_KEY_LATEST_MSG, 0L)
 			);
 		} catch (FormatException e) {
diff --git a/briar-core/src/org/briarproject/clients/ConversationClientImpl.java b/briar-core/src/org/briarproject/clients/ConversationClientImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..39342b87e73bf12d0d6c27700461ce43d376a07c
--- /dev/null
+++ b/briar-core/src/org/briarproject/clients/ConversationClientImpl.java
@@ -0,0 +1,33 @@
+package org.briarproject.clients;
+
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+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.messaging.ConversationManager.ConversationClient;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.GroupId;
+
+public abstract class ConversationClientImpl extends BdfIncomingMessageHook
+		implements ConversationClient {
+
+	protected ConversationClientImpl(DatabaseComponent db,
+			ClientHelper clientHelper, MetadataParser metadataParser) {
+		super(db, clientHelper, metadataParser);
+	}
+
+	protected abstract Group getContactGroup(Contact contact);
+
+	@Override
+	public GroupCount getGroupCount(Transaction txn, ContactId contactId)
+			throws DbException {
+		Contact contact = db.getContact(txn, contactId);
+		GroupId groupId = getContactGroup(contact).getId();
+		return getGroupCount(txn, groupId);
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
index 8f77539c79faee46489301b2fe982740ad089fa3..4115fe0c09e695096360b78c5f215f285cdf3384 100644
--- a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
+++ b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
@@ -12,6 +12,7 @@ import org.briarproject.api.introduction.IntroduceeAction;
 import org.briarproject.api.introduction.IntroduceeProtocolState;
 import org.briarproject.api.introduction.IntroductionRequest;
 import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 import java.util.ArrayList;
@@ -343,6 +344,7 @@ public class IntroduceeEngine
 
 		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
 		MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
+		GroupId groupId = new GroupId(msg.getRaw(GROUP_ID));
 		long time = msg.getLong(MESSAGE_TIME);
 		String name = msg.getString(NAME);
 		String message = msg.getOptionalString(MSG);
@@ -351,8 +353,9 @@ public class IntroduceeEngine
 				localState.getBoolean(REMOTE_AUTHOR_IS_US);
 
 		IntroductionRequest ir = new IntroductionRequest(sessionId, messageId,
-				ROLE_INTRODUCEE, time, false, false, false, false, authorId,
-				name, false, message, false, exists, introducesOtherIdentity);
+				groupId, ROLE_INTRODUCEE, time, false, false, false, false,
+				authorId, name, false, message, false, exists,
+				introducesOtherIdentity);
 		return new IntroductionRequestReceivedEvent(contactId, ir);
 	}
 
diff --git a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
index a431fd7e46d9040696c97abd99b815341d03ee98..0f180a622f01eea69da8f3c7aa0cb0775a3e6bd6 100644
--- a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
+++ b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
@@ -12,6 +12,7 @@ import org.briarproject.api.introduction.IntroducerAction;
 import org.briarproject.api.introduction.IntroducerProtocolState;
 import org.briarproject.api.introduction.IntroductionResponse;
 import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 import java.util.ArrayList;
@@ -298,14 +299,15 @@ public class IntroducerEngine
 
 		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
 		MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
+		GroupId groupId = new GroupId(msg.getRaw(GROUP_ID));
 		long time = msg.getLong(MESSAGE_TIME);
 		String name = getOtherContact(localState, msg);
 		boolean accept = msg.getBoolean(ACCEPT);
 
 		IntroductionResponse ir =
-				new IntroductionResponse(sessionId, messageId, ROLE_INTRODUCER,
-						time, false, false, false, false, authorId, name,
-						accept);
+				new IntroductionResponse(sessionId, messageId, groupId,
+						ROLE_INTRODUCER, time, false, false, false, false,
+						authorId, name, accept);
 		return new IntroductionResponseReceivedEvent(contactId, ir);
 	}
 
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
index 6c44e83e6bbf60d403a431bf59194c592b8f6ca3..e42ee1e0973156660b336258335e1a6ded233ba5 100644
--- a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
@@ -29,7 +29,7 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
-import org.briarproject.clients.BdfIncomingMessageHook;
+import org.briarproject.clients.ConversationClientImpl;
 import org.briarproject.util.StringUtils;
 
 import java.io.IOException;
@@ -76,7 +76,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUE
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 
-class IntroductionManagerImpl extends BdfIncomingMessageHook
+class IntroductionManagerImpl extends ConversationClientImpl
 		implements IntroductionManager, Client, AddContactHook,
 		RemoveContactHook {
 
@@ -119,7 +119,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 	public void addingContact(Transaction txn, Contact c) throws DbException {
 		try {
 			// Create an introduction group for sending introduction messages
-			Group g = introductionGroupFactory.createIntroductionGroup(c);
+			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
@@ -196,7 +196,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 
 		// remove the group (all messages will be removed with it)
 		// this contact won't get our abort message, but the other will
-		db.removeGroup(txn, introductionGroupFactory.createIntroductionGroup(c));
+		db.removeGroup(txn, getContactGroup(c));
 	}
 
 	/**
@@ -288,6 +288,11 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 		return false;
 	}
 
+	@Override
+	protected Group getContactGroup(Contact contact) {
+		return introductionGroupFactory.createIntroductionGroup(contact);
+	}
+
 	@Override
 	public void makeIntroduction(Contact c1, Contact c2, String msg,
 			final long timestamp)
@@ -296,8 +301,8 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 		Transaction txn = db.startTransaction(false);
 		try {
 			introducerManager.makeIntroduction(txn, c1, c2, msg, timestamp);
-			Group g1 = introductionGroupFactory.createIntroductionGroup(c1);
-			Group g2 = introductionGroupFactory.createIntroductionGroup(c2);
+			Group g1 = getContactGroup(c1);
+			Group g2 = getContactGroup(c2);
 			trackMessage(txn, g1.getId(), timestamp, true);
 			trackMessage(txn, g2.getId(), timestamp, true);
 			txn.setComplete();
@@ -314,7 +319,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 		Transaction txn = db.startTransaction(false);
 		try {
 			Contact c = db.getContact(txn, contactId);
-			Group g = introductionGroupFactory.createIntroductionGroup(c);
+			Group g = getContactGroup(c);
 			BdfDictionary state =
 					getSessionState(txn, g.getId(), sessionId.getBytes());
 
@@ -334,7 +339,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 		Transaction txn = db.startTransaction(false);
 		try {
 			Contact c = db.getContact(txn, contactId);
-			Group g = introductionGroupFactory.createIntroductionGroup(c);
+			Group g = getContactGroup(c);
 			BdfDictionary state =
 					getSessionState(txn, g.getId(), sessionId.getBytes());
 
@@ -358,9 +363,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 		Transaction txn = db.startTransaction(true);
 		try {
 			// get messages and their status
-			GroupId g = introductionGroupFactory
-					.createIntroductionGroup(db.getContact(txn, contactId))
-					.getId();
+			GroupId g = getContactGroup(db.getContact(txn, contactId)).getId();
 			metadata = clientHelper.getMessageMetadataAsDictionary(txn, g);
 			statuses = db.getMessageStatus(txn, contactId, g);
 
@@ -415,7 +418,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 							name = state.getString(NAME);
 						}
 						IntroductionResponse ir = new IntroductionResponse(
-								sessionId, messageId, role, time, local,
+								sessionId, messageId, g, role, time, local,
 								s.isSent(), s.isSeen(), read, authorId, name,
 								accepted);
 						list.add(ir);
@@ -445,7 +448,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 									state.getBoolean(REMOTE_AUTHOR_IS_US);
 						}
 						IntroductionRequest ir = new IntroductionRequest(
-								sessionId, messageId, role, time, local,
+								sessionId, messageId, g, role, time, local,
 								s.isSent(), s.isSeen(), read, authorId, name,
 								accepted, message, answered, exists,
 								introducesOtherIdentity);
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionModule.java b/briar-core/src/org/briarproject/introduction/IntroductionModule.java
index 4ea0ac6c3b5a9251d6b41db231f01f02cc3409fe..989100ff8b2266f5435def18051b36bdb111dfdc 100644
--- a/briar-core/src/org/briarproject/introduction/IntroductionModule.java
+++ b/briar-core/src/org/briarproject/introduction/IntroductionModule.java
@@ -6,6 +6,7 @@ import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.introduction.IntroductionManager;
 import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.messaging.ConversationManager;
 import org.briarproject.api.system.Clock;
 
 import javax.inject.Inject;
@@ -47,6 +48,7 @@ public class IntroductionModule {
 			LifecycleManager lifecycleManager,
 			ContactManager contactManager,
 			MessageQueueManager messageQueueManager,
+			ConversationManager conversationManager,
 			IntroductionManagerImpl introductionManager) {
 
 		lifecycleManager.registerClient(introductionManager);
@@ -55,6 +57,7 @@ public class IntroductionModule {
 		messageQueueManager.registerIncomingMessageHook(
 				introductionManager.getClientId(),
 				introductionManager);
+		conversationManager.registerConversationClient(introductionManager);
 
 		return introductionManager;
 	}
diff --git a/briar-core/src/org/briarproject/messaging/ConversationManagerImpl.java b/briar-core/src/org/briarproject/messaging/ConversationManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..670af288ed00ad9f465066b7fae0ae98fdbb3cd3
--- /dev/null
+++ b/briar-core/src/org/briarproject/messaging/ConversationManagerImpl.java
@@ -0,0 +1,80 @@
+package org.briarproject.messaging;
+
+import org.briarproject.api.clients.ContactGroupFactory;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+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.sync.Group;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import javax.inject.Inject;
+
+class ConversationManagerImpl implements ConversationManager {
+
+	private final DatabaseComponent db;
+	private final ContactGroupFactory contactGroupFactory;
+	private final Set<ConversationClient> clients;
+
+	@Inject
+	ConversationManagerImpl(DatabaseComponent db,
+			ContactGroupFactory contactGroupFactory) {
+		this.db = db;
+		this.contactGroupFactory = contactGroupFactory;
+		clients = new CopyOnWriteArraySet<ConversationClient>();
+	}
+
+	@Override
+	public void registerConversationClient(ConversationClient client) {
+		if (!clients.add(client)) {
+			throw new IllegalStateException(
+					"This client is already registered");
+		}
+	}
+
+	@Override
+	public GroupId getConversationId(ContactId contactId) throws DbException {
+		// TODO we should probably transition this to its own group
+		//      and/or work with the ContactId in the UI instead
+		Contact contact;
+		Transaction txn = db.startTransaction(true);
+		try {
+			contact = db.getContact(txn, contactId);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		Group group = contactGroupFactory
+				.createContactGroup(MessagingManagerImpl.CLIENT_ID, contact);
+		return group.getId();
+	}
+
+	@Override
+	public GroupCount getGroupCount(ContactId contactId)
+			throws DbException {
+
+		int msgCount = 0, unreadCount = 0;
+		long latestTime = 0;
+		Transaction txn = db.startTransaction(true);
+		try {
+			for (ConversationClient client : clients) {
+				GroupCount count = client.getGroupCount(txn, contactId);
+				msgCount += count.getMsgCount();
+				unreadCount += count.getUnreadCount();
+				if (count.getLatestMsgTime() > latestTime)
+					latestTime = count.getLatestMsgTime();
+			}
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		return new GroupCount(msgCount, unreadCount, latestTime);
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index d5bc6db1d374b6d70c9a9ecbc17fb8d867bca709..8a591849a24ca5643abf77ce54f12b2340cf8166 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
@@ -24,7 +24,7 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
-import org.briarproject.clients.BdfIncomingMessageHook;
+import org.briarproject.clients.ConversationClientImpl;
 import org.briarproject.util.StringUtils;
 
 import java.util.ArrayList;
@@ -35,7 +35,7 @@ import javax.inject.Inject;
 
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 
-class MessagingManagerImpl extends BdfIncomingMessageHook
+class MessagingManagerImpl extends ConversationClientImpl
 		implements MessagingManager, Client, AddContactHook, RemoveContactHook {
 
 	static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
@@ -77,7 +77,8 @@ class MessagingManagerImpl extends BdfIncomingMessageHook
 		}
 	}
 
-	private Group getContactGroup(Contact c) {
+	@Override
+	protected Group getContactGroup(Contact c) {
 		return contactGroupFactory.createContactGroup(CLIENT_ID, c);
 	}
 
@@ -101,7 +102,8 @@ class MessagingManagerImpl extends BdfIncomingMessageHook
 		boolean local = meta.getBoolean("local");
 		boolean read = meta.getBoolean(MSG_KEY_READ);
 		PrivateMessageHeader header = new PrivateMessageHeader(
-				m.getId(), timestamp, contentType, local, read, false, false);
+				m.getId(), m.getGroupId(), timestamp, contentType, local, read,
+				false, false);
 		PrivateMessageReceivedEvent event = new PrivateMessageReceivedEvent(
 				header, groupId);
 		txn.attach(event);
@@ -159,9 +161,10 @@ class MessagingManagerImpl extends BdfIncomingMessageHook
 			throws DbException {
 		Map<MessageId, BdfDictionary> metadata;
 		Collection<MessageStatus> statuses;
+		GroupId g;
 		Transaction txn = db.startTransaction(true);
 		try {
-			GroupId g = getContactGroup(db.getContact(txn, c)).getId();
+			g = getContactGroup(db.getContact(txn, c)).getId();
 			metadata = clientHelper.getMessageMetadataAsDictionary(txn, g);
 			statuses = db.getMessageStatus(txn, c, g);
 			txn.setComplete();
@@ -181,8 +184,8 @@ class MessagingManagerImpl extends BdfIncomingMessageHook
 				String contentType = meta.getString("contentType");
 				boolean local = meta.getBoolean("local");
 				boolean read = meta.getBoolean("read");
-				headers.add(new PrivateMessageHeader(id, timestamp, contentType,
-						local, read, s.isSent(), s.isSeen()));
+				headers.add(new PrivateMessageHeader(id, g, timestamp,
+						contentType, local, read, s.isSent(), s.isSeen()));
 			} catch (FormatException e) {
 				throw new DbException(e);
 			}
@@ -201,4 +204,14 @@ class MessagingManagerImpl extends BdfIncomingMessageHook
 		}
 	}
 
+	@Override
+	public GroupCount getGroupCount(Transaction txn, ContactId contactId)
+			throws DbException {
+
+		Contact contact = db.getContact(txn, contactId);
+		GroupId groupId = getContactGroup(contact).getId();
+
+		return getGroupCount(txn, groupId);
+	}
+
 }
diff --git a/briar-core/src/org/briarproject/messaging/MessagingModule.java b/briar-core/src/org/briarproject/messaging/MessagingModule.java
index d22782cf42b7f19d87c954721a3c0524759e160b..243042dbc49c857e88a3cc158110d9d8d19601cb 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingModule.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingModule.java
@@ -4,6 +4,7 @@ import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.messaging.ConversationManager;
 import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessageFactory;
 import org.briarproject.api.sync.ValidationManager;
@@ -22,6 +23,7 @@ public class MessagingModule {
 
 	public static class EagerSingletons {
 		@Inject MessagingManager messagingManager;
+		@Inject ConversationManager conversationManager;
 		@Inject PrivateMessageValidator privateMessageValidator;
 	}
 
@@ -46,12 +48,22 @@ public class MessagingModule {
 	@Singleton
 	MessagingManager getMessagingManager(LifecycleManager lifecycleManager,
 			ContactManager contactManager, ValidationManager validationManager,
+			ConversationManager conversationManager,
 			MessagingManagerImpl messagingManager) {
 		lifecycleManager.registerClient(messagingManager);
 		contactManager.registerAddContactHook(messagingManager);
 		contactManager.registerRemoveContactHook(messagingManager);
 		validationManager
 				.registerIncomingMessageHook(CLIENT_ID, messagingManager);
+		conversationManager.registerConversationClient(messagingManager);
 		return messagingManager;
 	}
+
+	@Provides
+	@Singleton
+	ConversationManager getConversationManager(
+			ConversationManagerImpl conversationManager) {
+		return conversationManager;
+	}
+
 }
diff --git a/briar-core/src/org/briarproject/sharing/BlogInviteeSessionState.java b/briar-core/src/org/briarproject/sharing/BlogInviteeSessionState.java
index 7bb3295b7b02a925718b08ca2da3875a25d58980..58ab24a0e9331a70c9dcec5fa08485c00fb6dd4b 100644
--- a/briar-core/src/org/briarproject/sharing/BlogInviteeSessionState.java
+++ b/briar-core/src/org/briarproject/sharing/BlogInviteeSessionState.java
@@ -5,6 +5,8 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import static org.briarproject.api.blogs.BlogConstants.BLOG_AUTHOR_NAME;
 import static org.briarproject.api.blogs.BlogConstants.BLOG_DESC;
@@ -21,8 +23,9 @@ public class BlogInviteeSessionState extends InviteeSessionState {
 	public BlogInviteeSessionState(SessionId sessionId, MessageId storageId,
 			GroupId groupId, State state, ContactId contactId, GroupId blogId,
 			String blogTitle, String blogDesc, String blogAuthorName,
-			byte[] blogPublicKey) {
-		super(sessionId, storageId, groupId, state, contactId, blogId);
+			byte[] blogPublicKey, @NotNull MessageId invitationId) {
+		super(sessionId, storageId, groupId, state, contactId, blogId,
+				invitationId);
 
 		this.blogTitle = blogTitle;
 		this.blogDesc = blogDesc;
diff --git a/briar-core/src/org/briarproject/sharing/BlogSharerSessionState.java b/briar-core/src/org/briarproject/sharing/BlogSharerSessionState.java
index 16ef1355b2183ab4d45c7074fef9e4b5c37b5e41..5dd365cae718a06d1156bbaaf324b8a5bb20d647 100644
--- a/briar-core/src/org/briarproject/sharing/BlogSharerSessionState.java
+++ b/briar-core/src/org/briarproject/sharing/BlogSharerSessionState.java
@@ -5,6 +5,7 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
 
 import static org.briarproject.api.blogs.BlogConstants.BLOG_AUTHOR_NAME;
 import static org.briarproject.api.blogs.BlogConstants.BLOG_DESC;
@@ -21,8 +22,9 @@ public class BlogSharerSessionState extends SharerSessionState {
 	public BlogSharerSessionState(SessionId sessionId, MessageId storageId,
 			GroupId groupId, State state, ContactId contactId, GroupId blogId,
 			String blogTitle, String blogDesc, String blogAuthorName,
-			byte[] blogPublicKey) {
-		super(sessionId, storageId, groupId, state, contactId, blogId);
+			byte[] blogPublicKey, @Nullable MessageId responseId) {
+		super(sessionId, storageId, groupId, state, contactId, blogId,
+				responseId);
 
 		this.blogTitle = blogTitle;
 		this.blogDesc = blogDesc;
diff --git a/briar-core/src/org/briarproject/sharing/BlogSharingManagerImpl.java b/briar-core/src/org/briarproject/sharing/BlogSharingManagerImpl.java
index 32314ec7305d8b6ae8d154a8686e1bf99264b177..7786dedee9cf585ec67e42c40c375b3d2b01d4b4 100644
--- a/briar-core/src/org/briarproject/sharing/BlogSharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/sharing/BlogSharingManagerImpl.java
@@ -10,8 +10,8 @@ import org.briarproject.api.blogs.BlogManager.RemoveBlogHook;
 import org.briarproject.api.blogs.BlogSharingManager;
 import org.briarproject.api.blogs.BlogSharingMessage.BlogInvitation;
 import org.briarproject.api.clients.ClientHelper;
-import org.briarproject.api.clients.MessageQueueManager;
 import org.briarproject.api.clients.ContactGroupFactory;
+import org.briarproject.api.clients.MessageQueueManager;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -43,6 +43,8 @@ import static org.briarproject.api.blogs.BlogConstants.BLOG_AUTHOR_NAME;
 import static org.briarproject.api.blogs.BlogConstants.BLOG_DESC;
 import static org.briarproject.api.blogs.BlogConstants.BLOG_PUBLIC_KEY;
 import static org.briarproject.api.blogs.BlogConstants.BLOG_TITLE;
+import static org.briarproject.api.sharing.SharingConstants.INVITATION_ID;
+import static org.briarproject.api.sharing.SharingConstants.RESPONSE_ID;
 
 class BlogSharingManagerImpl extends
 		SharingManagerImpl<Blog, BlogInvitation, BlogInviteeSessionState, BlogSharerSessionState, BlogInvitationReceivedEvent, BlogInvitationResponseReceivedEvent>
@@ -108,17 +110,18 @@ class BlogSharingManagerImpl extends
 			BlogInvitation msg, ContactId contactId, boolean available,
 			long time, boolean local, boolean sent, boolean seen,
 			boolean read) {
-		return new BlogInvitationRequest(id, msg.getSessionId(), contactId,
-				msg.getBlogAuthorName(), msg.getMessage(), available, time,
-				local, sent, seen, read);
+		return new BlogInvitationRequest(id, msg.getSessionId(),
+				msg.getGroupId(), contactId, msg.getBlogAuthorName(),
+				msg.getMessage(), available, time, local, sent, seen, read);
 	}
 
 	@Override
 	protected InvitationMessage createInvitationResponse(MessageId id,
-			SessionId sessionId, ContactId contactId, boolean accept, long time,
+			SessionId sessionId, GroupId groupId, ContactId contactId,
+			boolean accept, long time,
 			boolean local, boolean sent, boolean seen, boolean read) {
-		return new BlogInvitationResponse(id, sessionId, contactId, accept,
-				time, local, sent, seen, read);
+		return new BlogInvitationResponse(id, sessionId, groupId, contactId,
+				accept, time, local, sent, seen, read);
 	}
 
 	@Override
@@ -163,7 +166,7 @@ class BlogSharingManagerImpl extends
 		private final BlogFactory blogFactory;
 		private final BlogManager blogManager;
 
-		SFactory(AuthorFactory authorFactory, BlogFactory BlogFactory,
+		private SFactory(AuthorFactory authorFactory, BlogFactory BlogFactory,
 				BlogManager BlogManager) {
 			this.authorFactory = authorFactory;
 			this.blogFactory = BlogFactory;
@@ -230,11 +233,13 @@ class BlogSharingManagerImpl extends
 		}
 
 		@Override
-		public BlogInvitation build(BlogSharerSessionState localState) {
+		public BlogInvitation build(BlogSharerSessionState localState,
+				long time) {
 			return new BlogInvitation(localState.getGroupId(),
 					localState.getSessionId(), localState.getBlogTitle(),
 					localState.getBlogDesc(), localState.getBlogAuthorName(),
-					localState.getBlogPublicKey(), localState.getMessage());
+					localState.getBlogPublicKey(), time,
+					localState.getMessage());
 		}
 	}
 
@@ -249,20 +254,21 @@ class BlogSharingManagerImpl extends
 			String blogDesc = d.getString(BLOG_DESC);
 			String blogAuthorName = d.getString(BLOG_AUTHOR_NAME);
 			byte[] blogPublicKey = d.getRaw(BLOG_PUBLIC_KEY);
+			MessageId invitationId = new MessageId(d.getRaw(INVITATION_ID));
 			return new BlogInviteeSessionState(sessionId, storageId,
 					groupId, state, contactId, blogId, blogTitle, blogDesc,
-					blogAuthorName, blogPublicKey);
+					blogAuthorName, blogPublicKey, invitationId);
 		}
 
 		@Override
 		public BlogInviteeSessionState build(SessionId sessionId,
 				MessageId storageId, GroupId groupId,
 				InviteeSessionState.State state, ContactId contactId,
-				Blog blog) {
+				Blog blog, MessageId invitationId) {
 			return new BlogInviteeSessionState(sessionId, storageId,
 					groupId, state, contactId, blog.getId(), blog.getName(),
 					blog.getDescription(), blog.getAuthor().getName(),
-					blog.getAuthor().getPublicKey());
+					blog.getAuthor().getPublicKey(), invitationId);
 		}
 	}
 
@@ -277,9 +283,13 @@ class BlogSharingManagerImpl extends
 			String blogDesc = d.getString(BLOG_DESC);
 			String blogAuthorName = d.getString(BLOG_AUTHOR_NAME);
 			byte[] blogPublicKey = d.getRaw(BLOG_PUBLIC_KEY);
+			MessageId responseId = null;
+			byte[] responseIdBytes = d.getOptionalRaw(RESPONSE_ID);
+			if (responseIdBytes != null)
+				responseId = new MessageId(responseIdBytes);
 			return new BlogSharerSessionState(sessionId, storageId,
 					groupId, state, contactId, blogId, blogTitle, blogDesc,
-					blogAuthorName, blogPublicKey);
+					blogAuthorName, blogPublicKey, responseId);
 		}
 
 		@Override
@@ -290,7 +300,7 @@ class BlogSharingManagerImpl extends
 			return new BlogSharerSessionState(sessionId, storageId,
 					groupId, state, contactId, blog.getId(), blog.getName(),
 					blog.getDescription(), blog.getAuthor().getName(),
-					blog.getAuthor().getPublicKey());
+					blog.getAuthor().getPublicKey(), null);
 		}
 	}
 
@@ -299,16 +309,21 @@ class BlogSharingManagerImpl extends
 
 		private final SFactory sFactory;
 
-		IRFactory(SFactory sFactory) {
+		private IRFactory(SFactory sFactory) {
 			this.sFactory = sFactory;
 		}
 
 		@Override
 		public BlogInvitationReceivedEvent build(
-				BlogInviteeSessionState localState) {
+				BlogInviteeSessionState localState, long time, String msg) {
 			Blog blog = sFactory.parse(localState);
 			ContactId contactId = localState.getContactId();
-			return new BlogInvitationReceivedEvent(blog, contactId);
+			BlogInvitationRequest request =
+					new BlogInvitationRequest(localState.getInvitationId(),
+							localState.getSessionId(), localState.getGroupId(),
+							contactId, blog.getAuthor().getName(), msg, true,
+							time, false, false, false, false);
+			return new BlogInvitationReceivedEvent(blog, contactId, request);
 		}
 	}
 
@@ -316,10 +331,18 @@ class BlogSharingManagerImpl extends
 			InvitationResponseReceivedEventFactory<BlogSharerSessionState, BlogInvitationResponseReceivedEvent> {
 		@Override
 		public BlogInvitationResponseReceivedEvent build(
-				BlogSharerSessionState localState) {
+				BlogSharerSessionState localState, boolean accept, long time) {
 			String title = localState.getBlogTitle();
 			ContactId c = localState.getContactId();
-			return new BlogInvitationResponseReceivedEvent(title, c);
+			MessageId responseId = localState.getResponseId();
+			if (responseId == null)
+				throw new IllegalStateException("No responseId");
+			BlogInvitationResponse response =
+					new BlogInvitationResponse(responseId,
+							localState.getSessionId(), localState.getGroupId(),
+							localState.getContactId(), accept, time, false,
+							false, false, false);
+			return new BlogInvitationResponseReceivedEvent(title, c, response);
 		}
 	}
 }
diff --git a/briar-core/src/org/briarproject/sharing/ForumInviteeSessionState.java b/briar-core/src/org/briarproject/sharing/ForumInviteeSessionState.java
index f8ac1629b121deef1f45dd777af78df2b920e9ea..79c732e932702417cea9bfdf837b1b20739111c2 100644
--- a/briar-core/src/org/briarproject/sharing/ForumInviteeSessionState.java
+++ b/briar-core/src/org/briarproject/sharing/ForumInviteeSessionState.java
@@ -5,6 +5,7 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
 
 import static org.briarproject.api.forum.ForumConstants.FORUM_NAME;
 import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
@@ -16,8 +17,10 @@ public class ForumInviteeSessionState extends InviteeSessionState {
 
 	public ForumInviteeSessionState(SessionId sessionId, MessageId storageId,
 			GroupId groupId, State state, ContactId contactId, GroupId forumId,
-			String forumName, byte[] forumSalt) {
-		super(sessionId, storageId, groupId, state, contactId, forumId);
+			String forumName, byte[] forumSalt,
+			@NotNull MessageId invitationId) {
+		super(sessionId, storageId, groupId, state, contactId, forumId,
+				invitationId);
 
 		this.forumName = forumName;
 		this.forumSalt = forumSalt;
diff --git a/briar-core/src/org/briarproject/sharing/ForumSharerSessionState.java b/briar-core/src/org/briarproject/sharing/ForumSharerSessionState.java
index 4e0fd05049195ba6babb25fd35eff271da46f249..1403b75a67aae8137c4fe2568cfa79a6a880d494 100644
--- a/briar-core/src/org/briarproject/sharing/ForumSharerSessionState.java
+++ b/briar-core/src/org/briarproject/sharing/ForumSharerSessionState.java
@@ -5,6 +5,7 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
 
 import static org.briarproject.api.forum.ForumConstants.FORUM_NAME;
 import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
@@ -16,8 +17,10 @@ class ForumSharerSessionState extends SharerSessionState {
 
 	ForumSharerSessionState(SessionId sessionId, MessageId storageId,
 			GroupId groupId, State state, ContactId contactId, GroupId forumId,
-			String forumName, byte[] forumSalt) {
-		super(sessionId, storageId, groupId, state, contactId, forumId);
+			String forumName, byte[] forumSalt,
+			@Nullable MessageId responseId) {
+		super(sessionId, storageId, groupId, state, contactId, forumId,
+				responseId);
 
 		this.forumName = forumName;
 		this.forumSalt = forumSalt;
diff --git a/briar-core/src/org/briarproject/sharing/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/sharing/ForumSharingManagerImpl.java
index 89bf145cc7905e2ae25f95309de364003d454250..c78cda5bcc22b4bde0bf255f89cc7128791e08b3 100644
--- a/briar-core/src/org/briarproject/sharing/ForumSharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/sharing/ForumSharingManagerImpl.java
@@ -2,8 +2,8 @@ package org.briarproject.sharing;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
-import org.briarproject.api.clients.MessageQueueManager;
 import org.briarproject.api.clients.ContactGroupFactory;
+import org.briarproject.api.clients.MessageQueueManager;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
@@ -35,6 +35,8 @@ import javax.inject.Inject;
 
 import static org.briarproject.api.forum.ForumConstants.FORUM_NAME;
 import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
+import static org.briarproject.api.sharing.SharingConstants.INVITATION_ID;
+import static org.briarproject.api.sharing.SharingConstants.RESPONSE_ID;
 
 class ForumSharingManagerImpl extends
 		SharingManagerImpl<Forum, ForumInvitation, ForumInviteeSessionState, ForumSharerSessionState, ForumInvitationReceivedEvent, ForumInvitationResponseReceivedEvent>
@@ -82,17 +84,18 @@ class ForumSharingManagerImpl extends
 			ForumInvitation msg, ContactId contactId, boolean available,
 			long time, boolean local, boolean sent, boolean seen,
 			boolean read) {
-		return new ForumInvitationRequest(id, msg.getSessionId(), contactId,
-				msg.getForumName(), msg.getMessage(), available, time, local,
-				sent, seen, read);
+		return new ForumInvitationRequest(id, msg.getSessionId(),
+				msg.getGroupId(), contactId, msg.getForumName(),
+				msg.getMessage(), available, time, local, sent, seen, read);
 	}
 
 	@Override
 	protected InvitationMessage createInvitationResponse(MessageId id,
-			SessionId sessionId, ContactId contactId, boolean accept,
-			long time, boolean local, boolean sent, boolean seen, boolean read) {
-		return new ForumInvitationResponse(id, sessionId, contactId, accept,
-				time, local, sent, seen, read);
+			SessionId sessionId, GroupId groupId, ContactId contactId,
+			boolean accept, long time, boolean local, boolean sent,
+			boolean seen, boolean read) {
+		return new ForumInvitationResponse(id, sessionId, groupId, contactId,
+				accept, time, local, sent, seen, read);
 	}
 
 	@Override
@@ -136,7 +139,7 @@ class ForumSharingManagerImpl extends
 		private final ForumFactory forumFactory;
 		private final ForumManager forumManager;
 
-		SFactory(ForumFactory forumFactory, ForumManager forumManager) {
+		private SFactory(ForumFactory forumFactory, ForumManager forumManager) {
 			this.forumFactory = forumFactory;
 			this.forumManager = forumManager;
 		}
@@ -185,10 +188,11 @@ class ForumSharingManagerImpl extends
 		}
 
 		@Override
-		public ForumInvitation build(ForumSharerSessionState localState) {
+		public ForumInvitation build(ForumSharerSessionState localState,
+				long time) {
 			return new ForumInvitation(localState.getGroupId(),
 					localState.getSessionId(), localState.getForumName(),
-					localState.getForumSalt(), localState.getMessage());
+					localState.getForumSalt(), time, localState.getMessage());
 		}
 	}
 
@@ -201,18 +205,20 @@ class ForumSharingManagerImpl extends
 				GroupId forumId, BdfDictionary d) throws FormatException {
 			String forumName = d.getString(FORUM_NAME);
 			byte[] forumSalt = d.getRaw(FORUM_SALT);
+			MessageId invitationId = new MessageId(d.getRaw(INVITATION_ID));
 			return new ForumInviteeSessionState(sessionId, storageId,
-					groupId, state, contactId, forumId, forumName, forumSalt);
+					groupId, state, contactId, forumId, forumName, forumSalt,
+					invitationId);
 		}
 
 		@Override
 		public ForumInviteeSessionState build(SessionId sessionId,
 				MessageId storageId, GroupId groupId,
 				InviteeSessionState.State state, ContactId contactId,
-				Forum forum) {
+				Forum forum, MessageId invitationId) {
 			return new ForumInviteeSessionState(sessionId, storageId,
 					groupId, state, contactId, forum.getId(), forum.getName(),
-					forum.getSalt());
+					forum.getSalt(), invitationId);
 		}
 	}
 
@@ -225,8 +231,13 @@ class ForumSharingManagerImpl extends
 				GroupId forumId, BdfDictionary d) throws FormatException {
 			String forumName = d.getString(FORUM_NAME);
 			byte[] forumSalt = d.getRaw(FORUM_SALT);
+			MessageId responseId = null;
+			byte[] responseIdBytes = d.getOptionalRaw(RESPONSE_ID);
+			if (responseIdBytes != null)
+				responseId = new MessageId(responseIdBytes);
 			return new ForumSharerSessionState(sessionId, storageId,
-					groupId, state, contactId, forumId, forumName, forumSalt);
+					groupId, state, contactId, forumId, forumName, forumSalt,
+					responseId);
 		}
 
 		@Override
@@ -236,7 +247,7 @@ class ForumSharingManagerImpl extends
 				Forum forum) {
 			return new ForumSharerSessionState(sessionId, storageId,
 					groupId, state, contactId, forum.getId(), forum.getName(),
-					forum.getSalt());
+					forum.getSalt(), null);
 		}
 	}
 
@@ -245,16 +256,20 @@ class ForumSharingManagerImpl extends
 
 		private final SFactory sFactory;
 
-		IRFactory(SFactory sFactory) {
+		private IRFactory(SFactory sFactory) {
 			this.sFactory = sFactory;
 		}
 
 		@Override
 		public ForumInvitationReceivedEvent build(
-				ForumInviteeSessionState localState) {
+				ForumInviteeSessionState localState, long time, String msg) {
 			Forum forum = sFactory.parse(localState);
 			ContactId contactId = localState.getContactId();
-			return new ForumInvitationReceivedEvent(forum, contactId);
+			ForumInvitationRequest request = new ForumInvitationRequest(
+					localState.getInvitationId(), localState.getSessionId(),
+					localState.getGroupId(), contactId, forum.getName(), msg,
+					true, time, false, false, false, false);
+			return new ForumInvitationReceivedEvent(forum, contactId, request);
 		}
 	}
 
@@ -262,10 +277,17 @@ class ForumSharingManagerImpl extends
 			InvitationResponseReceivedEventFactory<ForumSharerSessionState, ForumInvitationResponseReceivedEvent> {
 		@Override
 		public ForumInvitationResponseReceivedEvent build(
-				ForumSharerSessionState localState) {
+				ForumSharerSessionState localState, boolean accept, long time) {
 			String name = localState.getForumName();
 			ContactId c = localState.getContactId();
-			return new ForumInvitationResponseReceivedEvent(name, c);
+			MessageId responseId = localState.getResponseId();
+			if (responseId == null)
+				throw new IllegalStateException("No responseId");
+			ForumInvitationResponse response = new ForumInvitationResponse(
+					responseId, localState.getSessionId(),
+					localState.getGroupId(), localState.getContactId(), accept,
+					time, false, false, false, false);
+			return new ForumInvitationResponseReceivedEvent(name, c, response);
 		}
 	}
 }
diff --git a/briar-core/src/org/briarproject/sharing/InvitationFactory.java b/briar-core/src/org/briarproject/sharing/InvitationFactory.java
index be82501e8f0c8d1e360eea579f3c75f45d1836d5..2c2d32c2938504279ec9431d67fcefcd64bfbac8 100644
--- a/briar-core/src/org/briarproject/sharing/InvitationFactory.java
+++ b/briar-core/src/org/briarproject/sharing/InvitationFactory.java
@@ -5,5 +5,5 @@ import org.briarproject.api.sharing.SharingMessage;
 public interface InvitationFactory<I extends SharingMessage.Invitation, SS extends SharerSessionState> extends
 		org.briarproject.api.sharing.InvitationFactory<I> {
 
-	I build(SS localState);
+	I build(SS localState, long time);
 }
diff --git a/briar-core/src/org/briarproject/sharing/InvitationReceivedEventFactory.java b/briar-core/src/org/briarproject/sharing/InvitationReceivedEventFactory.java
index bdedeee488b931c38605c306290a21d2adce1608..9172656e9f74e1f2bc37747d1a07ff4798f6d9e8 100644
--- a/briar-core/src/org/briarproject/sharing/InvitationReceivedEventFactory.java
+++ b/briar-core/src/org/briarproject/sharing/InvitationReceivedEventFactory.java
@@ -1,8 +1,8 @@
 package org.briarproject.sharing;
 
-import org.briarproject.api.event.InvitationReceivedEvent;
+import org.briarproject.api.event.InvitationRequestReceivedEvent;
 
-public interface InvitationReceivedEventFactory<IS extends InviteeSessionState, IR extends InvitationReceivedEvent> {
+public interface InvitationReceivedEventFactory<IS extends InviteeSessionState, IR extends InvitationRequestReceivedEvent> {
 
-	IR build(IS localState);
+	IR build(IS localState, long time, String msg);
 }
diff --git a/briar-core/src/org/briarproject/sharing/InvitationResponseReceivedEventFactory.java b/briar-core/src/org/briarproject/sharing/InvitationResponseReceivedEventFactory.java
index 4420f30edd003c2727e162649fa218943a99cf91..ea5cdd91abea250acf4c633de7c08e21a432f86a 100644
--- a/briar-core/src/org/briarproject/sharing/InvitationResponseReceivedEventFactory.java
+++ b/briar-core/src/org/briarproject/sharing/InvitationResponseReceivedEventFactory.java
@@ -4,5 +4,5 @@ import org.briarproject.api.event.InvitationResponseReceivedEvent;
 
 public interface InvitationResponseReceivedEventFactory<SS extends SharerSessionState, IRR extends InvitationResponseReceivedEvent> {
 
-	IRR build(SS localState);
+	IRR build(SS localState, boolean accept, long time);
 }
diff --git a/briar-core/src/org/briarproject/sharing/InviteeEngine.java b/briar-core/src/org/briarproject/sharing/InviteeEngine.java
index 766a37e4142f57a5033e9fe091c675bb43fd7f22..96af6e3207da4eb44e35a4c2d61a3cc5e76ae00b 100644
--- a/briar-core/src/org/briarproject/sharing/InviteeEngine.java
+++ b/briar-core/src/org/briarproject/sharing/InviteeEngine.java
@@ -3,7 +3,9 @@ package org.briarproject.sharing;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ProtocolEngine;
 import org.briarproject.api.event.Event;
-import org.briarproject.api.event.InvitationReceivedEvent;
+import org.briarproject.api.event.InvitationRequestReceivedEvent;
+import org.briarproject.api.sharing.SharingMessage.Invitation;
+import org.briarproject.api.system.Clock;
 
 import java.util.Collections;
 import java.util.List;
@@ -23,16 +25,20 @@ import static org.briarproject.api.sharing.SharingConstants.TASK_UNSHARE_SHAREAB
 import static org.briarproject.api.sharing.SharingMessage.BaseMessage;
 import static org.briarproject.api.sharing.SharingMessage.SimpleMessage;
 
-class InviteeEngine<IS extends InviteeSessionState, IR extends InvitationReceivedEvent>
+class InviteeEngine<IS extends InviteeSessionState, IR extends InvitationRequestReceivedEvent>
 		implements ProtocolEngine<InviteeSessionState.Action, IS, BaseMessage> {
 
 	private static final Logger LOG =
 			Logger.getLogger(InviteeEngine.class.getName());
 
 	private final InvitationReceivedEventFactory<IS, IR> invitationReceivedEventFactory;
+	private final Clock clock;
 
-	InviteeEngine(InvitationReceivedEventFactory<IS, IR> invitationReceivedEventFactory) {
+	InviteeEngine(
+			InvitationReceivedEventFactory<IS, IR> invitationReceivedEventFactory,
+			Clock clock) {
 		this.invitationReceivedEventFactory = invitationReceivedEventFactory;
+		this.clock = clock;
 	}
 
 	@Override
@@ -63,19 +69,22 @@ class InviteeEngine<IS extends InviteeSessionState, IR extends InvitationReceive
 				if (action == InviteeSessionState.Action.LOCAL_ACCEPT) {
 					localState.setTask(TASK_ADD_SHARED_SHAREABLE);
 					msg = new SimpleMessage(SHARE_MSG_TYPE_ACCEPT,
-							localState.getGroupId(), localState.getSessionId());
+							localState.getGroupId(), localState.getSessionId(),
+							clock.currentTimeMillis());
 				} else {
 					localState.setTask(
 							TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US);
 					msg = new SimpleMessage(SHARE_MSG_TYPE_DECLINE,
-							localState.getGroupId(), localState.getSessionId());
+							localState.getGroupId(), localState.getSessionId(),
+							clock.currentTimeMillis());
 				}
 				messages = Collections.singletonList(msg);
 				logLocalAction(currentState, localState, msg);
 			}
 			else if (action == InviteeSessionState.Action.LOCAL_LEAVE) {
 				BaseMessage msg = new SimpleMessage(SHARE_MSG_TYPE_LEAVE,
-						localState.getGroupId(), localState.getSessionId());
+						localState.getGroupId(), localState.getSessionId(),
+						clock.currentTimeMillis());
 				messages = Collections.singletonList(msg);
 				logLocalAction(currentState, localState, msg);
 			}
@@ -133,7 +142,9 @@ class InviteeEngine<IS extends InviteeSessionState, IR extends InvitationReceive
 			// we have just received our invitation
 			else if (action == InviteeSessionState.Action.REMOTE_INVITATION) {
 				localState.setTask(TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US);
-				Event event = invitationReceivedEventFactory.build(localState);
+				Invitation invitation = (Invitation) msg;
+				Event event = invitationReceivedEventFactory.build(localState,
+						msg.getTime(), invitation.getMessage());
 				events = Collections.singletonList(event);
 			}
 			else {
@@ -203,7 +214,7 @@ class InviteeEngine<IS extends InviteeSessionState, IR extends InvitationReceive
 		localState.setState(InviteeSessionState.State.ERROR);
 		BaseMessage msg =
 				new SimpleMessage(SHARE_MSG_TYPE_ABORT, localState.getGroupId(),
-						localState.getSessionId());
+						localState.getSessionId(), clock.currentTimeMillis());
 		List<BaseMessage> messages = Collections.singletonList(msg);
 
 		List<Event> events = Collections.emptyList();
diff --git a/briar-core/src/org/briarproject/sharing/InviteeSessionState.java b/briar-core/src/org/briarproject/sharing/InviteeSessionState.java
index 6082c7ba8dfa3100ad56d539b419f14e853cb8e3..1aa65b2320245ef655912328ef27a8c86769a9ad 100644
--- a/briar-core/src/org/briarproject/sharing/InviteeSessionState.java
+++ b/briar-core/src/org/briarproject/sharing/InviteeSessionState.java
@@ -5,7 +5,9 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
 
+import static org.briarproject.api.sharing.SharingConstants.INVITATION_ID;
 import static org.briarproject.api.sharing.SharingConstants.IS_SHARER;
 import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT;
 import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
@@ -21,19 +23,23 @@ import static org.briarproject.sharing.InviteeSessionState.Action.REMOTE_LEAVE;
 public abstract class InviteeSessionState extends SharingSessionState {
 
 	private State state;
+	@NotNull
+	private final MessageId invitationId;
 
 	public InviteeSessionState(SessionId sessionId, MessageId storageId,
 			GroupId groupId, State state, ContactId contactId,
-			GroupId shareableId) {
+			GroupId shareableId, @NotNull MessageId invitationId) {
 
 		super(sessionId, storageId, groupId, contactId, shareableId);
 		this.state = state;
+		this.invitationId = invitationId;
 	}
 
 	public BdfDictionary toBdfDictionary() {
 		BdfDictionary d = super.toBdfDictionary();
 		d.put(STATE, getState().getValue());
 		d.put(IS_SHARER, false);
+		d.put(INVITATION_ID, invitationId);
 		return d;
 	}
 
@@ -45,6 +51,11 @@ public abstract class InviteeSessionState extends SharingSessionState {
 		return state;
 	}
 
+	@NotNull
+	public MessageId getInvitationId() {
+		return invitationId;
+	}
+
 	public enum State {
 		ERROR(0),
 		AWAIT_INVITATION(1) {
diff --git a/briar-core/src/org/briarproject/sharing/InviteeSessionStateFactory.java b/briar-core/src/org/briarproject/sharing/InviteeSessionStateFactory.java
index 57b8adcfaa4359a1e4dbdddf72c3841f467fa2e2..0a514f519213cff5309b76797409261fc1c26e20 100644
--- a/briar-core/src/org/briarproject/sharing/InviteeSessionStateFactory.java
+++ b/briar-core/src/org/briarproject/sharing/InviteeSessionStateFactory.java
@@ -15,5 +15,6 @@ public interface InviteeSessionStateFactory<S extends Shareable, IS extends Invi
 			GroupId shareableId, BdfDictionary d) throws FormatException;
 
 	IS build(SessionId sessionId, MessageId storageId, GroupId groupId,
-			InviteeSessionState.State state, ContactId contactId, S shareable);
+			InviteeSessionState.State state, ContactId contactId, S shareable,
+			MessageId invitationId);
 }
diff --git a/briar-core/src/org/briarproject/sharing/SharerEngine.java b/briar-core/src/org/briarproject/sharing/SharerEngine.java
index 4f7d23f83b7af61346b88be3acfdeec81f5ea18c..3ddb17760671c8618b3a8d761d0c454aed5f01e3 100644
--- a/briar-core/src/org/briarproject/sharing/SharerEngine.java
+++ b/briar-core/src/org/briarproject/sharing/SharerEngine.java
@@ -4,6 +4,7 @@ import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ProtocolEngine;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.InvitationResponseReceivedEvent;
+import org.briarproject.api.system.Clock;
 
 import java.util.Collections;
 import java.util.List;
@@ -22,6 +23,8 @@ import static org.briarproject.api.sharing.SharingConstants.TASK_UNSHARE_SHAREAB
 import static org.briarproject.api.sharing.SharingMessage.BaseMessage;
 import static org.briarproject.api.sharing.SharingMessage.Invitation;
 import static org.briarproject.api.sharing.SharingMessage.SimpleMessage;
+import static org.briarproject.sharing.SharerSessionState.Action.REMOTE_ACCEPT;
+import static org.briarproject.sharing.SharerSessionState.Action.REMOTE_DECLINE;
 
 class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR extends InvitationResponseReceivedEvent>
 		implements ProtocolEngine<SharerSessionState.Action, SS, BaseMessage> {
@@ -32,12 +35,15 @@ class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR exte
 	private final InvitationFactory<I, SS> invitationFactory;
 	private final InvitationResponseReceivedEventFactory<SS, IRR>
 			invitationResponseReceivedEventFactory;
+	private final Clock clock;
 
 	SharerEngine(InvitationFactory<I, SS> invitationFactory,
-			InvitationResponseReceivedEventFactory<SS, IRR> invitationResponseReceivedEventFactory) {
+			InvitationResponseReceivedEventFactory<SS, IRR> invitationResponseReceivedEventFactory,
+			Clock clock) {
 		this.invitationFactory = invitationFactory;
 		this.invitationResponseReceivedEventFactory =
 				invitationResponseReceivedEventFactory;
+		this.clock = clock;
 	}
 
 	@Override
@@ -65,7 +71,8 @@ class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR exte
 			List<Event> events = Collections.emptyList();
 
 			if (action == SharerSessionState.Action.LOCAL_INVITATION) {
-				BaseMessage msg = invitationFactory.build(localState);
+				BaseMessage msg = invitationFactory.build(localState,
+						clock.currentTimeMillis());
 				messages = Collections.singletonList(msg);
 				logLocalAction(currentState, nextState, msg);
 
@@ -74,7 +81,8 @@ class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR exte
 						.setTask(TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US);
 			} else if (action == SharerSessionState.Action.LOCAL_LEAVE) {
 				BaseMessage msg = new SimpleMessage(SHARE_MSG_TYPE_LEAVE,
-						localState.getGroupId(), localState.getSessionId());
+						localState.getGroupId(), localState.getSessionId(),
+						clock.currentTimeMillis());
 				messages = Collections.singletonList(msg);
 				logLocalAction(currentState, nextState, msg);
 			} else {
@@ -122,9 +130,8 @@ class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR exte
 				deleteMsg = true;
 			}
 			// we have sent our invitation and just got a response
-			else if (action == SharerSessionState.Action.REMOTE_ACCEPT ||
-					action == SharerSessionState.Action.REMOTE_DECLINE) {
-				if (action == SharerSessionState.Action.REMOTE_ACCEPT) {
+			else if (action == REMOTE_ACCEPT || action == REMOTE_DECLINE) {
+				if (action == REMOTE_ACCEPT) {
 					localState.setTask(TASK_SHARE_SHAREABLE);
 				} else {
 					// this ensures that the forum can be shared again
@@ -132,7 +139,8 @@ class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR exte
 							TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US);
 				}
 				Event event = invitationResponseReceivedEventFactory
-						.build(localState);
+						.build(localState, action == REMOTE_ACCEPT,
+								msg.getTime());
 				events = Collections.singletonList(event);
 			} else {
 				throw new IllegalArgumentException("Bad state");
@@ -204,7 +212,8 @@ class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR exte
 
 		localState.setState(SharerSessionState.State.ERROR);
 		BaseMessage msg = new SimpleMessage(SHARE_MSG_TYPE_ABORT,
-				localState.getGroupId(), localState.getSessionId());
+				localState.getGroupId(), localState.getSessionId(),
+				clock.currentTimeMillis());
 		List<BaseMessage> messages = Collections.singletonList(msg);
 
 		List<Event> events = Collections.emptyList();
diff --git a/briar-core/src/org/briarproject/sharing/SharerSessionState.java b/briar-core/src/org/briarproject/sharing/SharerSessionState.java
index 6cbb271561aea089a18e2e58357d4dabc7d88472..a56be906785b5f96c63f806fbb232f7c8b3bb008 100644
--- a/briar-core/src/org/briarproject/sharing/SharerSessionState.java
+++ b/briar-core/src/org/briarproject/sharing/SharerSessionState.java
@@ -5,8 +5,10 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
 
 import static org.briarproject.api.sharing.SharingConstants.IS_SHARER;
+import static org.briarproject.api.sharing.SharingConstants.RESPONSE_ID;
 import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT;
 import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT;
 import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE;
@@ -22,20 +24,25 @@ import static org.briarproject.sharing.SharerSessionState.Action.REMOTE_LEAVE;
 public abstract class SharerSessionState extends SharingSessionState {
 
 	private State state;
+	@Nullable
 	private String msg = null;
+	@Nullable
+	private MessageId responseId;
 
 	public SharerSessionState(SessionId sessionId, MessageId storageId,
 			GroupId groupId, State state, ContactId contactId,
-			GroupId shareableId) {
+			GroupId shareableId, @Nullable MessageId responseId) {
 
 		super(sessionId, storageId, groupId, contactId, shareableId);
 		this.state = state;
+		this.responseId = responseId;
 	}
 
 	public BdfDictionary toBdfDictionary() {
 		BdfDictionary d = super.toBdfDictionary();
 		d.put(STATE, getState().getValue());
 		d.put(IS_SHARER, true);
+		if (responseId != null) d.put(RESPONSE_ID, responseId);
 		return d;
 	}
 
@@ -55,6 +62,15 @@ public abstract class SharerSessionState extends SharingSessionState {
 		return this.msg;
 	}
 
+	public void setResponseId(@Nullable MessageId responseId) {
+		this.responseId = responseId;
+	}
+
+	@Nullable
+	public MessageId getResponseId() {
+		return responseId;
+	}
+
 	public enum State {
 		ERROR(0),
 		PREPARE_INVITATION(1) {
diff --git a/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java b/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
index 805b9d0b4b0aad5bacf9e2147914da68efce0424..9def8487dc2b82b803b6baff9b0092062c13dc61 100644
--- a/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
@@ -4,8 +4,8 @@ import org.briarproject.api.Bytes;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.Client;
 import org.briarproject.api.clients.ClientHelper;
-import org.briarproject.api.clients.MessageQueueManager;
 import org.briarproject.api.clients.ContactGroupFactory;
+import org.briarproject.api.clients.MessageQueueManager;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -22,7 +22,7 @@ import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.NoSuchMessageException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.Event;
-import org.briarproject.api.event.InvitationReceivedEvent;
+import org.briarproject.api.event.InvitationRequestReceivedEvent;
 import org.briarproject.api.event.InvitationResponseReceivedEvent;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sharing.InvitationItem;
@@ -36,7 +36,7 @@ import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
 import org.briarproject.api.system.Clock;
-import org.briarproject.clients.BdfIncomingMessageHook;
+import org.briarproject.clients.ConversationClientImpl;
 import org.briarproject.util.StringUtils;
 
 import java.io.IOException;
@@ -85,8 +85,8 @@ import static org.briarproject.api.sharing.SharingMessage.Invitation;
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 import static org.briarproject.sharing.InviteeSessionState.State.AWAIT_LOCAL_RESPONSE;
 
-abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS extends InviteeSessionState, SS extends SharerSessionState, IR extends InvitationReceivedEvent, IRR extends InvitationResponseReceivedEvent>
-		extends BdfIncomingMessageHook
+abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS extends InviteeSessionState, SS extends SharerSessionState, IR extends InvitationRequestReceivedEvent, IRR extends InvitationResponseReceivedEvent>
+		extends ConversationClientImpl
 		implements SharingManager<S>, Client, AddContactHook,
 		RemoveContactHook {
 
@@ -117,13 +117,14 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 
 	public abstract ClientId getClientId();
 
-	protected abstract InvitationMessage createInvitationRequest(MessageId id, I msg,
-			ContactId contactId, boolean available, long time, boolean local,
-			boolean sent, boolean seen, boolean read);
+	protected abstract InvitationMessage createInvitationRequest(MessageId id,
+			I msg, ContactId contactId, boolean available, long time,
+			boolean local, boolean sent, boolean seen, boolean read);
 
 	protected abstract InvitationMessage createInvitationResponse(MessageId id,
-			SessionId sessionId, ContactId contactId, boolean accept, long time,
-			boolean local, boolean sent, boolean seen, boolean read);
+			SessionId sessionId, GroupId groupId, ContactId contactId,
+			boolean accept, long time, boolean local, boolean sent,
+			boolean seen, boolean read);
 
 	protected abstract ShareableFactory<S, I, IS, SS> getSFactory();
 
@@ -219,9 +220,10 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 					checkForRaceCondition(txn, f, contact);
 
 				// initialize state and process invitation
-				IS state = initializeInviteeState(txn, contactId, invitation);
+				IS state = initializeInviteeState(txn, contactId, invitation,
+						m.getId());
 				InviteeEngine<IS, IR> engine =
-						new InviteeEngine<IS, IR>(getIRFactory());
+						new InviteeEngine<IS, IR>(getIRFactory(), clock);
 				processInviteeStateUpdate(txn, m.getId(),
 						engine.onMessageReceived(state, msg));
 				trackIncomingMessage(txn, m);
@@ -233,9 +235,10 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 				msg.getType() == SHARE_MSG_TYPE_DECLINE) {
 			// we are a sharer who just received a response
 			SS state = getSessionStateForSharer(txn, sessionId);
+			state.setResponseId(m.getId());
 			SharerEngine<I, SS, IRR> engine =
 					new SharerEngine<I, SS, IRR>(getIFactory(),
-							getIRRFactory());
+							getIRRFactory(), clock);
 			processSharerStateUpdate(txn, m.getId(),
 					engine.onMessageReceived(state, msg));
 			trackIncomingMessage(txn, m);
@@ -248,14 +251,14 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 				SS state = (SS) s;
 				SharerEngine<I, SS, IRR> engine =
 						new SharerEngine<I, SS, IRR>(getIFactory(),
-								getIRRFactory());
+								getIRRFactory(), clock);
 				processSharerStateUpdate(txn, m.getId(),
 						engine.onMessageReceived(state, msg));
 			} else {
 				// we are an invitee and the sharer wants to leave or abort
 				IS state = (IS) s;
 				InviteeEngine<IS, IR> engine =
-						new InviteeEngine<IS, IR>(getIRFactory());
+						new InviteeEngine<IS, IR>(getIRFactory(), clock);
 				processInviteeStateUpdate(txn, m.getId(),
 						engine.onMessageReceived(state, msg));
 			}
@@ -285,13 +288,15 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 			// start engine and process its state update
 			SharerEngine<I, SS, IRR> engine =
 					new SharerEngine<I, SS, IRR>(getIFactory(),
-							getIRRFactory());
-			processSharerStateUpdate(txn, null,
+							getIRRFactory(), clock);
+			StateUpdate<SS, BaseMessage> update =
 					engine.onLocalAction(localState,
-							SharerSessionState.Action.LOCAL_INVITATION));
+							SharerSessionState.Action.LOCAL_INVITATION);
+			processSharerStateUpdate(txn, null, update);
 
 			// track message
-			long time = clock.currentTimeMillis();
+			// TODO handle this properly without engine hacks (#376)
+			long time = update.toSend.get(0).getTime();
 			trackMessage(txn, localState.getGroupId(), time, true);
 
 			txn.setComplete();
@@ -321,12 +326,14 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 
 			// start engine and process its state update
 			InviteeEngine<IS, IR> engine =
-					new InviteeEngine<IS, IR>(getIRFactory());
-			processInviteeStateUpdate(txn, null,
-					engine.onLocalAction(localState, localAction));
+					new InviteeEngine<IS, IR>(getIRFactory(), clock);
+			StateUpdate<IS, BaseMessage> update =
+					engine.onLocalAction(localState, localAction);
+			processInviteeStateUpdate(txn, null, update);
 
 			// track message
-			long time = clock.currentTimeMillis();
+			// TODO handle this properly without engine hacks (#376)
+			long time = update.toSend.get(0).getTime();
 			trackMessage(txn, localState.getGroupId(), time, true);
 
 			txn.setComplete();
@@ -387,8 +394,9 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 								.from(getIFactory(), group.getId(), d);
 						SessionId sessionId = msg.getSessionId();
 						InvitationMessage im = createInvitationResponse(
-								m.getKey(), sessionId, contactId, accept, time,
-								local, status.isSent(), status.isSeen(), read);
+								m.getKey(), sessionId, group.getId(), contactId,
+								accept, time, local, status.isSent(),
+								status.isSeen(), read);
 						list.add(im);
 					}
 					else {
@@ -555,6 +563,16 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		}
 	}
 
+	@Override
+	public GroupCount getGroupCount(Transaction txn, ContactId contactId)
+			throws DbException {
+
+		Contact contact = db.getContact(txn, contactId);
+		GroupId groupId = getContactGroup(contact).getId();
+
+		return getGroupCount(txn, groupId);
+	}
+
 	void removingShareable(Transaction txn, S f) throws DbException {
 		try {
 			for (Contact c : db.getContacts(txn)) {
@@ -642,7 +660,7 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 	}
 
 	private IS initializeInviteeState(Transaction txn,
-			ContactId contactId, I msg)
+			ContactId contactId, I msg, MessageId id)
 			throws FormatException, DbException {
 
 		Contact c = db.getContact(txn, contactId);
@@ -656,9 +674,10 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		Message m = clientHelper.createMessage(localGroup.getId(), now,
 				BdfList.of(mSalt));
 
-		IS s = getISFactory().build(msg.getSessionId(),
-				m.getId(), group.getId(),
-				InviteeSessionState.State.AWAIT_INVITATION, contactId, f);
+		IS s = getISFactory()
+				.build(msg.getSessionId(), m.getId(), group.getId(),
+						InviteeSessionState.State.AWAIT_INVITATION, contactId,
+						f, id);
 
 		// save local state to database
 		BdfDictionary d = s.toBdfDictionary();
@@ -898,19 +917,19 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 
 		byte[] body = clientHelper.toByteArray(m.toBdfList());
 		Group group = db.getGroup(txn, m.getGroupId());
-		long timestamp = clock.currentTimeMillis();
 
 		// add message itself as metadata
 		BdfDictionary d = m.toBdfDictionary();
 		d.put(LOCAL, true);
-		d.put(TIME, timestamp);
+		d.put(TIME, m.getTime());
 		Metadata meta = metadataEncoder.encode(d);
 
 		messageQueueManager
-				.sendMessage(txn, group, timestamp, body, meta);
+				.sendMessage(txn, group, m.getTime(), body, meta);
 	}
 
-	private Group getContactGroup(Contact c) {
+	@Override
+	protected Group getContactGroup(Contact c) {
 		return contactGroupFactory.createContactGroup(getClientId(), c);
 	}
 
@@ -930,14 +949,14 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 					SharerSessionState.Action.LOCAL_LEAVE;
 			SharerEngine<I, SS, IRR> engine =
 					new SharerEngine<I, SS, IRR>(getIFactory(),
-							getIRRFactory());
+							getIRRFactory(), clock);
 			processSharerStateUpdate(txn, null,
 					engine.onLocalAction((SS) state, action));
 		} else {
 			InviteeSessionState.Action action =
 					InviteeSessionState.Action.LOCAL_LEAVE;
 			InviteeEngine<IS, IR> engine =
-					new InviteeEngine<IS, IR>(getIRFactory());
+					new InviteeEngine<IS, IR>(getIRFactory(), clock);
 			processInviteeStateUpdate(txn, null,
 					engine.onLocalAction((IS) state, action));
 		}
diff --git a/briar-core/src/org/briarproject/sharing/SharingModule.java b/briar-core/src/org/briarproject/sharing/SharingModule.java
index a098fa1e143a5843fe097e456c3b622836616800..da616a43d7b20c7f3e520a3e3c5c2e91ca218020 100644
--- a/briar-core/src/org/briarproject/sharing/SharingModule.java
+++ b/briar-core/src/org/briarproject/sharing/SharingModule.java
@@ -9,6 +9,7 @@ import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.messaging.ConversationManager;
 import org.briarproject.api.system.Clock;
 
 import javax.inject.Inject;
@@ -52,6 +53,7 @@ public class SharingModule {
 			LifecycleManager lifecycleManager,
 			ContactManager contactManager,
 			MessageQueueManager messageQueueManager,
+			ConversationManager conversationManager,
 			BlogManager blogManager,
 			BlogSharingManagerImpl blogSharingManager) {
 
@@ -60,6 +62,7 @@ public class SharingModule {
 		contactManager.registerRemoveContactHook(blogSharingManager);
 		messageQueueManager.registerIncomingMessageHook(
 				BlogSharingManagerImpl.CLIENT_ID, blogSharingManager);
+		conversationManager.registerConversationClient(blogSharingManager);
 		blogManager.registerRemoveBlogHook(blogSharingManager);
 
 		return blogSharingManager;
@@ -86,6 +89,7 @@ public class SharingModule {
 			LifecycleManager lifecycleManager,
 			ContactManager contactManager,
 			MessageQueueManager messageQueueManager,
+			ConversationManager conversationManager,
 			ForumManager forumManager,
 			ForumSharingManagerImpl forumSharingManager) {
 
@@ -94,6 +98,7 @@ public class SharingModule {
 		contactManager.registerRemoveContactHook(forumSharingManager);
 		messageQueueManager.registerIncomingMessageHook(
 				ForumSharingManagerImpl.CLIENT_ID, forumSharingManager);
+		conversationManager.registerConversationClient(forumSharingManager);
 		forumManager.registerRemoveForumHook(forumSharingManager);
 
 		return forumSharingManager;
diff --git a/briar-tests/src/org/briarproject/introduction/IntroductionManagerImplTest.java b/briar-tests/src/org/briarproject/introduction/IntroductionManagerImplTest.java
index c02c29192bc06081234404467a633999c31ca4e2..19903224e5cb15ea301b3eef81c381f3f4497062 100644
--- a/briar-tests/src/org/briarproject/introduction/IntroductionManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/introduction/IntroductionManagerImplTest.java
@@ -96,13 +96,13 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 				TestUtils.getRandomBytes(MESSAGE_HEADER_LENGTH + 1)
 		);
 		metadataBefore = BdfDictionary.of(
-				new BdfEntry(GROUP_KEY_MSG_COUNT, 41L),
-				new BdfEntry(GROUP_KEY_UNREAD_COUNT, 0L),
-				new BdfEntry(GROUP_KEY_LATEST_MSG, 0L)
+				new BdfEntry(GROUP_KEY_MSG_COUNT, 41),
+				new BdfEntry(GROUP_KEY_UNREAD_COUNT, 0),
+				new BdfEntry(GROUP_KEY_LATEST_MSG, 0)
 		);
 		metadataAfter = BdfDictionary.of(
-				new BdfEntry(GROUP_KEY_MSG_COUNT, 42L),
-				new BdfEntry(GROUP_KEY_UNREAD_COUNT, 0L),
+				new BdfEntry(GROUP_KEY_MSG_COUNT, 42),
+				new BdfEntry(GROUP_KEY_UNREAD_COUNT, 0),
 				new BdfEntry(GROUP_KEY_LATEST_MSG, time)
 		);
 
@@ -273,8 +273,8 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 		final BdfDictionary state = new BdfDictionary();
 		txn = new Transaction(null, false);
 
-		metadataBefore.put(GROUP_KEY_UNREAD_COUNT, 1L);
-		metadataAfter.put(GROUP_KEY_UNREAD_COUNT, 2L);
+		metadataBefore.put(GROUP_KEY_UNREAD_COUNT, 1);
+		metadataAfter.put(GROUP_KEY_UNREAD_COUNT, 2);
 
 		context.checking(new Expectations() {{
 			oneOf(introduceeManager)
@@ -314,8 +314,8 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 
 		txn = new Transaction(null, false);
 
-		metadataBefore.put(GROUP_KEY_UNREAD_COUNT, 41L);
-		metadataAfter.put(GROUP_KEY_UNREAD_COUNT, 42L);
+		metadataBefore.put(GROUP_KEY_UNREAD_COUNT, 41);
+		metadataAfter.put(GROUP_KEY_UNREAD_COUNT, 42);
 
 		context.checking(new Expectations() {{
 			oneOf(clientHelper).getMessageMetadataAsDictionary(txn, sessionId);