diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index e402626affe6154e434dfd3d5c4bd8ce01cefbef..fc8fcd5289bfedca1eb5bb9377338417d8ce7865 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -69,11 +69,11 @@
 			android:label="@string/messages_title" >
 		</activity>
 		<activity
-			android:name="net.sf.briar.android.messages.ReadPrivateMessageActivity"
+			android:name=".android.messages.ReadPrivateMessageActivity"
 			android:label="@string/messages_title" >
 		</activity>
 		<activity
-			android:name="net.sf.briar.android.messages.WritePrivateMessageActivity"
+			android:name=".android.messages.WritePrivateMessageActivity"
 			android:label="@string/compose_message_title" >
 		</activity>
 	</application>
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java b/briar-android/src/net/sf/briar/android/contact/ContactComparator.java
deleted file mode 100644
index 8e9207c6fc1731c4f0ef1e288d2c358885c1b13f..0000000000000000000000000000000000000000
--- a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.sf.briar.android.contact;
-
-import java.util.Comparator;
-
-class ContactComparator implements Comparator<ContactListItem> {
-
-	static final ContactComparator INSTANCE = new ContactComparator();
-
-	public int compare(ContactListItem a, ContactListItem b) {
-		return String.CASE_INSENSITIVE_ORDER.compare(a.getContactName(),
-				b.getContactName());
-	}
-}
\ No newline at end of file
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
index fe5f673e9c463e3fdfe458824a53f105fbae4e93..5b01e6bb4fa69ae8ede754686b2d10c8d4ec82b7 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
@@ -6,6 +6,7 @@ import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -46,12 +47,13 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
 
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
 	@Inject private ConnectionRegistry connectionRegistry;
-
 	private ContactListAdapter adapter = null;
 
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(null);
@@ -87,8 +89,11 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 				serviceConnection, 0);
 
 		// Add some fake contacts to the database in a background thread
-		// FIXME: Remove this
-		final DatabaseComponent db = this.db;
+		insertFakeContacts();
+	}
+
+	// FIXME: Remove this
+	private void insertFakeContacts() {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
@@ -117,28 +122,10 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 	@Override
 	public void onResume() {
 		super.onResume();
-		reloadContactList();
-	}
-
-	@Override
-	public void onDestroy() {
-		super.onDestroy();
-		db.removeListener(this);
-		connectionRegistry.removeListener(this);
-		unbindService(serviceConnection);
-	}
-
-	public void onClick(View view) {
-		startActivity(new Intent(this, AddContactActivity.class));
-	}
-
-	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof ContactAddedEvent) reloadContactList();
-		else if(e instanceof ContactRemovedEvent) reloadContactList();
+		loadContacts();
 	}
 
-	private void reloadContactList() {
-		final DatabaseComponent db = this.db;
+	private void loadContacts() {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
@@ -148,8 +135,8 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 					Collection<Contact> contacts = db.getContacts();
 					if(LOG.isLoggable(INFO))
 						LOG.info("Loaded " + contacts.size() + " contacts");
-					// Update the contact list
-					updateContactList(contacts);
+					// Display the contacts in the UI
+					displayContacts(contacts);
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -162,7 +149,7 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 		});
 	}
 
-	private void updateContactList(final Collection<Contact> contacts) {
+	private void displayContacts(final Collection<Contact> contacts) {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				adapter.clear();
@@ -175,6 +162,24 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 		});
 	}
 
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		db.removeListener(this);
+		connectionRegistry.removeListener(this);
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		startActivity(new Intent(this, AddContactActivity.class));
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		// These events should be rare, so just reload the list
+		if(e instanceof ContactAddedEvent) loadContacts();
+		else if(e instanceof ContactRemovedEvent) loadContacts();
+	}
+
 	public void contactConnected(ContactId c) {
 		setConnected(c, true);
 	}
@@ -197,4 +202,15 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 			}
 		});
 	}
+
+	private static class ContactComparator
+	implements Comparator<ContactListItem> {
+
+		static final ContactComparator INSTANCE = new ContactComparator();
+
+		public int compare(ContactListItem a, ContactListItem b) {
+			return String.CASE_INSENSITIVE_ORDER.compare(a.getContactName(),
+					b.getContactName());
+		}
+	}
 }
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
index 148e73402b2de8c645c9531ee32e981ef9f88bfa..5873b07ce789a1eb9cde1e2fb1f7a3b8c7f75008 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
@@ -6,12 +6,9 @@ import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static net.sf.briar.api.Rating.UNRATED;
 
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -30,13 +27,13 @@ import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.db.NoSuchSubscriptionException;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
-import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.GroupMessageAddedEvent;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
-import net.sf.briar.api.db.event.RatingChangedEvent;
 import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
 import net.sf.briar.api.messaging.Author;
-import net.sf.briar.api.messaging.AuthorId;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.Message;
+import net.sf.briar.api.messaging.MessageId;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
@@ -58,14 +55,17 @@ OnClickListener, OnItemClickListener {
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
 
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
-
-	private GroupId groupId = null;
+	// The following fields must only be accessed from the UI thread
+	private final Set<MessageId> messageIds = new HashSet<MessageId>();
 	private String groupName = null;
 	private GroupAdapter adapter = null;
 	private ListView list = null;
 
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+	private volatile GroupId groupId = null;
+
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(null);
@@ -111,32 +111,22 @@ OnClickListener, OnItemClickListener {
 	@Override
 	public void onResume() {
 		super.onResume();
-		reloadMessageHeaders();
+		loadHeaders();
 	}
 
-	private void reloadMessageHeaders() {
-		final DatabaseComponent db = this.db;
-		final GroupId groupId = this.groupId;
+	private void loadHeaders() {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					// Wait for the service to be bound and started
 					serviceConnection.waitForStartup();
-					// Load the message headers from the database
+					// Load the headers from the database
 					Collection<GroupMessageHeader> headers =
 							db.getMessageHeaders(groupId);
 					if(LOG.isLoggable(INFO))
 						LOG.info("Loaded " + headers.size() + " headers");
-					// Load the ratings for the authors
-					Map<Author, Rating> ratings = new HashMap<Author, Rating>();
-					for(GroupMessageHeader h : headers) {
-						Author a = h.getAuthor();
-						if(a != null && !ratings.containsKey(a))
-							ratings.put(a, db.getRating(a.getId()));
-					}
-					ratings = Collections.unmodifiableMap(ratings);
-					// Update the conversation
-					updateConversation(headers, ratings);
+					// Display the headers in the UI
+					displayHeaders(headers);
 				} catch(NoSuchSubscriptionException e) {
 					if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
 					finishOnUiThread();
@@ -152,29 +142,46 @@ OnClickListener, OnItemClickListener {
 		});
 	}
 
-	private void updateConversation(
-			final Collection<GroupMessageHeader> headers,
-			final Map<Author, Rating> ratings) {
+	private void displayHeaders(final Collection<GroupMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				List<GroupMessageHeader> sort =
-						new ArrayList<GroupMessageHeader>(headers);
-				Collections.sort(sort, AscendingHeaderComparator.INSTANCE);
-				int firstUnread = -1;
+				messageIds.clear();
 				adapter.clear();
-				for(GroupMessageHeader h : sort) {
-					if(firstUnread == -1 && !h.isRead())
-						firstUnread = adapter.getCount();
-					Author a = h.getAuthor();
-					if(a == null) adapter.add(new GroupItem(h, UNRATED));
-					else adapter.add(new GroupItem(h, ratings.get(a)));
+				for(GroupMessageHeader h : headers) {
+					messageIds.add(h.getId());
+					adapter.add(h);
 				}
-				if(firstUnread == -1) list.setSelection(adapter.getCount() - 1);
-				else list.setSelection(firstUnread);
+				adapter.sort(AscendingHeaderComparator.INSTANCE);
+				selectFirstUnread();
 			}
 		});
 	}
 
+	private void selectFirstUnread() {
+		int firstUnread = -1, count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			if(!adapter.getItem(i).isRead()) {
+				firstUnread = i;
+				break;
+			}
+		}
+		if(firstUnread == -1) list.setSelection(count - 1);
+		else list.setSelection(firstUnread);
+	}
+
+	@Override
+	public void onActivityResult(int request, int result, Intent data) {
+		if(result == ReadGroupMessageActivity.RESULT_PREV) {
+			int position = request - 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		} else if(result == ReadGroupMessageActivity.RESULT_NEXT) {
+			int position = request + 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		}
+	}
+
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
@@ -183,38 +190,59 @@ OnClickListener, OnItemClickListener {
 	}
 
 	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof MessageAddedEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
