diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java
index 160b172e01e50c301b2bf5be207f0b7a247fb842..07fe620d161e9a37e0f169856a954fced9c2a91f 100644
--- a/components/net/sf/briar/db/Database.java
+++ b/components/net/sf/briar/db/Database.java
@@ -132,6 +132,13 @@ interface Database<T> {
 	 */
 	TransportIndex addTransport(T txn, TransportId t) throws DbException;
 
+	/**
+	 * Makes the given group visible to the given contact.
+	 * <p>
+	 * Locking: contact read, subscription write.
+	 */
+	void addVisibility(T txn, ContactId c, GroupId g) throws DbException;
+
 	/**
 	 * Returns true if the database contains the given contact.
 	 * <p>
@@ -514,6 +521,13 @@ interface Database<T> {
 	 */
 	void removeSubscription(T txn, GroupId g) throws DbException;
 
+	/**
+	 * Makes the given group invisible to the given contact.
+	 * <p>
+	 * Locking: contact read, subscription write.
+	 */
+	void removeVisibility(T txn, ContactId c, GroupId g) throws DbException;
+
 	/**
 	 * Sets the configuration for the given transport, replacing any existing
 	 * configuration for that transport.
@@ -642,13 +656,4 @@ interface Database<T> {
 	 */
 	void setTransportsSent(T txn, ContactId c, long timestamp)
 	throws DbException;
-
-	/**
-	 * Makes the given group visible to the given set of contacts and invisible
-	 * to any other contacts.
-	 * <p>
-	 * Locking: contact read, subscription write.
-	 */
-	void setVisibility(T txn, GroupId g, Collection<ContactId> visible)
-	throws DbException;
 }
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index 6318d29241be83a3b51672af5e4e054eb9ae3bdd..271f37fdc4c3e0bf3b258c2df2b6fe21ff6feb40 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -1425,7 +1425,7 @@ DatabaseCleaner.Callback {
 
 	public void setVisibility(GroupId g, Collection<ContactId> visible)
 	throws DbException {
-		List<ContactId> affected;
+		List<ContactId> affected = new ArrayList<ContactId>();
 		contactLock.readLock().lock();
 		try {
 			subscriptionLock.writeLock().lock();
@@ -1433,23 +1433,25 @@ DatabaseCleaner.Callback {
 				T txn = db.startTransaction();
 				try {
 					// Use HashSets for O(1) lookups, O(n) overall running time
-					HashSet<ContactId> then, now;
-					// Retrieve the group's current visibility
-					then = new HashSet<ContactId>(db.getVisibility(txn, g));
-					// Don't try to make the group visible to ex-contacts
-					now = new HashSet<ContactId>(visible);
-					now.retainAll(new HashSet<ContactId>(db.getContacts(txn)));
-					db.setVisibility(txn, g, now);
-					// Work out which contacts were affected by the change
-					affected = new ArrayList<ContactId>();
-					for(ContactId c : then) {
-						if(!now.contains(c)) affected.add(c);
+					visible = new HashSet<ContactId>(visible);
+					HashSet<ContactId> oldVisible =
+							new HashSet<ContactId>(db.getVisibility(txn, g));
+					// Set the group's visibility for each current contact
+					for(ContactId c : db.getContacts(txn)) {
+						boolean then = oldVisible.contains(c);
+						boolean now = visible.contains(c);
+						if(!then && now) {
+							db.addVisibility(txn, c, g);
+							affected.add(c);
+						} else if(then && !now) {
+							db.removeVisibility(txn, c, g);
+							affected.add(c);
+						}
 					}
-					for(ContactId c : now) {
-						if(!then.contains(c)) affected.add(c);
+					if(!affected.isEmpty()) {
+						db.setSubscriptionsModified(txn, affected,
+								System.currentTimeMillis());
 					}
-					db.setSubscriptionsModified(txn, affected,
-							System.currentTimeMillis());
 					db.commitTransaction(txn);
 				} catch(DbException e) {
 					db.abortTransaction(txn);
diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java
index c2ec4eeb0106e520bb07187911b01b976035de2c..916c30ed5c4612b79e2f1579e6ac76d9f3552e03 100644
--- a/components/net/sf/briar/db/JdbcDatabase.java
+++ b/components/net/sf/briar/db/JdbcDatabase.java
@@ -59,12 +59,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		+ " start BIGINT NOT NULL,"
 		+ " PRIMARY KEY (groupId))";
 
-	private static final String CREATE_SUBSCRIPTION_IDS =
-		"CREATE TABLE subscriptionIds"
-		+ " (groupId HASH," // Null for the head of the list
-		+ " nextId HASH," // Null for the tail of the list
-		+ " deleted BIGINT NOT NULL)";
-
 	private static final String CREATE_CONTACTS =
 		"CREATE TABLE contacts"
 		+ " (contactId COUNTER,"
@@ -104,12 +98,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_VISIBILITIES =
 		"CREATE TABLE visibilities"
-		+ " (groupId HASH NOT NULL,"
-		+ " contactId INT NOT NULL,"
-		+ " PRIMARY KEY (groupId, contactId),"
-		+ " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)"
-		+ " ON DELETE CASCADE,"
+		+ " (contactId INT NOT NULL,"
+		+ " groupId HASH," // Null for the head of the linked list
+		+ " nextId HASH," // Null for the tail of the linked list
+		+ " deleted BIGINT NOT NULL,"
 		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+		+ " ON DELETE CASCADE,"
+		+ " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)"
 		+ " ON DELETE CASCADE)";
 
 	private static final String INDEX_VISIBILITIES_BY_GROUP =
@@ -333,7 +328,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			s = txn.createStatement();
 			s.executeUpdate(insertTypeNames(CREATE_SUBSCRIPTIONS));
-			s.executeUpdate(insertTypeNames(CREATE_SUBSCRIPTION_IDS));
 			s.executeUpdate(insertTypeNames(CREATE_CONTACTS));
 			s.executeUpdate(insertTypeNames(CREATE_MESSAGES));
 			s.executeUpdate(INDEX_MESSAGES_BY_PARENT);
@@ -729,7 +723,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	public void addSubscription(Connection txn, Group g) throws DbException {
 		PreparedStatement ps = null;
-		ResultSet rs = null;
 		try {
 			// Add the group to the subscriptions table
 			String sql = "INSERT INTO subscriptions"
@@ -743,11 +736,62 @@ abstract class JdbcDatabase implements Database<Connection> {
 			int affected = ps.executeUpdate();
 			if(affected != 1) throw new DbStateException();
 			ps.close();
+		} catch(SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	public TransportIndex addTransport(Connection txn, TransportId t)
+	throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			// Allocate a new index
+			String sql = "INSERT INTO transports (transportId) VALUES (?)";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, t.getBytes());
+			int affected = ps.executeUpdate();
+			if(affected != 1) throw new DbStateException();
+			ps.close();
+			// If the new index is in range, return it
+			sql = "SELECT index FROM transports WHERE transportId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, t.getBytes());
+			rs = ps.executeQuery();
+			if(!rs.next()) throw new DbStateException();
+			int i = rs.getInt(1);
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			if(i < ProtocolConstants.MAX_TRANSPORTS)
+				return new TransportIndex(i);
+			// Too many transports - delete the new index and return null
+			sql = "DELETE FROM transports WHERE transportId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, t.getBytes());
+			affected = ps.executeUpdate();
+			if(affected != 1) throw new DbStateException();
+			return null;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	public void addVisibility(Connection txn, ContactId c, GroupId g)
+	throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
 			// Insert the group ID into the linked list
-			byte[] id = g.getId().getBytes();
-			sql = "SELECT groupId, nextId, deleted FROM subscriptionIds"
+			byte[] id = g.getBytes();
+			String sql = "SELECT groupId, nextId, deleted FROM visibilities"
+				+ " WHERE contactId = ?"
 				+ " ORDER BY groupId";
 			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
 			rs = ps.executeQuery();
 			if(rs.next()) {
 				// The head pointer of the list exists
@@ -768,29 +812,33 @@ abstract class JdbcDatabase implements Database<Connection> {
 				// Update the previous element
 				if(groupId == null) {
 					// Inserting at the head of the list
-					sql = "UPDATE subscriptionIds SET nextId = ?"
-						+ " WHERE groupId IS NULL";
+					sql = "UPDATE visibilities SET nextId = ?"
+						+ " WHERE contactId = ? AND groupId IS NULL";
 					ps = txn.prepareStatement(sql);
 					ps.setBytes(1, id);
+					ps.setInt(2, c.getInt());
 				} else {
 					// Inserting in the middle or at the tail of the list
-					sql = "UPDATE subscriptionIds SET nextId = ?"
-						+ " WHERE groupId = ?";
+					sql = "UPDATE visibilities SET nextId = ?"
+						+ " WHERE contactId = ? AND groupId = ?";
 					ps = txn.prepareStatement(sql);
 					ps.setBytes(1, id);
-					ps.setBytes(2, groupId);
+					ps.setInt(2, c.getInt());
+					ps.setBytes(3, groupId);
 				}
-				affected = ps.executeUpdate();
+				int affected = ps.executeUpdate();
 				if(affected != 1) throw new DbStateException();
 				ps.close();
 				// Insert the new element
-				sql = "INSERT INTO subscriptionIds (groupId, nextId, deleted)"
-						+ " VALUES (?, ?, ?)";
+				sql = "INSERT INTO visibilities"
+					+ " (contactId, groupId, nextId, deleted)"
+					+ " VALUES (?, ?, ?, ?)";
 				ps = txn.prepareStatement(sql);
-				ps.setBytes(1, id);
-				if(nextId == null) ps.setNull(2, Types.BINARY); // At the tail
-				else ps.setBytes(2, nextId); // In the middle
-				ps.setLong(3, deleted);
+				ps.setInt(1, c.getInt());
+				ps.setBytes(2, id);
+				if(nextId == null) ps.setNull(3, Types.BINARY); // At the tail
+				else ps.setBytes(3, nextId); // In the middle
+				ps.setLong(4, deleted);
 				affected = ps.executeUpdate();
 				if(affected != 1) throw new DbStateException();
 				ps.close();
@@ -798,17 +846,19 @@ abstract class JdbcDatabase implements Database<Connection> {
 				// The head pointer of the list does not exist
 				rs.close();
 				ps.close();
-				sql = "INSERT INTO subscriptionIds (nextId, deleted)"
-					+ " VALUES (?, ZERO())";
+				sql = "INSERT INTO visibilities (contactId, nextId, deleted)"
+					+ " VALUES (?, ?, ZERO())";
 				ps = txn.prepareStatement(sql);
-				ps.setBytes(1, id);
-				affected = ps.executeUpdate();
+				ps.setInt(1, c.getInt());
+				ps.setBytes(2, id);
+				int affected = ps.executeUpdate();
 				if(affected != 1) throw new DbStateException();
 				ps.close();
-				sql = "INSERT INTO subscriptionIds (groupId, deleted)"
-					+ " VALUES (?, ZERO())";
+				sql = "INSERT INTO visibilities (contactId, groupId, deleted)"
+					+ " VALUES (?, ?, ZERO())";
 				ps = txn.prepareStatement(sql);
-				ps.setBytes(1, id);
+				ps.setInt(1, c.getInt());
+				ps.setBytes(2, id);
 				affected = ps.executeUpdate();
 				if(affected != 1) throw new DbStateException();
 				ps.close();
@@ -820,44 +870,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public TransportIndex addTransport(Connection txn, TransportId t)
-	throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			// Allocate a new index
-			String sql = "INSERT INTO transports (transportId) VALUES (?)";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, t.getBytes());
-			int affected = ps.executeUpdate();
-			if(affected != 1) throw new DbStateException();
-			ps.close();
-			// If the new index is in range, return it
-			sql = "SELECT index FROM transports WHERE transportId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, t.getBytes());
-			rs = ps.executeQuery();
-			if(!rs.next()) throw new DbStateException();
-			int i = rs.getInt(1);
-			if(rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			if(i < ProtocolConstants.MAX_TRANSPORTS)
-				return new TransportIndex(i);
-			// Too many transports - delete the new index and return null
-			sql = "DELETE FROM transports WHERE transportId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, t.getBytes());
-			affected = ps.executeUpdate();
-			if(affected != 1) throw new DbStateException();
-			return null;
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public boolean containsContact(Connection txn, ContactId c)
 	throws DbException {
 		PreparedStatement ps = null;
@@ -2176,39 +2188,82 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	public void removeSubscription(Connection txn, GroupId g)
 	throws DbException {
-		PreparedStatement ps = null;
+		PreparedStatement ps = null, ps1 = null;
 		ResultSet rs = null;
 		try {
+			// Remove the group ID from the visibility lists
+			long now = System.currentTimeMillis();
+			String sql = "SELECT contactId, nextId FROM visibilities"
+				+ " WHERE groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, g.getBytes());
+			rs = ps.executeQuery();
+			while(rs.next()) {
+				int contactId = rs.getInt(1);
+				byte[] nextId = rs.getBytes(2);
+				sql = "UPDATE visibilities SET nextId = ?, deleted = ?"
+					+ " WHERE contactId = ? AND nextId = ?";
+				ps1 = txn.prepareStatement(sql);
+				if(nextId == null) ps1.setNull(1, Types.BINARY); // At the tail
+				else ps1.setBytes(1, nextId); // At the head or in the middle
+				ps1.setLong(2, now);
+				ps1.setInt(3, contactId);
+				ps1.setBytes(4, g.getBytes());
+				int affected = ps1.executeUpdate();
+				if(affected != 1) throw new DbStateException();
+				ps1.close();
+			}
+			rs.close();
+			ps.close();
 			// Remove the group from the subscriptions table
-			String sql = "DELETE FROM subscriptions WHERE groupId = ?";
+			sql = "DELETE FROM subscriptions WHERE groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			int affected = ps.executeUpdate();
 			if(affected != 1) throw new DbStateException();
 			ps.close();
+		} catch(SQLException e) {
+			e.printStackTrace();
+			tryToClose(rs);
+			tryToClose(ps);
+			tryToClose(ps1);
+			throw new DbException(e);
+		}
+	}
+
+	public void removeVisibility(Connection txn, ContactId c, GroupId g)
+	throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
 			// Remove the group ID from the linked list
-			sql = "SELECT nextId FROM subscriptionIds WHERE groupId = ?";
+			String sql = "SELECT nextId FROM visibilities"
+				+ " WHERE contactId = ? AND groupId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
+			ps.setInt(1, c.getInt());
+			ps.setBytes(2, g.getBytes());
 			rs = ps.executeQuery();
 			if(!rs.next()) throw new DbStateException();
 			byte[] nextId = rs.getBytes(1);
 			if(rs.next()) throw new DbStateException();
 			rs.close();
 			ps.close();
-			sql = "DELETE FROM subscriptionIds WHERE groupId = ?";
+			sql = "DELETE FROM visibilities"
+				+ " WHERE contactId = ? AND groupId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
-			affected = ps.executeUpdate();
+			ps.setInt(1, c.getInt());
+			ps.setBytes(2, g.getBytes());
+			int affected = ps.executeUpdate();
 			if(affected != 1) throw new DbStateException();
 			ps.close();
-			sql = "UPDATE subscriptionIds SET nextId = ?, deleted = ?"
-				+ " WHERE nextId = ?";
+			sql = "UPDATE visibilities SET nextId = ?, deleted = ?"
+				+ " WHERE contactId = ? AND nextId = ?";
 			ps = txn.prepareStatement(sql);
 			if(nextId == null) ps.setNull(1, Types.BINARY); // At the tail
 			else ps.setBytes(1, nextId); // At the head or in the middle
 			ps.setLong(2, System.currentTimeMillis());
-			ps.setBytes(3, g.getBytes());
+			ps.setInt(3, c.getInt());
+			ps.setBytes(4, g.getBytes());
 			affected = ps.executeUpdate();
 			if(affected != 1) throw new DbStateException();
 			ps.close();
@@ -2800,36 +2855,4 @@ abstract class JdbcDatabase implements Database<Connection> {
 			throw new DbException(e);
 		}
 	}
-
-	public void setVisibility(Connection txn, GroupId g,
-			Collection<ContactId> visible) throws DbException {
-		PreparedStatement ps = null;
-		try {
-			// Delete any existing visibilities
-			String sql = "DELETE FROM visibilities where groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
-			ps.executeUpdate();
-			ps.close();
-			// Store the new visibilities
-			sql = "INSERT INTO visibilities (groupId, contactId)"
-				+ " VALUES (?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
-			for(ContactId c : visible) {
-				ps.setInt(2, c.getInt());
-				ps.addBatch();
-			}
-			int[] batchAffected = ps.executeBatch();
-			if(batchAffected.length != visible.size())
-				throw new DbStateException();
-			for(int i = 0; i < batchAffected.length; i++) {
-				if(batchAffected[i] != 1) throw new DbStateException();
-			}
-			ps.close();
-		} catch(SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
 }
diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java
index 9b86bf2a3abf49398c6c35b542e16a1e146a39ff..f10d3cc74b4815620454c86568f513c8642ce117 100644
--- a/test/net/sf/briar/db/DatabaseComponentTest.java
+++ b/test/net/sf/briar/db/DatabaseComponentTest.java
@@ -22,6 +22,7 @@ import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.MessagesAddedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
+import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent;
 import net.sf.briar.api.db.event.TransportAddedEvent;
 import net.sf.briar.api.lifecycle.ShutdownManager;
 import net.sf.briar.api.protocol.Ack;
@@ -679,14 +680,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 	public void testGenerateBatch() throws Exception {
 		final MessageId messageId1 = new MessageId(TestUtils.getRandomId());
 		final byte[] raw1 = new byte[size];
-		final Collection<MessageId> sendable = Arrays.asList(new MessageId[] {
-				messageId,
-				messageId1
-		});
-		final Collection<byte[]> messages = Arrays.asList(new byte[][] {
-				raw,
-				raw1
-		});
+		final Collection<MessageId> sendable =
+				Arrays.asList(new MessageId[] {messageId, messageId1});
+		final Collection<byte[]> messages =
+				Arrays.asList(new byte[][] {raw, raw1});
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -733,9 +730,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		requested.add(messageId);
 		requested.add(messageId1);
 		requested.add(messageId2);
-		final Collection<byte[]> msgs = Arrays.asList(new byte[][] {
-				raw1
-		});
+		final Collection<byte[]> msgs = Arrays.asList(new byte[][] {raw1});
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1541,4 +1536,70 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 		context.assertIsSatisfied();
 	}
+
+	@Test
+	public void testVisibilityChangedCallsListeners() throws Exception {
+		final ContactId contactId1 = new ContactId(234);
+		final Collection<ContactId> both =
+				Arrays.asList(new ContactId[] {contactId, contactId1});
+		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);
+		final PacketFactory packetFactory = context.mock(PacketFactory.class);
+		final DatabaseListener listener = context.mock(DatabaseListener.class);
+		context.checking(new Expectations() {{
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).getVisibility(txn, groupId);
+			will(returnValue(both));
+			oneOf(database).getContacts(txn);
+			will(returnValue(both));
+			oneOf(database).removeVisibility(txn, contactId1, groupId);
+			oneOf(database).setSubscriptionsModified(with(txn),
+					with(Collections.singletonList(contactId1)),
+					with(any(long.class)));
+			oneOf(database).commitTransaction(txn);
+			oneOf(listener).eventOccurred(with(any(
+					SubscriptionsUpdatedEvent.class)));
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				shutdown, packetFactory);
+
+		db.addListener(listener);
+		db.setVisibility(groupId, Collections.singletonList(contactId));
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testVisibilityUnchangedDoesNotCallListeners() throws Exception {
+		final ContactId contactId1 = new ContactId(234);
+		final Collection<ContactId> both =
+				Arrays.asList(new ContactId[] {contactId, contactId1});
+		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);
+		final PacketFactory packetFactory = context.mock(PacketFactory.class);
+		final DatabaseListener listener = context.mock(DatabaseListener.class);
+		context.checking(new Expectations() {{
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).getVisibility(txn, groupId);
+			will(returnValue(both));
+			oneOf(database).getContacts(txn);
+			will(returnValue(both));
+			oneOf(database).commitTransaction(txn);
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				shutdown, packetFactory);
+
+		db.addListener(listener);
+		db.setVisibility(groupId, both);
+
+		context.assertIsSatisfied();
+	}
 }
diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java
index 6cdad47d48ccf04dd0c7dcae33e26680a9f0ef36..310c3219430994267840a129afce2a12850a7c93 100644
--- a/test/net/sf/briar/db/H2DatabaseTest.java
+++ b/test/net/sf/briar/db/H2DatabaseTest.java
@@ -355,7 +355,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
 		db.setStatus(txn, contactId, messageId, Status.NEW);
@@ -393,7 +393,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
 		db.setSendability(txn, messageId, 1);
@@ -435,7 +435,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.addGroupMessage(txn, message);
 		db.setSendability(txn, messageId, 1);
 		db.setStatus(txn, contactId, messageId, Status.NEW);
@@ -474,7 +474,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.addGroupMessage(txn, message);
 		db.setSendability(txn, messageId, 1);
 		db.setStatus(txn, contactId, messageId, Status.NEW);
@@ -509,7 +509,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
 		db.setSendability(txn, messageId, 1);
@@ -553,7 +553,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertFalse(it.hasNext());
 
 		// Making the subscription visible should make the message sendable
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		assertTrue(db.hasSendableMessages(txn, contactId));
 		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertTrue(it.hasNext());
@@ -674,7 +674,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
 		db.setSendability(txn, messageId, 1);
@@ -713,7 +713,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
 		db.setSendability(txn, messageId, 1);
@@ -1274,7 +1274,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// the message is older than the contact's subscription
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		Map<Group, Long> subs = Collections.singletonMap(group, timestamp + 1);
 		db.setSubscriptions(txn, contactId, subs, 1);
 		db.addGroupMessage(txn, message);
@@ -1298,7 +1298,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
 
@@ -1323,7 +1323,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact and subscribe to a group
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 
 		// The message is not in the database
@@ -1398,7 +1398,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
 
@@ -1420,7 +1420,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact, subscribe to a group and store a message
 		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
 		db.addSubscription(txn, group);
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
 
@@ -1444,11 +1444,11 @@ public class H2DatabaseTest extends BriarTestCase {
 		// The group should not be visible to the contact
 		assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId));
 		// Make the group visible to the contact
-		db.setVisibility(txn, groupId, Collections.singletonList(contactId));
+		db.addVisibility(txn, contactId, groupId);
 		assertEquals(Collections.singletonList(contactId),
 				db.getVisibility(txn, groupId));
 		// Make the group invisible again
-		db.setVisibility(txn, groupId, Collections.<ContactId>emptyList());
+		db.removeVisibility(txn, contactId, groupId);
 		assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId));
 
 		db.commitTransaction(txn);
@@ -1895,12 +1895,20 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add the groups to the database
+		// Subscribe to the groups and add a contact
 		for(Group g : groups) db.addSubscription(txn, g);
+		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+
+		// Make the groups visible to the contact
+		Collections.shuffle(groups);
+		for(Group g : groups) db.addVisibility(txn, contactId, g.getId());
 
-		// Remove the groups in a different order
+		// Make the groups invisible to the contact and remove them
 		Collections.shuffle(groups);
-		for(Group g : groups) db.removeSubscription(txn, g.getId());
+		for(Group g : groups) {
+			db.removeVisibility(txn, contactId, g.getId());
+			db.removeSubscription(txn, g.getId());
+		}
 
 		db.commitTransaction(txn);
 		db.close();