-			reloadMessageHeaders();
+		if(e instanceof GroupMessageAddedEvent) {
+			GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
+			Message m = g.getMessage();
+			if(m.getGroup().getId().equals(groupId))
+				loadRatingOrAddToGroup(m, g.isIncoming());
 		} else if(e instanceof MessageExpiredEvent) {
 			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			reloadMessageHeaders();
-		} else if(e instanceof RatingChangedEvent) {
-			RatingChangedEvent r = (RatingChangedEvent) e;
-			updateRating(r.getAuthorId(), r.getRating());
+			loadHeaders(); // FIXME: Don't reload unnecessarily
 		} else if(e instanceof SubscriptionRemovedEvent) {
-			SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e;
-			if(s.getGroupId().equals(groupId)) {
+			if(((SubscriptionRemovedEvent) e).getGroupId().equals(groupId)) {
 				if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
 				finishOnUiThread();
 			}
 		}
 	}
 
-	private void updateRating(final AuthorId a, final Rating r) {
+	private void loadRatingOrAddToGroup(Message m, boolean incoming) {
+		// FIXME: Cache ratings to avoid hitting the DB
+		if(m.getAuthor() == null) addToGroup(m, UNRATED, incoming);
+		else loadRating(m, incoming);
+	}
+
+	private void addToGroup(final Message m, final Rating r,
+			final boolean incoming) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				boolean affected = false;
-				int count = adapter.getCount();
-				for(int i = 0; i < count; i++) {
-					GroupItem item = adapter.getItem(i);
-					Author author = item.getAuthor();
-					if(author != null && author.getId().equals(a)) {
-						item.setRating(r);
-						affected = true;
-					}
+				if(messageIds.add(m.getId())) {
+					adapter.add(new GroupMessageHeader(m, !incoming, false, r));
+					adapter.sort(AscendingHeaderComparator.INSTANCE);
+					selectFirstUnread();
+				}
+			}
+		});
+	}
+
+	private void loadRating(final Message m, final boolean incoming) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// Load the rating from the database
+					Rating r = db.getRating(m.getAuthor().getId());
+					// Display the message
+					addToGroup(m, r, incoming);
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
 				}
-				if(affected) list.invalidate();
 			}
 		});
 	}
@@ -222,7 +250,6 @@ OnClickListener, OnItemClickListener {
 	public void onClick(View view) {
 		Intent i = new Intent(this, WriteGroupMessageActivity.class);
 		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
-		i.putExtra("net.sf.briar.GROUP_NAME", groupName);
 		startActivity(i);
 	}
 
@@ -232,7 +259,7 @@ OnClickListener, OnItemClickListener {
 	}
 
 	private void showMessage(int position) {
-		GroupItem item = adapter.getItem(position);
+		GroupMessageHeader item = adapter.getItem(position);
 		Intent i = new Intent(this, ReadGroupMessageActivity.class);
 		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
 		i.putExtra("net.sf.briar.GROUP_NAME", groupName);
@@ -252,17 +279,4 @@ OnClickListener, OnItemClickListener {
 		i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1);
 		startActivityForResult(i, position);
 	}
-
-	@Override
-	public void onActivityResult(int request, int result, Intent data) {
-		if(result == ReadGroupMessageActivity.RESULT_PREV) {
-			int position = request - 1;
-			if(position >= 0 && position < adapter.getCount())
-				showMessage(position);
-		} else if(result == ReadGroupMessageActivity.RESULT_NEXT) {
-			int position = request + 1;
-			if(position >= 0 && position < adapter.getCount())
-				showMessage(position);
-		}
-	}
 }
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java
index 463785884c9d264b4f02d25800bec2b3e7b6b1a9..c908c0332a184683e66838663319ebdd63b1ce77 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java
@@ -15,6 +15,7 @@ import net.sf.briar.R;
 import net.sf.briar.android.widgets.CommonLayoutParams;
 import net.sf.briar.android.widgets.HorizontalSpace;
 import net.sf.briar.api.Rating;
+import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.messaging.Author;
 import android.content.Context;
 import android.content.res.Resources;
@@ -26,21 +27,20 @@ import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
-class GroupAdapter extends ArrayAdapter<GroupItem> {
+class GroupAdapter extends ArrayAdapter<GroupMessageHeader> {
 
 	GroupAdapter(Context ctx) {
 		super(ctx, android.R.layout.simple_expandable_list_item_1,
-				new ArrayList<GroupItem>());
+				new ArrayList<GroupMessageHeader>());
 	}
 
 	@Override
 	public View getView(int position, View convertView, ViewGroup parent) {
-		GroupItem item = getItem(position);
+		GroupMessageHeader item = getItem(position);
 		Context ctx = getContext();
 		// FIXME: Use a RelativeLayout
 		LinearLayout layout = new LinearLayout(ctx);
 		layout.setOrientation(HORIZONTAL);
-		// layout.setGravity(CENTER_VERTICAL);
 		if(!item.isRead()) {
 			Resources res = ctx.getResources();
 			layout.setBackgroundColor(res.getColor(R.color.unread_background));
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupItem.java b/briar-android/src/net/sf/briar/android/groups/GroupItem.java
deleted file mode 100644
index 3e6cb182136fd264721795132e4b5178b242a142..0000000000000000000000000000000000000000
--- a/briar-android/src/net/sf/briar/android/groups/GroupItem.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package net.sf.briar.android.groups;
-
-import net.sf.briar.api.Rating;
-import net.sf.briar.api.db.GroupMessageHeader;
-import net.sf.briar.api.messaging.Author;
-import net.sf.briar.api.messaging.MessageId;
-
-// This class is not thread-safe
-class GroupItem {
-
-	private final GroupMessageHeader header;
-	private Rating rating;
-
-	GroupItem(GroupMessageHeader header, Rating rating) {
-		this.header = header;
-		this.rating = rating;
-	}
-
-	MessageId getId() {
-		return header.getId();
-	}
-
-	Author getAuthor() {
-		return header.getAuthor();
-	}
-
-	String getContentType() {
-		return header.getContentType();
-	}
-
-	String getSubject() {
-		return header.getSubject();
-	}
-
-	long getTimestamp() {
-		return header.getTimestamp();
-	}
-
-	boolean isRead() {
-		return header.isRead();
-	}
-
-	Rating getRating() {
-		return rating;
-	}
-
-	void setRating(Rating rating) {
-		this.rating = rating;
-	}
-}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
index ee38bfd8d8b997cb04d839d6007388daec71178c..48a977e842bd755c47632c92271cba2e94a7b50e 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
@@ -36,14 +36,14 @@ import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.db.NoSuchSubscriptionException;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
-import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.GroupMessageAddedEvent;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
-import net.sf.briar.api.db.event.SubscriptionAddedEvent;
 import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
 import net.sf.briar.api.messaging.Author;
 import net.sf.briar.api.messaging.AuthorFactory;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupFactory;
+import net.sf.briar.api.messaging.GroupId;
 import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageFactory;
 import android.content.Intent;
@@ -65,14 +65,16 @@ implements OnClickListener, DatabaseListener {
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
 
-	@Inject private CryptoComponent crypto;
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
-	@Inject private AuthorFactory authorFactory;
-	@Inject private GroupFactory groupFactory;
-	@Inject private MessageFactory messageFactory;
-
 	private GroupListAdapter adapter = null;
+	private ListView list = null;
+
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile CryptoComponent crypto;
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+	@Inject private volatile AuthorFactory authorFactory;
+	@Inject private volatile GroupFactory groupFactory;
+	@Inject private volatile MessageFactory messageFactory;
 
 	@Override
 	public void onCreate(Bundle state) {
@@ -83,7 +85,7 @@ implements OnClickListener, DatabaseListener {
 		layout.setGravity(CENTER_HORIZONTAL);
 
 		adapter = new GroupListAdapter(this);
-		ListView list = new ListView(this);
+		list = new ListView(this);
 		// Give me all the width and all the unused height
 		list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
 		list.setAdapter(adapter);
@@ -112,9 +114,6 @@ implements OnClickListener, DatabaseListener {
 
 	// FIXME: Remove this
 	private void insertFakeMessages() {
-		final DatabaseComponent db = this.db;
-		final GroupFactory groupFactory = this.groupFactory;
-		final MessageFactory messageFactory = this.messageFactory;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
@@ -204,18 +203,16 @@ implements OnClickListener, DatabaseListener {
 	@Override
 	public void onResume() {
 		super.onResume();
-		reloadGroupList();
+		loadGroups();
 	}
 
-	private void reloadGroupList() {
-		final DatabaseComponent db = this.db;
+	private void loadGroups() {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					// Wait for the service to be bound and started
 					serviceConnection.waitForStartup();
-					// Load the groups and message headers from the DB
-					if(LOG.isLoggable(INFO)) LOG.info("Loading groups");
+					// Load the subscribed groups from the DB
 					Collection<Group> groups = db.getSubscriptions();
 					if(LOG.isLoggable(INFO))
 						LOG.info("Loaded " + groups.size() + " groups");
@@ -223,20 +220,20 @@ implements OnClickListener, DatabaseListener {
 					for(Group g : groups) {
 						// Filter out restricted groups
 						if(g.getPublicKey() != null) continue;
+						// Load the message headers
 						Collection<GroupMessageHeader> headers;
 						try {
 							headers = db.getMessageHeaders(g.getId());
 						} catch(NoSuchSubscriptionException e) {
-							// We'll reload the list when we get the event
-							continue;
+							continue; // Unsubscribed since getSubscriptions()
 						}
 						if(LOG.isLoggable(INFO))
 							LOG.info("Loaded " + headers.size() + " headers");
 						if(!headers.isEmpty())
 							items.add(createItem(g, headers));
 					}
-					// Update the group list
-					updateGroupList(Collections.unmodifiableList(items));
+					// Display the groups in the UI
+					displayGroups(Collections.unmodifiableList(items));
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -257,16 +254,29 @@ implements OnClickListener, DatabaseListener {
 		return new GroupListItem(group, sort);
 	}
 
-	private void updateGroupList(final Collection<GroupListItem> items) {
+	private void displayGroups(final Collection<GroupListItem> items) {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				adapter.clear();
 				for(GroupListItem i : items) adapter.add(i);
 				adapter.sort(GroupComparator.INSTANCE);
+				selectFirstUnread();
 			}
 		});
 	}
 
+	private void selectFirstUnread() {
+		int firstUnread = -1, count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			if(adapter.getItem(i).getUnreadCount() > 0) {
+				firstUnread = i;
+				break;
+			}
+		}
+		if(firstUnread == -1) list.setSelection(count - 1);
+		else list.setSelection(firstUnread);
+	}
+
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
@@ -279,19 +289,94 @@ implements OnClickListener, DatabaseListener {
 	}
 
 	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof MessageAddedEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
-			reloadGroupList();
+		if(e instanceof GroupMessageAddedEvent) {
+			GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
+			addToGroup(g.getMessage(), g.isIncoming());
 		} else if(e instanceof MessageExpiredEvent) {
 			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			reloadGroupList();
-		} else if(e instanceof SubscriptionAddedEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
-			reloadGroupList();
+			loadGroups(); // FIXME: Don't reload unnecessarily
 		} else if(e instanceof SubscriptionRemovedEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
-			reloadGroupList();
+			removeGroup(((SubscriptionRemovedEvent) e).getGroupId());
+		}
+	}
+
+	private void addToGroup(final Message m, final boolean incoming) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				GroupId g = m.getGroup().getId();
+				GroupListItem item = findGroup(g);
+				if(item == null) {
+					loadGroup(g, m, incoming);
+				} else if(item.add(m, incoming)) {
+					adapter.sort(GroupComparator.INSTANCE);
+					selectFirstUnread();
+					list.invalidate();
+				}
+			}
+		});
+	}
+
+	private GroupListItem findGroup(GroupId g) {
+		int count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			GroupListItem item = adapter.getItem(i);
+			if(item.getGroupId().equals(g)) return item;
 		}
+		return null; // Not found
+	}
+
+	private void loadGroup(final GroupId g, final Message m,
+			final boolean incoming) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// Load the group from the DB and display it in the UI
+					displayGroup(db.getGroup(g), m, incoming);
+				} catch(NoSuchSubscriptionException e) {
+					if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	private void displayGroup(final Group g, final Message m,
+			final boolean incoming) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				// The item may have been added since loadGroup() was called
+				GroupListItem item = findGroup(g.getId());
+				if(item == null) {
+					adapter.add(new GroupListItem(g, m, incoming));
+					adapter.sort(GroupComparator.INSTANCE);
+					selectFirstUnread();
+				} else if(item.add(m, incoming)) {
+					adapter.sort(GroupComparator.INSTANCE);
+					selectFirstUnread();
+					list.invalidate();
+				}
+			}
+		});
+	}
+
+	private void removeGroup(final GroupId g) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				GroupListItem item = findGroup(g);
+				if(item != null) {
+					adapter.remove(item);
+					selectFirstUnread();
+				}
+			}
+		});
 	}
 
 	private static class GroupComparator implements Comparator<GroupListItem> {
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
index 38a4513c21cb24679762f1373a093e72e00feab5..918e272d4851e8f7c8a845c974944c0a30734513 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
@@ -1,20 +1,26 @@
 package net.sf.briar.android.groups;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import net.sf.briar.android.DescendingHeaderComparator;
 import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.messaging.Author;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.Message;
+import net.sf.briar.api.messaging.MessageId;
 
+// This class is not thread-safe
 class GroupListItem {
 
+	private final Set<MessageId> messageIds = new HashSet<MessageId>();
 	private final Group group;
-	private final String author, subject;
-	private final long timestamp;
-	private final int unread;
+	private String authorName, subject;
+	private long timestamp;
+	private int unread;
 
 	GroupListItem(Group group, List<GroupMessageHeader> headers) {
 		if(headers.isEmpty()) throw new IllegalArgumentException();
@@ -22,13 +28,40 @@ class GroupListItem {
 		Collections.sort(headers, DescendingHeaderComparator.INSTANCE);
 		GroupMessageHeader newest = headers.get(0);
 		Author a = newest.getAuthor();
-		if(a == null) author = null;
-		else author = a.getName();
+		if(a == null) authorName = null;
+		else authorName = a.getName();
 		subject = newest.getSubject();
 		timestamp = newest.getTimestamp();
-		int unread = 0;
-		for(GroupMessageHeader h : headers) if(!h.isRead()) unread++;
-		this.unread = unread;
+		unread = 0;
+		for(GroupMessageHeader h : headers) {
+			if(!h.isRead()) unread++;
+			if(!messageIds.add(h.getId())) throw new IllegalArgumentException();
+		}
+	}
+
+	GroupListItem(Group group, Message first, boolean incoming) {
+		this.group = group;
+		Author a = first.getAuthor();
+		if(a == null) authorName = null;
+		else authorName = a.getName();
+		subject = first.getSubject();
+		timestamp = first.getTimestamp();
+		unread = incoming ? 1 : 0;
+		messageIds.add(first.getId());
+	}
+
+	boolean add(Message m, boolean incoming) {
+		if(!messageIds.add(m.getId())) return false;
+		if(m.getTimestamp() > timestamp) {
+			// The added message is the newest
+			Author a = m.getAuthor();
+			if(a == null) authorName = null;
+			else authorName = a.getName();
+			subject = m.getSubject();
+			timestamp = m.getTimestamp();
+		}
+		if(incoming) unread++;
+		return true;
 	}
 
 	GroupId getGroupId() {
@@ -40,7 +73,7 @@ class GroupListItem {
 	}
 
 	String getAuthorName() {
-		return author;
+		return authorName;
 	}
 
 	String getSubject() {
diff --git a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
index 9a43b56d4deaf9f72a0d56fc34509496acc25d9f..845c7b9b1e7e3fb80922d8740331f00d93f527e5 100644
--- a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
@@ -61,13 +61,7 @@ implements OnClickListener {
 			new BriarServiceConnection();
 
 	@Inject private BundleEncrypter bundleEncrypter;
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
-
 	private GroupId groupId = null;
-	private MessageId messageId = null;
-	private AuthorId authorId = null;
-	private String authorName = null;
 	private Rating rating = UNRATED;
 	private boolean read;
 	private ImageView thumb = null;
@@ -76,6 +70,12 @@ implements OnClickListener {
 	private ImageButton replyButton = null;
 	private TextView content = null;
 
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+	private volatile MessageId messageId = null;
+	private volatile AuthorId authorId = null;
+
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(null);
@@ -91,6 +91,7 @@ implements OnClickListener {
 		if(id == null) throw new IllegalStateException();
 		messageId = new MessageId(id);
 		boolean anonymous = i.getBooleanExtra("net.sf.briar.ANONYMOUS", false);
+		String authorName = null;
 		if(!anonymous) {
 			id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
 			if(id == null) throw new IllegalStateException();
@@ -235,8 +236,6 @@ implements OnClickListener {
 	}
 
 	private void setReadInDatabase(final boolean read) {
-		final DatabaseComponent db = this.db;
-		final MessageId messageId = this.messageId;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
@@ -266,8 +265,6 @@ implements OnClickListener {
 	}
 
 	private void loadMessageBody() {
-		final DatabaseComponent db = this.db;
-		final MessageId messageId = this.messageId;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
@@ -334,8 +331,6 @@ implements OnClickListener {
 	}
 
 	private void setRatingInDatabase(final Rating r) {
-		final DatabaseComponent db = this.db;
-		final AuthorId authorId = this.authorId;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java
index 7a1a93c06f3ac3cb5ad32e72d014c289a2877988..6dd8792ea63efbaa88f8f77c5eb5b27271f45d05 100644
--- a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java
@@ -53,18 +53,19 @@ implements OnClickListener, OnItemSelectedListener {
 			new BriarServiceConnection();
 
 	@Inject private BundleEncrypter bundleEncrypter;
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
-	@Inject private MessageFactory messageFactory;
-
-	private Group group = null;
-	private GroupId groupId = null;
-	private MessageId parentId = null;
 	private GroupNameSpinnerAdapter adapter = null;
 	private Spinner spinner = null;
 	private ImageButton sendButton = null;
 	private EditText content = null;
 
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+	@Inject private volatile MessageFactory messageFactory;
+	private volatile Group group = null;
+	private volatile GroupId groupId = null;
+	private volatile MessageId parentId = null;
+
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(null);
@@ -94,7 +95,7 @@ implements OnClickListener, OnItemSelectedListener {
 		spinner = new Spinner(this);
 		spinner.setAdapter(adapter);
 		spinner.setOnItemSelectedListener(this);
-		loadContactNames();
+		loadGroupList();
 		actionBar.addView(spinner);
 
 		actionBar.addView(new HorizontalSpace(this));
@@ -122,24 +123,12 @@ implements OnClickListener, OnItemSelectedListener {
 				serviceConnection, 0);
 	}
 
-	private void loadContactNames() {
-		final DatabaseComponent db = this.db;
+	private void loadGroupList() {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					serviceConnection.waitForStartup();
-					final Collection<Group> groups = db.getSubscriptions();
-					runOnUiThread(new Runnable() {
-						public void run() {
-							for(Group g : groups) {
-								if(g.getId().equals(groupId)) {
-									group = g;
-									spinner.setSelection(adapter.getCount());
-								}
-								adapter.add(g);
-							}
-						}
-					});
+					updateGroupList(db.getSubscriptions());
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -151,6 +140,20 @@ implements OnClickListener, OnItemSelectedListener {
 		});
 	}
 
+	private void updateGroupList(final Collection<Group> groups) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				for(Group g : groups) {
+					if(g.getId().equals(groupId)) {
+						group = g;
+						spinner.setSelection(adapter.getCount());
+					}
+					adapter.add(g);
+				}
+			}
+		});
+	}
+
 	@Override
 	public void onSaveInstanceState(Bundle state) {
 		Parcelable p = content.onSaveInstanceState();
@@ -175,10 +178,6 @@ implements OnClickListener, OnItemSelectedListener {
 	}
 
 	private void storeMessage(final byte[] body) {
-		final DatabaseComponent db = this.db;
-		final MessageFactory messageFactory = this.messageFactory;
-		final Group group = this.group;
-		final MessageId parentId = this.parentId;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
diff --git a/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java b/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java
index 4c246c491396bf6f4094c9306c80278db9f5aeb4..6bc03686f8711cc1432e094ac1da96c21c87a8dd 100644
--- a/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java
+++ b/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java
@@ -35,12 +35,8 @@ implements InvitationListener {
 
 	@Inject private BundleEncrypter bundleEncrypter;
 	@Inject private CryptoComponent crypto;
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
 	@Inject private InvitationTaskFactory invitationTaskFactory;
 	@Inject private ReferenceManager referenceManager;
-
-	// All of the following must be accessed on the UI thread
 	private AddContactView view = null;
 	private InvitationTask task = null;
 	private long taskHandle = -1;
@@ -52,6 +48,10 @@ implements InvitationListener {
 	private boolean localCompared = false, remoteCompared = false;
 	private boolean localMatched = false, remoteMatched = false;
 
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(null);
@@ -217,7 +217,6 @@ implements InvitationListener {
 	}
 
 	void addContactAndFinish(final String nickname) {
-		final DatabaseComponent db = this.db;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
index aefd64d0da5fd689eba1009d2c03fb752b05a1a3..ef7252eeda94f8d810a8ee1dc48f887713408fa8 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
@@ -5,10 +5,9 @@ import static android.widget.LinearLayout.VERTICAL;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -28,8 +27,10 @@ import net.sf.briar.api.db.PrivateMessageHeader;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
-import net.sf.briar.api.db.event.MessageAddedEvent;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
+import net.sf.briar.api.messaging.Message;
+import net.sf.briar.api.messaging.MessageId;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
@@ -51,14 +52,17 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
 
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
-
-	private ContactId contactId = null;
+	// The following fields must only be accessed from the UI thread
+	private Set<MessageId> messageIds = new HashSet<MessageId>();
 	private String contactName = null;
 	private ConversationAdapter adapter = null;
 	private ListView list = null;
 
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+	private volatile ContactId contactId = null;
+
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(null);
@@ -104,24 +108,22 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 	@Override
 	public void onResume() {
 		super.onResume();
-		reloadMessageHeaders();
+		loadHeaders();
 	}
 
-	private void reloadMessageHeaders() {
-		final DatabaseComponent db = this.db;
-		final ContactId contactId = this.contactId;
+	private void loadHeaders() {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					// Wait for the service to be bound and started
 					serviceConnection.waitForStartup();
-					// Load the message headers from the database
+					// Load the headers from the database
 					Collection<PrivateMessageHeader> headers =
 							db.getPrivateMessageHeaders(contactId);
 					if(LOG.isLoggable(INFO))
 						LOG.info("Loaded " + headers.size() + " headers");
-					// Update the conversation
-					updateConversation(headers);
+					// Display the headers in the UI
+					displayHeaders(headers);
 				} catch(NoSuchContactException e) {
 					if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
 					finishOnUiThread();
@@ -137,26 +139,34 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		});
 	}
 
-	private void updateConversation(
+	private void displayHeaders(
 			final Collection<PrivateMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				List<PrivateMessageHeader> sort =
-						new ArrayList<PrivateMessageHeader>(headers);
-				Collections.sort(sort, AscendingHeaderComparator.INSTANCE);
-				int firstUnread = -1;
+				messageIds.clear();
 				adapter.clear();
-				for(PrivateMessageHeader h : sort) {
-					if(firstUnread == -1 && !h.isRead())
-						firstUnread = adapter.getCount();
+				for(PrivateMessageHeader h : headers) {
+					messageIds.add(h.getId());
 					adapter.add(h);
 				}
-				if(firstUnread == -1) list.setSelection(adapter.getCount() - 1);
-				else list.setSelection(firstUnread);
+				adapter.sort(AscendingHeaderComparator.INSTANCE);
+				selectFirstUnread();
 			}
 		});
 	}
 
+	private void selectFirstUnread() {
+		int firstUnread = -1, count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			if(!adapter.getItem(i).isRead()) {
+				firstUnread = i;
+				break;
+			}
+		}
+		if(firstUnread == -1) list.setSelection(count - 1);
+		else list.setSelection(firstUnread);
+	}
+
 	@Override
 	public void onActivityResult(int request, int result, Intent data) {
 		if(result == ReadPrivateMessageActivity.RESULT_PREV) {
@@ -182,17 +192,31 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 			ContactRemovedEvent c = (ContactRemovedEvent) e;
 			if(c.getContactId().equals(contactId)) {
 				if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
-				finish();
+				finishOnUiThread();
 			}
-		} else if(e instanceof MessageAddedEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
-			reloadMessageHeaders();
 		} else if(e instanceof MessageExpiredEvent) {
 			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			reloadMessageHeaders();
+			loadHeaders(); // FIXME: Don't reload unnecessarily
+		} else if(e instanceof PrivateMessageAddedEvent) {
+			PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e;
+			if(p.getContactId().equals(contactId))
+				addToConversation(p.getMessage(), p.isIncoming());
 		}
 	}
 
+	private void addToConversation(final Message m, final boolean incoming) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				if(messageIds.add(m.getId())) {
+					adapter.add(new PrivateMessageHeader(m, !incoming, false,
+							contactId, incoming));
+					adapter.sort(AscendingHeaderComparator.INSTANCE);
+					selectFirstUnread();
+				}
+			}
+		});
+	}
+
 	public void onClick(View view) {
 		Intent i = new Intent(this, WritePrivateMessageActivity.class);
 		i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationComparator.java b/briar-android/src/net/sf/briar/android/messages/ConversationComparator.java
deleted file mode 100644
index fcf0b179991c325eb1ed87942245f9d9a0d57824..0000000000000000000000000000000000000000
--- a/briar-android/src/net/sf/briar/android/messages/ConversationComparator.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package net.sf.briar.android.messages;
-
-import java.util.Comparator;
-
-class ConversationComparator implements Comparator<ConversationListItem> {
-
-	static final ConversationComparator INSTANCE = new ConversationComparator();
-
-	public int compare(ConversationListItem a, ConversationListItem b) {
-		// The item with the newest message comes first
-		long aTime = a.getTimestamp(), bTime = b.getTimestamp();
-		if(aTime > bTime) return -1;
-		if(aTime < bTime) return 1;
-		return 0;
-	}
-}
\ No newline at end of file
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
index 5eafcf3367303550aabe2763b095eb8e5155d700..4734728148051e43fb0193c8ff5e4cb3d3308159 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
@@ -9,6 +9,7 @@ import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -26,11 +27,13 @@ import net.sf.briar.api.ContactId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.NoSuchContactException;
 import net.sf.briar.api.db.PrivateMessageHeader;
+import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
-import net.sf.briar.api.db.event.MessageAddedEvent;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
 import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageFactory;
 import android.content.Intent;
@@ -52,11 +55,13 @@ implements OnClickListener, DatabaseListener {
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
 
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
-	@Inject private MessageFactory messageFactory;
-
 	private ConversationListAdapter adapter = null;
+	private ListView list = null;
+
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+	@Inject private volatile MessageFactory messageFactory;
 
 	@Override
 	public void onCreate(Bundle state) {
@@ -67,7 +72,7 @@ implements OnClickListener, DatabaseListener {
 		layout.setGravity(CENTER_HORIZONTAL);
 
 		adapter = new ConversationListAdapter(this);
-		ListView list = new ListView(this);
+		list = new ListView(this);
 		// Give me all the width and all the unused height
 		list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
 		list.setAdapter(adapter);
@@ -96,8 +101,6 @@ implements OnClickListener, DatabaseListener {
 
 	// FIXME: Remove this
 	private void insertFakeMessages() {
-		final DatabaseComponent db = this.db;
-		final MessageFactory messageFactory = this.messageFactory;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
@@ -160,29 +163,26 @@ implements OnClickListener, DatabaseListener {
 	@Override
 	public void onResume() {
 		super.onResume();
-		reloadMessageHeaders();
+		loadHeaders();
 	}
 
-	private void reloadMessageHeaders() {
-		final DatabaseComponent db = this.db;
+	private void loadHeaders() {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					// Wait for the service to be bound and started
 					serviceConnection.waitForStartup();
 					// Load the contact list from the database
-					if(LOG.isLoggable(INFO)) LOG.info("Loading contacts");
 					Collection<Contact> contacts = db.getContacts();
 					if(LOG.isLoggable(INFO))
 						LOG.info("Loaded " + contacts.size() + " contacts");
-					// Load the message headers from the database
-					if(LOG.isLoggable(INFO)) LOG.info("Loading headers");
+					// Load the headers from the database
 					Collection<PrivateMessageHeader> headers =
 							db.getPrivateMessageHeaders();
 					if(LOG.isLoggable(INFO))
 						LOG.info("Loaded " + headers.size() + " headers");
-					// Update the conversation list
-					updateConversationList(contacts, headers);
+					// Display the headers in the UI
+					displayHeaders(contacts, headers);
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -195,7 +195,7 @@ implements OnClickListener, DatabaseListener {
 		});
 	}
 
-	private void updateConversationList(final Collection<Contact> contacts,
+	private void displayHeaders(final Collection<Contact> contacts,
 			final Collection<PrivateMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
@@ -203,6 +203,7 @@ implements OnClickListener, DatabaseListener {
 				for(ConversationListItem i : sortHeaders(contacts, headers))
 					adapter.add(i);
 				adapter.sort(ConversationComparator.INSTANCE);
+				selectFirstUnread();
 			}
 		});
 	}
@@ -230,6 +231,18 @@ implements OnClickListener, DatabaseListener {
 		return list;
 	}
 
+	private void selectFirstUnread() {
+		int firstUnread = -1, count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			if(adapter.getItem(i).getUnreadCount() > 0) {
+				firstUnread = i;
+				break;
+			}
+		}
+		if(firstUnread == -1) list.setSelection(count - 1);
+		else list.setSelection(firstUnread);
+	}
+
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
@@ -242,12 +255,108 @@ implements OnClickListener, DatabaseListener {
 	}
 
 	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof MessageAddedEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
-			reloadMessageHeaders();
+		if(e instanceof ContactRemovedEvent) {
+			removeContact(((ContactRemovedEvent) e).getContactId());
 		} else if(e instanceof MessageExpiredEvent) {
 			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			reloadMessageHeaders();
+			loadHeaders(); // FIXME: Don't reload unnecessarily
+		} else if(e instanceof PrivateMessageAddedEvent) {
+			PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e;
+			addToConversation(p.getContactId(), p.getMessage(), p.isIncoming());
+		}
+	}
+
+	private void removeContact(final ContactId c) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				ConversationListItem item = findConversation(c);
+				if(item != null) {
+					adapter.remove(item);
+					selectFirstUnread();
+				}
+			}
+		});
+	}
+
+	private ConversationListItem findConversation(ContactId c) {
+		int count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			ConversationListItem item = adapter.getItem(i);
+			if(item.getContactId().equals(c)) return item;
+		}
+		return null; // Not found
+	}
+
+	private void addToConversation(final ContactId c, final Message m,
+			final boolean incoming) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				ConversationListItem item = findConversation(c);
+				if(item == null) {
+					loadContact(c, m, incoming);
+				} else if(item.add(m, incoming)) {
+					adapter.sort(ConversationComparator.INSTANCE);
+					selectFirstUnread();
+					list.invalidate();
+				}
+			}
+		});
+	}
+
+	private void loadContact(final ContactId c, final Message m,
+			final boolean incoming) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// Load the contact from the DB and display it in the UI
+					displayContact(db.getContact(c), m, incoming);
+				} catch(NoSuchContactException e) {
+					if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	private void displayContact(final Contact c, final Message m,
+			final boolean incoming) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				// The item may have been added since loadContact() was called
+				ConversationListItem item = findConversation(c.getId());
+				if(item == null) {
+					adapter.add(new ConversationListItem(c, m, incoming));
+					adapter.sort(ConversationComparator.INSTANCE);
+					selectFirstUnread();
+				} else if(item.add(m, incoming)) {
+					adapter.sort(ConversationComparator.INSTANCE);
+					selectFirstUnread();
+					list.invalidate();
+				}
+			}
+		});
+	}
+
+	private static class ConversationComparator
+	implements Comparator<ConversationListItem> {
+
+		static final ConversationComparator INSTANCE =
+				new ConversationComparator();
+
+		public int compare(ConversationListItem a, ConversationListItem b) {
+			// The item with the newest message comes first
+			long aTime = a.getTimestamp(), bTime = b.getTimestamp();
+			if(aTime > bTime) return -1;
+			if(aTime < bTime) return 1;
+			return 0;
 		}
 	}
 }
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
index eec30a91c80947d7cd4142db16e009f7c4c50991..edec2e49b360cfd2a0b1e4190f460a8cb0528f43 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
@@ -1,19 +1,25 @@
 package net.sf.briar.android.messages;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import net.sf.briar.android.DescendingHeaderComparator;
 import net.sf.briar.api.Contact;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.db.PrivateMessageHeader;
+import net.sf.briar.api.messaging.Message;
+import net.sf.briar.api.messaging.MessageId;
 
+// This class is not thread-safe
 class ConversationListItem {
 
+	private final Set<MessageId> messageIds = new HashSet<MessageId>();
 	private final Contact contact;
-	private final String subject;
-	private final long timestamp;
-	private final int unread;
+	private String subject;
+	private long timestamp;
+	private int unread;
 
 	ConversationListItem(Contact contact, List<PrivateMessageHeader> headers) {
 		if(headers.isEmpty()) throw new IllegalArgumentException();
@@ -21,9 +27,30 @@ class ConversationListItem {
 		Collections.sort(headers, DescendingHeaderComparator.INSTANCE);
 		subject = headers.get(0).getSubject();
 		timestamp = headers.get(0).getTimestamp();
-		int unread = 0;
-		for(PrivateMessageHeader h : headers) if(!h.isRead()) unread++;
-		this.unread = unread;
+		unread = 0;
+		for(PrivateMessageHeader h : headers) {
+			if(!h.isRead()) unread++;
+			if(!messageIds.add(h.getId())) throw new IllegalArgumentException();
+		}
+	}
+
+	ConversationListItem(Contact contact, Message first, boolean incoming) {
+		this.contact = contact;
+		subject = first.getSubject();
+		timestamp = first.getTimestamp();
+		unread = incoming ? 1 : 0;
+		messageIds.add(first.getId());
+	}
+
+	boolean add(Message m, boolean incoming) {
+		if(!messageIds.add(m.getId())) return false;
+		if(m.getTimestamp() > timestamp) {
+			// The added message is the newest
+			subject = m.getSubject();
+			timestamp = m.getTimestamp();
+		}
+		if(incoming) unread++;
+		return true;
 	}
 
 	ContactId getContactId() {
diff --git a/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
index 3183aae7cffb1cc18cdd506f5d65e030727ed9a1..30a17a264bafe5526a1c83a8eaa79bcf622deb4a 100644
--- a/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
@@ -53,16 +53,17 @@ implements OnClickListener {
 			new BriarServiceConnection();
 
 	@Inject private BundleEncrypter bundleEncrypter;
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
-
 	private ContactId contactId = null;
-	private MessageId messageId = null;
 	private boolean read;
 	private ImageButton readButton = null, prevButton = null, nextButton = null;
 	private ImageButton replyButton = null;
 	private TextView content = null;
 
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+	private volatile MessageId messageId = null;
+
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(null);
@@ -186,8 +187,6 @@ implements OnClickListener {
 	}
 
 	private void setReadInDatabase(final boolean read) {
-		final DatabaseComponent db = this.db;
-		final MessageId messageId = this.messageId;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
@@ -217,8 +216,6 @@ implements OnClickListener {
 	}
 
 	private void loadMessageBody() {
-		final DatabaseComponent db = this.db;
-		final MessageId messageId = this.messageId;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
diff --git a/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
index ced62326ac5f737bc66264c187b63a5f85d0ed69..589cdc31ef974b6dbb0673f5fff611d0b0c75445 100644
--- a/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
@@ -53,17 +53,18 @@ implements OnClickListener, OnItemSelectedListener {
 			new BriarServiceConnection();
 
 	@Inject private BundleEncrypter bundleEncrypter;
-	@Inject private DatabaseComponent db;
-	@Inject @DatabaseExecutor private Executor dbExecutor;
-	@Inject private MessageFactory messageFactory;
-
-	private ContactId contactId = null;
-	private MessageId parentId = null;
 	private ContactNameSpinnerAdapter adapter = null;
 	private Spinner spinner = null;
 	private ImageButton sendButton = null;
 	private EditText content = null;
 
+	// Fields that are accessed from DB threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseExecutor private volatile Executor dbExecutor;
+	@Inject private volatile MessageFactory messageFactory;
+	private volatile ContactId contactId = null;
+	private volatile MessageId parentId = null;
+
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(null);
@@ -93,7 +94,7 @@ implements OnClickListener, OnItemSelectedListener {
 		spinner = new Spinner(this);
 		spinner.setAdapter(adapter);
 		spinner.setOnItemSelectedListener(this);
-		loadContactNames();
+		loadContactList();
 		actionBar.addView(spinner);
 
 		actionBar.addView(new HorizontalSpace(this));
@@ -121,22 +122,12 @@ implements OnClickListener, OnItemSelectedListener {
 				serviceConnection, 0);
 	}
 
-	private void loadContactNames() {
-		final DatabaseComponent db = this.db;
+	private void loadContactList() {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					serviceConnection.waitForStartup();
-					final Collection<Contact> contacts = db.getContacts();
-					runOnUiThread(new Runnable() {
-						public void run() {
-							for(Contact c : contacts) {
-								if(c.getId().equals(contactId))
-									spinner.setSelection(adapter.getCount());
-								adapter.add(c);
-							}
-						}
-					});
+					updateContactList(db.getContacts());
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -148,6 +139,18 @@ implements OnClickListener, OnItemSelectedListener {
 		});
 	}
 
+	private void updateContactList(final Collection<Contact> contacts) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				for(Contact c : contacts) {
+					if(c.getId().equals(contactId))
+						spinner.setSelection(adapter.getCount());
+					adapter.add(c);
+				}
+			}
+		});
+	}
+
 	@Override
 	public void onSaveInstanceState(Bundle state) {
 		Parcelable p = content.onSaveInstanceState();
@@ -172,10 +175,6 @@ implements OnClickListener, OnItemSelectedListener {
 	}
 
 	private void storeMessage(final byte[] body) {
-		final DatabaseComponent db = this.db;
-		final MessageFactory messageFactory = this.messageFactory;
-		final ContactId contactId = this.contactId;
-		final MessageId parentId = this.parentId;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
index b930322fb4e59ec2b8424d7d9603c114cc4a8432..3e466d1918fd02dce4b0f4a45b8b7a6ef01b778f 100644
--- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
+++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
@@ -157,9 +157,15 @@ public interface DatabaseComponent {
 	/** Returns the configuration for the given transport. */
 	TransportConfig getConfig(TransportId t) throws DbException;
 
+	/** Returns the contact with the given ID. */
+	Contact getContact(ContactId c) throws DbException;
+
 	/** Returns all contacts. */
 	Collection<Contact> getContacts() throws DbException;
 
+	/** Returns the group with the given ID, if the user subscribes to it. */
+	Group getGroup(GroupId g) throws DbException;
+
 	/** Returns the local transport properties for the given transport. */
 	TransportProperties getLocalProperties(TransportId t) throws DbException;
 
diff --git a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java b/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java
index 543f34a939b1eaed183c11b3efc9c26ddf0fb52e..a0d796a7483b4c9c60a653bd8b53041cd2b9d9c6 100644
--- a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java
+++ b/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java
@@ -1,20 +1,31 @@
 package net.sf.briar.api.db;
 
+import net.sf.briar.api.Rating;
 import net.sf.briar.api.messaging.Author;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageId;
 
 public class GroupMessageHeader extends MessageHeader {
 
 	private final GroupId groupId;
 	private final Author author;
+	private final Rating rating;
 
 	public GroupMessageHeader(MessageId id, MessageId parent,
 			String contentType, String subject, long timestamp, boolean read,
-			boolean starred, GroupId groupId, Author author) {
+			boolean starred, GroupId groupId, Author author, Rating rating) {
 		super(id, parent, contentType, subject, timestamp, read, starred);
 		this.groupId = groupId;
 		this.author = author;
+		this.rating = rating;
+	}
+
+	public GroupMessageHeader(Message m, boolean read, boolean starred,
+			Rating rating) {
+		this(m.getId(), m.getParent(), m.getContentType(), m.getSubject(),
+				m.getTimestamp(), read, starred, m.getGroup().getId(),
+				m.getAuthor(), rating);
 	}
 
 	/** Returns the ID of the group to which the message belongs. */
@@ -28,4 +39,12 @@ public class GroupMessageHeader extends MessageHeader {
 	public Author getAuthor() {
 		return author;
 	}
+
+	/**
+	 * Returns the rating for the message's author, or Rating.UNRATED if this
+	 * is an anonymous message.
+	 */
+	public Rating getRating() {
+		return rating;
+	}
 }
diff --git a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java b/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java
index af55211c66fb691f233ce82816525d03fca2ff29..0add5ab62ae1f48162e2c88f8574a6feab978f58 100644
--- a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java
+++ b/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java
@@ -1,6 +1,7 @@
 package net.sf.briar.api.db;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageId;
 
 public class PrivateMessageHeader extends MessageHeader {
@@ -16,6 +17,12 @@ public class PrivateMessageHeader extends MessageHeader {
 		this.incoming = incoming;
 	}
 
+	public PrivateMessageHeader(Message m, boolean read, boolean starred,
+			ContactId contactId, boolean incoming) {
+		this(m.getId(), m.getParent(), m.getContentType(), m.getSubject(),
+				m.getTimestamp(), read, starred, contactId, incoming);
+	}
+
 	/**
 	 * Returns the ID of the contact who is the sender (if incoming) or
 	 * recipient (if outgoing) of this message.
diff --git a/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..acf9ca49bc4c39c3d16ad2cda5e832158a18d97e
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java
@@ -0,0 +1,23 @@
+package net.sf.briar.api.db.event;
+
+import net.sf.briar.api.messaging.Message;
+
+/** An event that is broadcast when a group message is added to the database. */
+public class GroupMessageAddedEvent extends DatabaseEvent {
+
+	private final Message message;
+	private final boolean incoming;
+
+	public GroupMessageAddedEvent(Message message, boolean incoming) {
+		this.message = message;
+		this.incoming = incoming;
+	}
+
+	public Message getMessage() {
+		return message;
+	}
+
+	public boolean isIncoming() {
+		return incoming;
+	}
+}
diff --git a/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java
deleted file mode 100644
index d28dd1664d455c6188cc1c520d919cf1798ef215..0000000000000000000000000000000000000000
--- a/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.sf.briar.api.db.event;
-
-/**
- * An event that is broadcast when one or more messages are added to the
- * database.
- */
-public class MessageAddedEvent extends DatabaseEvent {
-
-}
diff --git a/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java b/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java
index 4a5ae2c688144c237f5d260e80ad37e86dc3f2d5..2d338abf962ff59badd5b88b17bd0b801769a72c 100644
--- a/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java
+++ b/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java
@@ -1,6 +1,17 @@
 package net.sf.briar.api.db.event;
 
+import net.sf.briar.api.ContactId;
+
 /** An event that is broadcast when a message is received. */
 public class MessageReceivedEvent extends DatabaseEvent {
 
+	private final ContactId contactId;
+
+	public MessageReceivedEvent(ContactId contactId) {
+		this.contactId = contactId;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
 }
diff --git a/briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..45bcd16bd38d875fb5ce5e587f619d90139a213f
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java
@@ -0,0 +1,33 @@
+package net.sf.briar.api.db.event;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.messaging.Message;
+
+/**
+ * An event that is broadcast when a private message is added to the database.
+ */
+public class PrivateMessageAddedEvent extends DatabaseEvent {
+
+	private final Message message;
+	private final ContactId contactId;
+	private final boolean incoming;
+
+	public PrivateMessageAddedEvent(Message message, ContactId contactId,
+			boolean incoming) {
+		this.message = message;
+		this.contactId = contactId;
+		this.incoming = incoming;
+	}
+
+	public Message getMessage() {
+		return message;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+
+	public boolean isIncoming() {
+		return incoming;
+	}
+}
diff --git a/briar-api/src/net/sf/briar/api/db/event/SubscriptionAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/SubscriptionAddedEvent.java
index 76b39be75b457673ef884b9b8582ed3dc79364ea..1955d8f9dfc4f9889fc1e1de2301c5240f85efb8 100644
--- a/briar-api/src/net/sf/briar/api/db/event/SubscriptionAddedEvent.java
+++ b/briar-api/src/net/sf/briar/api/db/event/SubscriptionAddedEvent.java
@@ -1,17 +1,17 @@
 package net.sf.briar.api.db.event;
 
-import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.Group;
 
 /** An event that is broadcast when the user subscribes to a group. */
 public class SubscriptionAddedEvent extends DatabaseEvent {
 
-	private final GroupId groupId;
+	private final Group group;
 
-	public SubscriptionAddedEvent(GroupId groupId) {
-		this.groupId = groupId;
+	public SubscriptionAddedEvent(Group group) {
+		this.group = group;
 	}
 
-	public GroupId getGroupId() {
-		return groupId;
+	public Group getGroup() {
+		return group;
 	}
 }
diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java
index 97df26ac6e48387952bc523d8b909f842d67eb89..0dc1c9d360deb830ad7479b55a554cf9dc4eb7e3 100644
--- a/briar-core/src/net/sf/briar/db/Database.java
+++ b/briar-core/src/net/sf/briar/db/Database.java
@@ -202,6 +202,13 @@ interface Database<T> {
 	 */
 	TransportConfig getConfig(T txn, TransportId t) throws DbException;
 
+	/**
+	 * Returns the contact with the given ID.
+	 * <p>
+	 * Locking: contact read, window read.
+	 */
+	Contact getContact(T txn, ContactId c) throws DbException;
+
 	/**
 	 * Returns the IDs of all contacts.
 	 * <p>
@@ -230,6 +237,11 @@ interface Database<T> {
 	 */
 	long getFreeSpace() throws DbException;
 
+	/**
+	 * Returns the group with the given ID, if the user subscribes to it.
+	 */
+	Group getGroup(T txn, GroupId g) throws DbException;
+
 	/**
 	 * Returns the parent of the given group message, or null if either the
 	 * message has no parent, or the parent is absent from the database, or the
diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
index bd34f2e3f59f13064624802e2a0cc93d3d63d275..67287cd99a2036e072755b639be0d9b96a91d5a8 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -41,11 +41,12 @@ import net.sf.briar.api.db.event.ContactAddedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.GroupMessageAddedEvent;
 import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
 import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
-import net.sf.briar.api.db.event.MessageAddedEvent;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
 import net.sf.briar.api.db.event.MessageReceivedEvent;
+import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
 import net.sf.briar.api.db.event.RemoteRetentionTimeUpdatedEvent;
 import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent;
@@ -286,7 +287,7 @@ DatabaseCleaner.Callback {
 		} finally {
 			contactLock.readLock().unlock();
 		}
-		if(added) callListeners(new MessageAddedEvent());
+		if(added) callListeners(new GroupMessageAddedEvent(m, false));
 	}
 
 	/**
@@ -399,7 +400,7 @@ DatabaseCleaner.Callback {
 		} finally {
 			contactLock.readLock().unlock();
 		}
-		if(added) callListeners(new MessageAddedEvent());
+		if(added) callListeners(new PrivateMessageAddedEvent(m, c, false));
 	}
 
 	public void addSecrets(Collection<TemporarySecret> secrets)
@@ -844,6 +845,30 @@ DatabaseCleaner.Callback {
 		}
 	}
 
+	public Contact getContact(ContactId c) throws DbException {
+		contactLock.readLock().lock();
+		try {
+			windowLock.readLock().lock();
+			try {
+				T txn = db.startTransaction();
+				try {
+					if(!db.containsContact(txn, c))
+						throw new NoSuchContactException();
+					Contact contact = db.getContact(txn, c);
+					db.commitTransaction(txn);
+					return contact;
+				} catch(DbException e) {
+					db.abortTransaction(txn);
+					throw e;
+				}
+			} finally {
+				windowLock.readLock().unlock();
+			}
+		} finally {
+			contactLock.readLock().unlock();
+		}
+	}
+
 	public Collection<Contact> getContacts() throws DbException {
 		contactLock.readLock().lock();
 		try {
@@ -866,6 +891,25 @@ DatabaseCleaner.Callback {
 		}
 	}
 
+	public Group getGroup(GroupId g) throws DbException {
+		subscriptionLock.readLock().lock();
+		try {
+			T txn = db.startTransaction();
+			try {
+				if(!db.containsSubscription(txn, g))
+					throw new NoSuchSubscriptionException();
+				Group group = db.getGroup(txn, g);
+				db.commitTransaction(txn);
+				return group;
+			} catch(DbException e) {
+				db.abortTransaction(txn);
+				throw e;
+			}
+		} finally {
+			subscriptionLock.readLock().unlock();
+		}
+	}
+
 	public TransportProperties getLocalProperties(TransportId t)
 			throws DbException {
 		transportLock.readLock().lock();
@@ -1301,8 +1345,12 @@ DatabaseCleaner.Callback {
 		} finally {
 			contactLock.readLock().unlock();
 		}
-		callListeners(new MessageReceivedEvent());
-		if(added) callListeners(new MessageAddedEvent());
+		callListeners(new MessageReceivedEvent(c));
+		if(added) {
+			if(m.getGroup() == null)
+				callListeners(new PrivateMessageAddedEvent(m, c, true));
+			else callListeners(new GroupMessageAddedEvent(m, true));
+		}
 	}
 
 	/**
@@ -1795,7 +1843,7 @@ DatabaseCleaner.Callback {
 		} finally {
 			subscriptionLock.writeLock().unlock();
 		}
-		if(added) callListeners(new SubscriptionAddedEvent(g.getId()));
+		if(added) callListeners(new SubscriptionAddedEvent(g));
 		return added;
 	}
 
diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
index 0877effb7c0d510349fe5945e34283856abf08b6..e0d7150e58a5b3ac94b7296c983f7ffc81ab33e8 100644
--- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java
+++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
@@ -1047,6 +1047,31 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Contact getContact(Connection txn, ContactId c) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT name, lastConnected"
+					+ " FROM contacts AS c"
+					+ " JOIN connectionTimes AS ct"
+					+ " ON c.contactId = ct.contactId"
+					+ " WHERE c.contactId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			rs = ps.executeQuery();
+			if(!rs.next()) throw new DbStateException();
+			String name = rs.getString(1);
+			long lastConnected = rs.getLong(2);
+			rs.close();
+			ps.close();
+			return new Contact(c, name, lastConnected);
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Collection<ContactId> getContactIds(Connection txn)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1124,6 +1149,27 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Group getGroup(Connection txn, GroupId g) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT name, key FROM groups WHERE groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, g.getBytes());
+			rs = ps.executeQuery();
+			if(!rs.next()) throw new DbStateException();
+			String name = rs.getString(1);
+			byte[] publicKey = rs.getBytes(2);
+			rs.close();
+			ps.close();
+			return new Group(g, name, publicKey);
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public MessageId getGroupMessageParent(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1228,10 +1274,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT messageId, parentId, authorId, authorName,"
+			String sql = "SELECT messageId, parentId, m.authorId, authorName,"
 					+ " authorKey, contentType, subject, timestamp, read,"
-					+ " starred"
-					+ " FROM messages"
+					+ " starred, rating"
+					+ " FROM messages AS m"
+					+ " LEFT OUTER JOIN ratings AS r"
+					+ " ON m.authorId = r.authorId"
 					+ " WHERE groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
@@ -1242,13 +1290,18 @@ abstract class JdbcDatabase implements Database<Connection> {
 				MessageId id = new MessageId(rs.getBytes(1));
 				byte[] b = rs.getBytes(2);
 				MessageId parent = b == null ? null : new MessageId(b);
-				Author author = null;
+				Author author;
+				Rating rating;
 				b = rs.getBytes(3);
-				if(b != null) {
+				if(b == null) {
+					author = null;
+					rating = UNRATED;
+				} else {
 					AuthorId authorId = new AuthorId(b);
 					String authorName = rs.getString(4);
 					byte[] authorKey = rs.getBytes(5);
 					author = new Author(authorId, authorName, authorKey);
+					rating = Rating.values()[rs.getByte(11)];
 				}
 				String contentType = rs.getString(6);
 				String subject = rs.getString(7);
@@ -1256,7 +1309,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				boolean read = rs.getBoolean(9);
 				boolean starred = rs.getBoolean(10);
 				headers.add(new GroupMessageHeader(id, parent, contentType,
-						subject, timestamp, read, starred, g, author));
+						subject, timestamp, read, starred, g, author, rating));
 			}
 			rs.close();
 			ps.close();
diff --git a/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java b/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java
index 2681ec67b5ed200457b0641d3ea0f2521203066f..c0604546f76b93f697afadca3f5ed2817a0ddf0d 100644
--- a/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java
+++ b/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java
@@ -30,11 +30,12 @@ import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.GroupMessageAddedEvent;
 import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
 import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
-import net.sf.briar.api.db.event.MessageAddedEvent;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
 import net.sf.briar.api.db.event.MessageReceivedEvent;
+import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
 import net.sf.briar.api.db.event.RemoteRetentionTimeUpdatedEvent;
 import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent;
@@ -134,6 +135,9 @@ abstract class DuplexConnection implements DatabaseListener {
 		if(e instanceof ContactRemovedEvent) {
 			ContactRemovedEvent c = (ContactRemovedEvent) e;
 			if(contactId.equals(c.getContactId())) dispose(false, true);
+		} else if(e instanceof GroupMessageAddedEvent) {
+			if(canSendOffer.getAndSet(false))
+				dbExecutor.execute(new GenerateOffer());
 		} else if(e instanceof MessageExpiredEvent) {
 			dbExecutor.execute(new GenerateRetentionUpdate());
 		} else if(e instanceof LocalSubscriptionsUpdatedEvent) {
@@ -143,11 +147,15 @@ abstract class DuplexConnection implements DatabaseListener {
 				dbExecutor.execute(new GenerateSubscriptionUpdate());
 		} else if(e instanceof LocalTransportsUpdatedEvent) {
 			dbExecutor.execute(new GenerateTransportUpdates());
-		} else if(e instanceof MessageAddedEvent) {
-			if(canSendOffer.getAndSet(false))
-				dbExecutor.execute(new GenerateOffer());
 		} else if(e instanceof MessageReceivedEvent) {
-			dbExecutor.execute(new GenerateAcks());
+			if(((MessageReceivedEvent) e).getContactId().equals(contactId))
+				dbExecutor.execute(new GenerateAcks());
+		} else if(e instanceof PrivateMessageAddedEvent) {
+			PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e;
+			if(!p.isIncoming() && p.getContactId().equals(contactId)) {
+				if(canSendOffer.getAndSet(false))
+					dbExecutor.execute(new GenerateOffer());
+			}
 		} else if(e instanceof RatingChangedEvent) {
 			RatingChangedEvent r = (RatingChangedEvent) e;
 			if(r.getRating() == GOOD && canSendOffer.getAndSet(false))
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
index 63d8330ada7c3ab8f24aff24e9497add7fa6cfff..f1622c17c0a47868aca3abc686fde13521df47a1 100644
--- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
@@ -20,12 +20,14 @@ import net.sf.briar.api.TransportConfig;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.NoSuchContactException;
+import net.sf.briar.api.db.NoSuchSubscriptionException;
 import net.sf.briar.api.db.NoSuchTransportException;
 import net.sf.briar.api.db.event.ContactAddedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.GroupMessageAddedEvent;
 import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
-import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
 import net.sf.briar.api.db.event.SubscriptionAddedEvent;
 import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
@@ -503,11 +505,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		context.checking(new Expectations() {{
 			// Check whether the contact is in the DB (which it's not)
-			exactly(27).of(database).startTransaction();
+			exactly(28).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(27).of(database).containsContact(txn, contactId);
+			exactly(28).of(database).containsContact(txn, contactId);
 			will(returnValue(false));
-			exactly(27).of(database).abortTransaction(txn);
+			exactly(28).of(database).abortTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown);
@@ -572,6 +574,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			fail();
 		} catch(NoSuchContactException expected) {}
 
+		try {
+			db.getContact(contactId);
+			fail();
+		} catch(NoSuchContactException expected) {}
+
 		try {
 			db.getVisibleSubscriptions(contactId);
 			fail();
@@ -660,6 +667,53 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		context.assertIsSatisfied();
 	}
 
+	@Test
+	public void testVariousMethodsThrowExceptionIfSubscriptionIsMissing()
+			throws Exception {
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
+		context.checking(new Expectations() {{
+			// Check whether the subscription is in the DB (which it's not)
+			exactly(5).of(database).startTransaction();
+			will(returnValue(txn));
+			exactly(5).of(database).containsTransport(txn, transportId);
+			will(returnValue(false));
+			exactly(5).of(database).abortTransaction(txn);
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				shutdown);
+
+		try {
+			db.getGroup(groupId);
+			fail();
+		} catch(NoSuchSubscriptionException expected) {}
+
+		try {
+			db.getMessageHeaders(groupId);
+			fail();
+		} catch(NoSuchSubscriptionException expected) {}
+
+		try {
+			db.getVisibility(groupId);
+			fail();
+		} catch(NoSuchSubscriptionException expected) {}
+
+		try {
+			db.setVisibility(groupId, Collections.<ContactId>emptyList());
+			fail();
+		} catch(NoSuchSubscriptionException expected) {}
+
+		try {
+			db.unsubscribe(groupId);
+			fail();
+		} catch(NoSuchSubscriptionException expected) {}
+
+		context.assertIsSatisfied();
+	}
+
 	@Test
 	public void testVariousMethodsThrowExceptionIfTransportIsMissing()
 			throws Exception {
@@ -1454,7 +1508,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setSendability(txn, messageId, 0);
 			oneOf(database).commitTransaction(txn);
 			// The message was added, so the listener should be called
-			oneOf(listener).eventOccurred(with(any(MessageAddedEvent.class)));
+			oneOf(listener).eventOccurred(with(any(
+					GroupMessageAddedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown);
@@ -1485,7 +1540,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setReadFlag(txn, messageId, true);
 			oneOf(database).addStatus(txn, contactId, messageId, false);
 			// The message was added, so the listener should be called
-			oneOf(listener).eventOccurred(with(any(MessageAddedEvent.class)));
+			oneOf(listener).eventOccurred(with(any(
+					PrivateMessageAddedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown);
diff --git a/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java b/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java
index 105518b8f53537393fde5308e8a1f4115ea6e357..b87285eb61654c51464869bd015e99e6a83badfd 100644
--- a/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java
+++ b/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java
@@ -15,7 +15,7 @@ import net.sf.briar.api.crypto.KeyManager;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
-import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
 import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageFactory;
 import net.sf.briar.api.messaging.MessageVerifier;
@@ -186,14 +186,14 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 				messageVerifier, db, connRegistry, connWriterFactory,
 				packetWriterFactory, ctx, transport);
 		// No messages should have been added yet
-		assertFalse(listener.messagesAdded);
+		assertFalse(listener.messageAdded);
 		// Read whatever needs to be read
 		simplex.read();
 		assertTrue(transport.getDisposed());
 		assertFalse(transport.getException());
 		assertTrue(transport.getRecognised());
 		// The private message from Alice should have been added
-		assertTrue(listener.messagesAdded);
+		assertTrue(listener.messageAdded);
 		// Clean up
 		km.stop();
 		db.close();
@@ -206,10 +206,10 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 
 	private static class MessageListener implements DatabaseListener {
 
-		private boolean messagesAdded = false;
+		private boolean messageAdded = false;
 
 		public void eventOccurred(DatabaseEvent e) {
-			if(e instanceof MessageAddedEvent) messagesAdded = true;
+			if(e instanceof PrivateMessageAddedEvent) messageAdded = true;
 		}
 	}
 }